Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

non-chained group invoked without subcommand invokes result callback #1621

Merged
merged 1 commit into from
Aug 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ Unreleased
``Context.invoked_subcommand`` consistent when using patterns like
``AliasedGroup``. :issue:`1422`
- The ``BOOL`` type accepts the values "on" and "off". :issue:`1629`
- A ``Group`` with ``invoke_without_command=True`` will always invoke
its result callback. :issue:`1178`


Version 7.1.2
Expand Down
19 changes: 7 additions & 12 deletions src/click/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1170,7 +1170,7 @@ def format_options(self, ctx, formatter):
self.format_commands(ctx, formatter)

def resultcallback(self, replace=False):
"""Adds a result callback to the chain command. By default if a
"""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
callback is invoked with the return value of the subcommand
Expand All @@ -1189,10 +1189,10 @@ def cli(input):
def process_result(result, input):
return result + input

.. versionadded:: 3.0

:param replace: if set to `True` an already existing result
callback will be removed.

.. versionadded:: 3.0
"""

def decorator(f):
Expand Down Expand Up @@ -1258,18 +1258,13 @@ def _process_result(value):
return value

if not ctx.protected_args:
# If we are invoked without command the chain flag controls
# how this happens. If we are not in chain mode, the return
# value here is the return value of the command.
# If however we are in chain mode, the return value is the
# return value of the result processor invoked with an empty
# list (which means that no subcommand actually was executed).
if self.invoke_without_command:
if not self.chain:
return Command.invoke(self, ctx)
# No subcommand was invoked, so the result callback is
# invoked with None for regular groups, or an empty list
# for chained groups.
with ctx:
Command.invoke(self, ctx)
return _process_result([])
return _process_result([] if self.chain else None)
ctx.fail("Missing command.")

# Fetch args back out
Expand Down
19 changes: 19 additions & 0 deletions tests/test_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,25 @@ def bdist(format):
assert result.output.splitlines() == ["bdist called 1", "sdist called 2"]


@pytest.mark.parametrize(("chain", "expect"), [(False, "None"), (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 ``[]``.
"""

@click.group(invoke_without_command=True, chain=chain)
def cli():
pass

@cli.resultcallback()
def process_result(result):
click.echo(str(result), nl=False)

result = runner.invoke(cli, [])
assert result.output == expect


def test_chaining_with_arguments(runner):
@click.group(chain=True)
def cli():
Expand Down