Repository: pallets/click

Issue:
Docs: MultiCommand.resultcallback vs MultiCommand.result_callback
The [commands](https://click.palletsprojects.com/en/7.x/commands/) doc page shows usage of `MultiCommand.resultcallback` under the header "Multi Command Pipelines" as being a callable itself, that is passed all the associated callbacks with the `chain`ed group.

>Where do the returned functions go? The chained multicommand can register a callback with `MultiCommand.resultcallback()` that goes over all these functions and then invoke them.

However, at the bottom of that same doc page, it seems to imply that `MultiCommand.result_callback` is the same.

>Return values of groups can be processed through a `MultiCommand.result_callback`. This is invoked with the list of all return values in chain mode, or the single return value in case of non chained commands.

The API seems to show that [`result_callback`](https://click.palletsprojects.com/en/7.x/api/#click.MultiCommand.result_callback) is an attribute that stores the callback itself, while [`resultcallback`](https://click.palletsprojects.com/en/7.x/api/#click.MultiCommand.resultcallback) is a function that sets what the callback should be. The naming here is confusing, and it's not clear which one is which. In addition: referring to *both* of them in the documentation (without clarifying which is which and why) throws an additional spanner into the works.

I recommend a rename for a permanent solution (`result_callback -> _result_callback`) and more explicit documentation in the short-term.


Discussion:
 I don't think adding a _ really improves clarity. 
The parentheses imply resultcallback is a function, and the dot lookup implies result_callback is an attribute. 

How would you suggest the documentation be improved?

I'm renaming the decorator to `@result_callback()` and the attribute to `_result_callback`, since it's only used to store the callback, it doesn't appear to be intended to be set directly as a public API. The docs will not mention the attribute and will only mention the renamed decorator.

Resolving patch:
diff --git a/examples/imagepipe/imagepipe.py b/examples/imagepipe/imagepipe.py
--- a/examples/imagepipe/imagepipe.py
+++ b/examples/imagepipe/imagepipe.py
@@ -20,7 +20,7 @@ def cli():
     """
 
 
-@cli.resultcallback()
+@cli.result_callback()
 def process_commands(processors):
     """This result callback is invoked with an iterable of all the chained
     subcommands.  As in this example each subcommand returns a function
diff --git a/src/click/core.py b/src/click/core.py
--- a/src/click/core.py
+++ b/src/click/core.py
@@ -309,7 +309,7 @@ def __init__(
         #: If chaining is enabled this will be set to ``'*'`` in case
         #: any commands are executed.  It is however not possible to
         #: figure out which ones.  If you require this knowledge you
-        #: should use a :func:`resultcallback`.
+        #: should use a :func:`result_callback`.
         self.invoked_subcommand = None
 
         if terminal_width is None and parent is not None:
@@ -1363,8 +1363,9 @@ class MultiCommand(Command):
                   is enabled.  This restricts the form of commands in that
                   they cannot have optional arguments but it allows
                   multiple commands to be chained together.
-    :param result_callback: the result callback to attach to this multi
-                            command.
+    :param result_callback: The result callback to attach to this multi
+        command. This can be set or changed later with the
+        :meth:`result_callback` decorator.
     """
 
     allow_extra_args = True
@@ -1392,9 +1393,9 @@ def __init__(
                 subcommand_metavar = SUBCOMMAND_METAVAR
         self.subcommand_metavar = subcommand_metavar
         self.chain = chain
-        #: The result callback that is stored.  This can be set or
-        #: overridden with the :func:`resultcallback` decorator.
-        self.result_callback = result_callback
+        # The result callback that is stored. This can be set or
+        # overridden with the :func:`result_callback` decorator.
+        self._result_callback = result_callback
 
         if self.chain:
             for param in self.params:
@@ -1427,7 +1428,7 @@ def format_options(self, ctx, formatter):
         super().format_options(ctx, formatter)
         self.format_commands(ctx, formatter)
 
-    def resultcallback(self, replace=False):
+    def result_callback(self, replace=False):
         """Adds a result callback to the command.  By default if a
         result callback is already registered this will chain them but
         this can be disabled with the `replace` parameter.  The result
@@ -1443,30 +1444,45 @@ def resultcallback(self, replace=False):
             def cli(input):
                 return 42
 
-            @cli.resultcallback()
+            @cli.result_callback()
             def process_result(result, input):
                 return result + input
 
         :param replace: if set to `True` an already existing result
                         callback will be removed.
 
+        .. versionchanged:: 8.0
+            Renamed from ``resultcallback``.
+
         .. versionadded:: 3.0
         """
 
         def decorator(f):
-            old_callback = self.result_callback
+            old_callback = self._result_callback
+
             if old_callback is None or replace:
-                self.result_callback = f
+                self._result_callback = f
                 return f
 
             def function(__value, *args, **kwargs):
                 return f(old_callback(__value, *args, **kwargs), *args, **kwargs)
 
-            self.result_callback = rv = update_wrapper(function, f)
+            self._result_callback = rv = update_wrapper(function, f)
             return rv
 
         return decorator
 
+    def resultcallback(self, replace=False):
+        import warnings
+
+        warnings.warn(
+            "'resultcallback' has been renamed to 'result_callback'."
+            " The old name will be removed in Click 8.1.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return self.result_callback(replace=replace)
+
     def format_commands(self, ctx, formatter):
         """Extra format methods for multi methods that adds all the commands
         after the options.
@@ -1512,8 +1528,8 @@ def parse_args(self, ctx, args):
 
     def invoke(self, ctx):
         def _process_result(value):
-            if self.result_callback is not None:
-                value = ctx.invoke(self.result_callback, value, **ctx.params)
+            if self._result_callback is not None:
+                value = ctx.invoke(self._result_callback, value, **ctx.params)
             return value
 
         if not ctx.protected_args:


Test patch:
diff --git a/tests/test_chain.py b/tests/test_chain.py
--- a/tests/test_chain.py
+++ b/tests/test_chain.py
@@ -99,7 +99,7 @@ def test_no_command_result_callback(runner, chain, expect):
     def cli():
         pass
 
-    @cli.resultcallback()
+    @cli.result_callback()
     def process_result(result):
         click.echo(str(result), nl=False)
 
@@ -133,7 +133,7 @@ def test_pipeline(runner):
     def cli(input):
         pass
 
-    @cli.resultcallback()
+    @cli.result_callback()
     def process_pipeline(processors, input):
         iterator = (x.rstrip("\r\n") for x in input)
         for processor in processors: