Skip to content

Commit

Permalink
Merge pull request #1098 from sirosen/non-standalone-exit
Browse files Browse the repository at this point in the history
Allow non-standalone ctx.exit() calls to return values rather than call sys.exit()
  • Loading branch information
davidism authored Aug 28, 2018
2 parents 011fd62 + a94c0be commit 8df9a6b
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 2 deletions.
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Version 7.0

(upcoming release with new features, release date to be decided)

- Non-standalone calls to Context.exit return the exit code, rather than
calling ``sys.exit`` (`#667`_)(`#533`_)
- Updated test env matrix. (`#1027`_)
- Fixes a ``ZeroDivisionError`` in ``ProgressBar.make_step``,
when the arg passed to the first call of ``ProgressBar.update`` is 0. (`#1012`_)(`#447`_)
Expand Down
24 changes: 22 additions & 2 deletions click/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from .types import convert_type, IntRange, BOOL
from .utils import make_str, make_default_short_help, echo, get_os_args
from .exceptions import ClickException, UsageError, BadParameter, Abort, \
MissingParameter
MissingParameter, Exit
from .termui import prompt, confirm, style
from .formatting import HelpFormatter, join_options
from .parser import OptionParser, split_opt
Expand Down Expand Up @@ -498,7 +498,7 @@ def abort(self):

def exit(self, code=0):
"""Exits the application with a given exit code."""
sys.exit(code)
raise Exit(code)

def get_usage(self):
"""Helper method to get formatted usage string for the current
Expand Down Expand Up @@ -714,6 +714,13 @@ def main(self, args=None, prog_name=None, complete_var=None,
rv = self.invoke(ctx)
if not standalone_mode:
return rv
# it's not safe to `ctx.exit(rv)` here!
# note that `rv` may actually contain data like "1" which
# has obvious effects
# more subtle case: `rv=[None, None]` can come out of
# chained commands which all returned `None` -- so it's not
# even always obvious that `rv` indicates success/failure
# by its truthiness/falsiness
ctx.exit()
except (EOFError, KeyboardInterrupt):
echo(file=sys.stderr)
Expand All @@ -728,6 +735,19 @@ def main(self, args=None, prog_name=None, complete_var=None,
sys.exit(1)
else:
raise
except Exit as e:
if standalone_mode:
sys.exit(e.exit_code)
else:
# in non-standalone mode, return the exit code
# note that this is only reached if `self.invoke` above raises
# an Exit explicitly -- thus bypassing the check there which
# would return its result
# the results of non-standalone execution may therefore be
# somewhat ambiguous: if there are codepaths which lead to
# `ctx.exit(1)` and to `return 1`, the caller won't be able to
# tell the difference between the two
return e.exit_code
except Abort:
if not standalone_mode:
raise
Expand Down
10 changes: 10 additions & 0 deletions click/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,13 @@ def format_message(self):

class Abort(RuntimeError):
"""An internal signalling exception that signals Click to abort."""


class Exit(RuntimeError):
"""An exception that indicates that the application should exit with some
status code.
:param code: the status code to exit with.
"""
def __init__(self, code=0):
self.exit_code = code
17 changes: 17 additions & 0 deletions tests/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ def test_close_before_pop(runner):
@click.pass_context
def cli(ctx):
ctx.obj = 'test'

@ctx.call_on_close
def foo():
assert click.get_current_context().obj == 'test'
Expand Down Expand Up @@ -239,3 +240,19 @@ def test2(ctx, foo):
result = runner.invoke(cli, ['test2'])
assert not result.exception
assert result.output == 'foocmd\n'


def test_exit_not_standalone():
@click.command()
@click.pass_context
def cli(ctx):
ctx.exit(1)

assert cli.main([], 'test_exit_not_standalone', standalone_mode=False) == 1

@click.command()
@click.pass_context
def cli(ctx):
ctx.exit(0)

assert cli.main([], 'test_exit_not_standalone', standalone_mode=False) == 0
30 changes: 30 additions & 0 deletions tests/test_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,16 +153,34 @@ def cli_string():
click.echo('hello world')
sys.exit('error')

@click.command()
@click.pass_context
def cli_string_ctx_exit(ctx):
click.echo('hello world')
ctx.exit('error')

@click.command()
def cli_int():
click.echo('hello world')
sys.exit(1)

@click.command()
@click.pass_context
def cli_int_ctx_exit(ctx):
click.echo('hello world')
ctx.exit(1)

@click.command()
def cli_float():
click.echo('hello world')
sys.exit(1.0)

@click.command()
@click.pass_context
def cli_float_ctx_exit(ctx):
click.echo('hello world')
ctx.exit(1.0)

@click.command()
def cli_no_error():
click.echo('hello world')
Expand All @@ -173,14 +191,26 @@ def cli_no_error():
assert result.exit_code == 1
assert result.output == 'hello world\nerror\n'

result = runner.invoke(cli_string_ctx_exit)
assert result.exit_code == 1
assert result.output == 'hello world\nerror\n'

result = runner.invoke(cli_int)
assert result.exit_code == 1
assert result.output == 'hello world\n'

result = runner.invoke(cli_int_ctx_exit)
assert result.exit_code == 1
assert result.output == 'hello world\n'

result = runner.invoke(cli_float)
assert result.exit_code == 1
assert result.output == 'hello world\n1.0\n'

result = runner.invoke(cli_float_ctx_exit)
assert result.exit_code == 1
assert result.output == 'hello world\n1.0\n'

result = runner.invoke(cli_no_error)
assert result.exit_code == 0
assert result.output == 'hello world\n'
Expand Down

0 comments on commit 8df9a6b

Please sign in to comment.