Source code for click_extra.tests.test_commands

# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
"""Test defaults of our custom commands, as well as their customizations and attached
options, and how they interact with each others."""

from __future__ import annotations

import ast
import inspect
import re
from pathlib import Path
from textwrap import dedent

import click
import cloup
import pytest
from pytest_cases import fixture, parametrize

from click_extra import echo, option, option_group, pass_context
from click_extra.decorators import extra_command, extra_group

from .conftest import (
    command_decorators,
    default_debug_uncolored_log_end,
    default_debug_uncolored_log_start,
    default_options_colored_help,
    default_options_uncolored_help,
    skip_windows_colors,
)


[docs]def test_module_root_declarations(): def fetch_root_members(module): """Fetch all members exposed at the module root.""" members = set() for name, member in inspect.getmembers(module): # Exclude private members. if name.startswith("_"): continue # Exclude automatic imports of submodules as we inspect __init__'s content # only. if inspect.ismodule(member): continue members.add(name) return members click_members = fetch_root_members(click) cloup_members = {m for m in cloup.__all__ if not m.startswith("_")} tree = ast.parse(Path(__file__).parent.joinpath("../__init__.py").read_bytes()) click_extra_members = [] for node in tree.body: if isinstance(node, ast.Assign): for target in node.targets: if target.id == "__all__": for element in node.value.elts: click_extra_members.append(element.s) assert click_members <= set(click_extra_members) assert cloup_members <= set(click_extra_members) expected_members = sorted( click_members.union(cloup_members).union(click_extra_members), key=lambda m: (m.lower(), m), ) assert expected_members == click_extra_members
[docs]@fixture def all_command_cli(): """A CLI that is mixing all variations and flavors of subcommands.""" @extra_group(version="2021.10.08") def command_cli1(): echo("It works!") @command_cli1.command() def default_subcommand(): echo("Run default subcommand...") @extra_command def click_extra_subcommand(): echo("Run click-extra subcommand...") @cloup.command() def cloup_subcommand(): echo("Run cloup subcommand...") @click.command def click_subcommand(): echo("Run click subcommand...") command_cli1.section( "Subcommand group", click_extra_subcommand, cloup_subcommand, click_subcommand, ) return command_cli1
help_screen = ( r"Usage: command-cli1 \[OPTIONS\] COMMAND \[ARGS\]...\n" r"\n" r"Options:\n" rf"{default_options_uncolored_help}" r"\n" r"Subcommand group:\n" r" click-extra-subcommand\n" r" cloup-subcommand\n" r" click-subcommand\n" r"\n" r"Other commands:\n" r" default-subcommand\n" )
[docs]def test_unknown_option(invoke, all_command_cli): result = invoke(all_command_cli, "--blah") assert result.exit_code == 2 assert not result.stdout assert "Error: No such option: --blah" in result.stderr
[docs]def test_unknown_command(invoke, all_command_cli): result = invoke(all_command_cli, "blah") assert result.exit_code == 2 assert not result.stdout assert "Error: No such command 'blah'." in result.stderr
[docs]def test_required_command(invoke, all_command_cli): result = invoke(all_command_cli, "--verbosity", "DEBUG", color=False) assert result.exit_code == 2 # In debug mode, the version is always printed. assert not result.stdout assert re.fullmatch( ( rf"{default_debug_uncolored_log_start}" rf"{default_debug_uncolored_log_end}" r"Usage: command-cli1 \[OPTIONS\] COMMAND \[ARGS\]...\n" r"\n" r"Error: Missing command.\n" ), result.stderr, )
[docs]@pytest.mark.parametrize("param", (None, "-h", "--help")) def test_group_help(invoke, all_command_cli, param): result = invoke(all_command_cli, param, color=False) assert result.exit_code == 0 assert re.fullmatch(help_screen, result.stdout) assert "It works!" not in result.stdout assert not result.stderr
[docs]@pytest.mark.parametrize( "params", ("--version", "blah", ("--verbosity", "DEBUG"), ("--config", "random.toml")), ) def test_help_eagerness(invoke, all_command_cli, params): """See: https://click.palletsprojects.com/en/8.0.x/advanced/#callback-evaluation- order. """ result = invoke(all_command_cli, "--help", params, color=False) assert result.exit_code == 0 assert re.fullmatch(help_screen, result.stdout) assert "It works!" not in result.stdout assert not result.stderr
[docs]@skip_windows_colors @pytest.mark.parametrize("cmd_id", ("default", "click-extra", "cloup", "click")) @pytest.mark.parametrize("param", ("-h", "--help")) def test_subcommand_help(invoke, all_command_cli, cmd_id, param): result = invoke(all_command_cli, f"{cmd_id}-subcommand", param) assert result.exit_code == 0 assert not result.stderr colored_help_header = ( r"It works!\n" r"\x1b\[94m\x1b\[1m\x1b\[4mUsage:\x1b\[0m " rf"\x1b\[97mcommand-cli1 {cmd_id}-subcommand\x1b\[0m" r" \x1b\[36m\x1b\[2m\[OPTIONS\]\x1b\[0m\n" r"\n" r"\x1b\[94m\x1b\[1m\x1b\[4mOptions:\x1b\[0m\n" ) # Extra sucommands are colored and include all extra options. if cmd_id == "click-extra": assert re.fullmatch( rf"{colored_help_header}{default_options_colored_help}", result.stdout, ) # Default subcommand inherits from extra family and is colored, but does not include # extra options. elif cmd_id == "default": assert re.fullmatch( ( rf"{colored_help_header}" r" \x1b\[36m-h\x1b\[0m, \x1b\[36m--help\x1b\[0m" r" Show this message and exit.\n" ), result.stdout, ) # Non-extra subcommands are not colored. else: assert result.stdout == dedent( f"""\ It works! Usage: command-cli1 {cmd_id}-subcommand [OPTIONS] Options: -h, --help Show this message and exit. """, )
[docs]@pytest.mark.parametrize("cmd_id", ("default", "click-extra", "cloup", "click")) def test_subcommand_execution(invoke, all_command_cli, cmd_id): result = invoke(all_command_cli, f"{cmd_id}-subcommand", color=False) assert result.exit_code == 0 assert result.stdout == dedent( f"""\ It works! Run {cmd_id} subcommand... """, ) assert not result.stderr
[docs]def test_integrated_version_value(invoke, all_command_cli): result = invoke(all_command_cli, "--version", color=False) assert result.exit_code == 0 assert not result.stderr assert result.stdout == "command-cli1, version 2021.10.08\n"
[docs]@skip_windows_colors @parametrize( "cmd_decorator", command_decorators( no_click=True, no_cloup=True, no_redefined=True, with_parenthesis=False, ), ) @pytest.mark.parametrize("param", ("-h", "--help")) def test_colored_bare_help(invoke, cmd_decorator, param): """Extra decorators are always colored. Even when stripped of their default parameters, as reported in: https://github.com/kdeldycke/click-extra/issues/534 https://github.com/kdeldycke/click-extra/pull/543 """ @cmd_decorator(params=None) def bare_cli(): pass result = invoke(bare_cli, param) assert result.exit_code == 0 assert not result.stderr assert ( "\n" "\x1b[94m\x1b[1m\x1b[4mOptions:\x1b[0m\n" " \x1b[36m-h\x1b[0m, \x1b[36m--help\x1b[0m Show this message and exit.\n" ) in result.stdout
[docs]def test_no_option_leaks_between_subcommands(invoke): """As reported in https://github.com/kdeldycke/click-extra/issues/489.""" @click.group def cli(): echo("Run cli...") @extra_command @click.option("--one") def foo(): echo("Run foo...") @extra_command(short_help="Bar subcommand.") @click.option("--two") def bar(): echo("Run bar...") cli.add_command(foo) cli.add_command(bar) result = invoke(cli, "--help", color=False) assert result.exit_code == 0 assert result.stdout == dedent( """\ Usage: cli [OPTIONS] COMMAND [ARGS]... Options: --help Show this message and exit. Commands: bar Bar subcommand. foo """, ) assert not result.stderr result = invoke(cli, "foo", "--help", color=False) assert result.exit_code == 0 assert re.fullmatch( ( r"Run cli\.\.\.\n" r"Usage: cli foo \[OPTIONS\]\n" r"\n" r"Options:\n" r" --one TEXT\n" rf"{default_options_uncolored_help}" ), result.stdout, ) assert not result.stderr result = invoke(cli, "bar", "--help", color=False) assert result.exit_code == 0 assert re.fullmatch( ( r"Run cli\.\.\.\n" r"Usage: cli bar \[OPTIONS\]\n" r"\n" r"Options:\n" r" --two TEXT\n" rf"{default_options_uncolored_help}" ), result.stdout, ) assert not result.stderr
[docs]def test_option_group_integration(invoke): # Mix regular and grouped options @extra_group @option_group( "Group 1", click.option("-a", "--opt1"), option("-b", "--opt2"), ) @click.option("-c", "--opt3") @option("-d", "--opt4") def command_cli2(opt1, opt2, opt3, opt4): echo("It works!") @command_cli2.command() def default_command(): echo("Run command...") # Remove colors to simplify output comparison. result = invoke(command_cli2, "--help", color=False) assert result.exit_code == 0 assert re.fullmatch( ( r"Usage: command-cli2 \[OPTIONS\] COMMAND \[ARGS\]...\n" r"\n" r"Group 1:\n" r" -a, --opt1 TEXT\n" r" -b, --opt2 TEXT\n" r"\n" r"Other options:\n" r" -c, --opt3 TEXT\n" r" -d, --opt4 TEXT\n" rf"{default_options_uncolored_help}" r"\n" r"Commands:\n" r" default-command\n" ), result.stdout, ) assert "It works!" not in result.stdout assert not result.stderr
[docs]@pytest.mark.parametrize( ("cmd_decorator", "ctx_settings", "expected_help"), ( # Click does not show all envvar in the help screen by default, unless # specifficaly set on an option. ( click.command, {}, " --flag1\n --flag2 [env var: custom2]\n --flag3\n", ), # Click Extra defaults to let each option choose its own show_envvar value. ( extra_command, {}, " --flag1\n" " --flag2 [env var: custom2, CLI_FLAG2]\n" " --flag3\n", ), # Click Extra allow bypassing its global show_envvar setting. ( extra_command, {"show_envvar": None}, " --flag1\n" " --flag2 [env var: custom2, CLI_FLAG2]\n" " --flag3\n", ), # Click Extra force the show_envvar value on all options. ( extra_command, {"show_envvar": True}, " --flag1 [env var: custom1, CLI_FLAG1]\n" " --flag2 [env var: custom2, CLI_FLAG2]\n" " --flag3 [env var: custom3, CLI_FLAG3]\n", ), ( extra_command, {"show_envvar": False}, " --flag1\n --flag2\n --flag3\n", ), ), ) def test_show_envvar_parameter(invoke, cmd_decorator, ctx_settings, expected_help): @cmd_decorator(context_settings=ctx_settings) @option("--flag1", is_flag=True, envvar=["custom1"]) @option("--flag2", is_flag=True, envvar=["custom2"], show_envvar=True) @option("--flag3", is_flag=True, envvar=["custom3"], show_envvar=False) def cli(): pass # Remove colors to simplify output comparison. result = invoke(cli, "--help", color=False) assert result.exit_code == 0 assert not result.stderr assert expected_help in result.stdout
[docs]def test_raw_args(invoke): """Raw args are expected to be scoped in subcommands.""" @extra_group @option("--dummy-flag/--no-flag") @pass_context def my_cli(ctx, dummy_flag): echo("-- Group output --") echo(f"dummy_flag is {dummy_flag!r}") echo(f"Raw parameters: {ctx.meta.get('click_extra.raw_args', [])}") @my_cli.command() @pass_context @option("--int-param", type=int, default=10) def subcommand(ctx, int_param): echo("-- Subcommand output --") echo(f"int_parameter is {int_param!r}") echo(f"Raw parameters: {ctx.meta.get('click_extra.raw_args', [])}") result = invoke(my_cli, "--dummy-flag", "subcommand", "--int-param", "33") assert result.exit_code == 0 assert not result.stderr assert result.stdout == dedent( """\ -- Group output -- dummy_flag is True Raw parameters: ['--dummy-flag', 'subcommand', '--int-param', '33'] -- Subcommand output -- int_parameter is 33 Raw parameters: ['--int-param', '33'] """, )