Repository: pallets/click (python)

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.

Pull request description:
rename resultcallback to result_callback
This is a clearer name for the decorator. The attribute is renamed to `_result_callback`, the only mention in the docs already implied that it shouldn't be used directly and should be set with the parameter or decorator.

- fixes #1160 

<!--
Ensure each step in CONTRIBUTING.rst is complete by adding an "x" to
each box below.

If only docs were changed, these aren't relevant and can be removed.
-->

Checklist:

- [x] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
- [x] Add or update relevant docs, in the docs folder and in code.
- [x] Add an entry in `CHANGES.rst` summarizing the change and linking to the issue.
- [x] Add `.. versionchanged::` entries in any relevant code docs.
- [x] Run `pre-commit` hooks and fix any issues.
- [x] Run `pytest` and `tox`, no tests failed.


Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index a94f5f7..bc5c9c1 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -206,6 +206,8 @@ Unreleased
 -   Add all kwargs passed to ``Context.invoke()`` to ``ctx.params``.
     Fixes an inconsistency when nesting ``Context.forward()`` calls.
     :issue:`1568`
+-   The ``MultiCommand.resultcallback`` decorator is renamed to
+    ``result_callback``. The old name is deprecated. :issue:`1160`
 
 
 Version 7.1.2
diff --git a/docs/commands.rst b/docs/commands.rst
index 2bb9111..5c02123 100644
--- a/docs/commands.rst
+++ b/docs/commands.rst
@@ -351,7 +351,7 @@ how to do its processing.  At that point it then returns a processing
 function and returns.
 
 Where do the returned functions go?  The chained multicommand can register
-a callback with :meth:`MultiCommand.resultcallback` that goes over all
+a callback with :meth:`MultiCommand.result_callback` that goes over all
 these functions and then invoke them.
 
 To make this a bit more concrete consider this example:
@@ -363,7 +363,7 @@ To make this a bit more concrete consider this example:
     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:
diff --git a/docs/upgrading.rst b/docs/upgrading.rst
index ea082bb..c6fa554 100644
--- a/docs/upgrading.rst
+++ b/docs/upgrading.rst
@@ -90,7 +90,7 @@ restored.
 If you do require the know which exact commands will be invoked there are
 different ways to cope with this.  The first one is to let the subcommands
 all return functions and then to invoke the functions in a
-:meth:`Context.resultcallback`.
+:meth:`Context.result_callback`.
 
 
 .. _upgrade-to-2.0:
diff --git a/examples/imagepipe/imagepipe.py b/examples/imagepipe/imagepipe.py
index 57432fa..e2d2f03 100644
--- 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
index 2a170c9..e7e33f5 100644
--- a/src/click/core.py
+++ b/src/click/core.py
@@ -309,7 +309,7 @@ class Context:
         #: 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 @@ class MultiCommand(Command):
                 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 @@ class MultiCommand(Command):
         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 @@ class MultiCommand(Command):
             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 @@ class MultiCommand(Command):
 
     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
index 74a04f1..23520a0 100644
--- 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: