Skip to content
Advertisement

pytest throws exception while using ClickArguments SystemExit: 0

I am trying to run a test case for a python script. Test case is successful when I don’t use Click decorator and argument in python script method.

But when I am using it, it gives SystemExit: 0.

commands.py:

import sys
import click

@click.command()
@click.argument('bar_info', nargs=-1)
def get_all_foos(bar_info):
    print("bars_to_retain --> {}".format(bar_info[1]))
    print("bars_prefix --> {}".format(bar_info[0]))
    # do something with bar_info
    return "result"

tests/test_commands.py:

import sys
import pytest
import commands
from click.testing import CliRunner

def test_foo_list():
    response = commands.get_all_foos(["ctm", "10"])
    print("response is --> {}".format(response))

When I run the test case:

pytest -rA tests/test_commands.py

then the test fails with:

FAILED tests/test_commands.py::test_foo_list - SystemExit: 0

The full output is:

============================= test session starts ==============================
platform darwin -- Python 3.10.3, pytest-7.1.2, pluggy-1.0.0
rootdir: ...
collected 1 item

tests/test_commands.py F                                                 [100%]

=================================== FAILURES ===================================
________________________________ test_foo_list _________________________________

self = <Command get-all-foos>, args = [], prog_name = 'pytest'
complete_var = None, standalone_mode = True, windows_expand_args = True
extra = {}, ctx = <click.core.Context object at 0x107585d20>, rv = 'result'

    def main(
        self,
        args: t.Optional[t.Sequence[str]] = None,
        prog_name: t.Optional[str] = None,
        complete_var: t.Optional[str] = None,
        standalone_mode: bool = True,
        windows_expand_args: bool = True,
        **extra: t.Any,
    ) -> t.Any:
        """This is the way to invoke a script with all the bells and
        whistles as a command line application.  This will always terminate
        the application after a call.  If this is not wanted, ``SystemExit``
        needs to be caught.

        This method is also available by directly calling the instance of
        a :class:`Command`.

        :param args: the arguments that should be used for parsing.  If not
                     provided, ``sys.argv[1:]`` is used.
        :param prog_name: the program name that should be used.  By default
                          the program name is constructed by taking the file
                          name from ``sys.argv[0]``.
        :param complete_var: the environment variable that controls the
                             bash completion support.  The default is
                             ``"_<prog_name>_COMPLETE"`` with prog_name in
                             uppercase.
        :param standalone_mode: the default behavior is to invoke the script
                                in standalone mode.  Click will then
                                handle exceptions and convert them into
                                error messages and the function will never
                                return but shut down the interpreter.  If
                                this is set to `False` they will be
                                propagated to the caller and the return
                                value of this function is the return value
                                of :meth:`invoke`.
        :param windows_expand_args: Expand glob patterns, user dir, and
            env vars in command line args on Windows.
        :param extra: extra keyword arguments are forwarded to the context
                      constructor.  See :class:`Context` for more information.

        .. versionchanged:: 8.0.1
            Added the ``windows_expand_args`` parameter to allow
            disabling command line arg expansion on Windows.

        .. versionchanged:: 8.0
            When taking arguments from ``sys.argv`` on Windows, glob
            patterns, user dir, and env vars are expanded.

        .. versionchanged:: 3.0
           Added the ``standalone_mode`` parameter.
        """
        if args is None:
            args = sys.argv[1:]

            if os.name == "nt" and windows_expand_args:
                args = _expand_args(args)
        else:
            args = list(args)

        if prog_name is None:
            prog_name = _detect_program_name()

        # Process shell completion requests and exit early.
        self._main_shell_completion(extra, prog_name, complete_var)

        try:
            try:
                with self.make_context(prog_name, args, **extra) as ctx:
                    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()

lib/python3.10/site-packages/click/core.py:1065:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <click.core.Context object at 0x107585d20>, code = 0

    def exit(self, code: int = 0) -> "te.NoReturn":
        """Exits the application with a given exit code."""
>       raise Exit(code)
E       click.exceptions.Exit: 0

lib/python3.10/site-packages/click/core.py:687: Exit

During handling of the above exception, another exception occurred:

    def test_foo_list():
>       response = commands.get_all_foos(["ctm", "10"])

tests/test_commands.py:9:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
lib/python3.10/site-packages/click/core.py:1130: in __call__
    return self.main(*args, **kwargs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <Command get-all-foos>, args = [], prog_name = 'pytest'
complete_var = None, standalone_mode = True, windows_expand_args = True
extra = {}, ctx = <click.core.Context object at 0x107585d20>, rv = 'result'

    def main(
        self,
        args: t.Optional[t.Sequence[str]] = None,
        prog_name: t.Optional[str] = None,
        complete_var: t.Optional[str] = None,
        standalone_mode: bool = True,
        windows_expand_args: bool = True,
        **extra: t.Any,
    ) -> t.Any:
        """This is the way to invoke a script with all the bells and
        whistles as a command line application.  This will always terminate
        the application after a call.  If this is not wanted, ``SystemExit``
        needs to be caught.

        This method is also available by directly calling the instance of
        a :class:`Command`.

        :param args: the arguments that should be used for parsing.  If not
                     provided, ``sys.argv[1:]`` is used.
        :param prog_name: the program name that should be used.  By default
                          the program name is constructed by taking the file
                          name from ``sys.argv[0]``.
        :param complete_var: the environment variable that controls the
                             bash completion support.  The default is
                             ``"_<prog_name>_COMPLETE"`` with prog_name in
                             uppercase.
        :param standalone_mode: the default behavior is to invoke the script
                                in standalone mode.  Click will then
                                handle exceptions and convert them into
                                error messages and the function will never
                                return but shut down the interpreter.  If
                                this is set to `False` they will be
                                propagated to the caller and the return
                                value of this function is the return value
                                of :meth:`invoke`.
        :param windows_expand_args: Expand glob patterns, user dir, and
            env vars in command line args on Windows.
        :param extra: extra keyword arguments are forwarded to the context
                      constructor.  See :class:`Context` for more information.

        .. versionchanged:: 8.0.1
            Added the ``windows_expand_args`` parameter to allow
            disabling command line arg expansion on Windows.

        .. versionchanged:: 8.0
            When taking arguments from ``sys.argv`` on Windows, glob
            patterns, user dir, and env vars are expanded.

        .. versionchanged:: 3.0
           Added the ``standalone_mode`` parameter.
        """
        if args is None:
            args = sys.argv[1:]

            if os.name == "nt" and windows_expand_args:
                args = _expand_args(args)
        else:
            args = list(args)

        if prog_name is None:
            prog_name = _detect_program_name()

        # Process shell completion requests and exit early.
        self._main_shell_completion(extra, prog_name, complete_var)

        try:
            try:
                with self.make_context(prog_name, args, **extra) as ctx:
                    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)
                raise Abort() from None
            except ClickException as e:
                if not standalone_mode:
                    raise
                e.show()
                sys.exit(e.exit_code)
            except OSError as e:
                if e.errno == errno.EPIPE:
                    sys.stdout = t.cast(t.TextIO, PacifyFlushWrapper(sys.stdout))
                    sys.stderr = t.cast(t.TextIO, PacifyFlushWrapper(sys.stderr))
                    sys.exit(1)
                else:
                    raise
        except Exit as e:
            if standalone_mode:
>               sys.exit(e.exit_code)
E               SystemExit: 0

lib/python3.10/site-packages/click/core.py:1083: SystemExit
----------------------------- Captured stdout call -----------------------------
bars_to_retain --> 10
bars_prefix --> ctm
=========================== short test summary info ============================
FAILED tests/test_commands.py::test_foo_list - SystemExit: 0
============================== 1 failed in 0.15s ===============================

Advertisement

Answer

Don’t run click commands directly; they will trigger sys.exit(0) when done, which is the normal and correct way to end a command-line tool, but not very useful when trying to test the command.

Instead, follow the Testing Click Applications chapter and use CliRunner() object to run your commands:

from click.testing import CliRunner
# ...


def test_foo_list():
    runner = CliRunner()
    result = runner.invoke(commands.get_all_foos, ["ctm", "10"])
    response = result.return_value

Note that I passed in a list of string arguments; click will parse those into the correct structure.

I assigned result.return_value to response here, but know that that’ll be None; click commands are not supposed to return anything really, because they normally would communicate with the user via the terminal or by interacting with the filesystem, etc.:

________________________________ test_foo_list _________________________________
----------------------------- Captured stdout call -----------------------------
response is --> None
=========================== short test summary info ============================
PASSED tests/test_commands.py::test_foo_list

You could perhaps use print() or click.echo() to write something to stdout and then test for that output via result.stdout, though.

Advertisement