Typing stubs for click - a command line interface creation kit for Python
Built-in testing framework with CLI runner, result objects, and utilities for testing CLI applications in isolation with controlled input/output.
Primary testing utility for invoking CLI commands in isolated environments.
class CliRunner:
charset: str
env: Mapping[str, str]
echo_stdin: bool
mix_stderr: bool
def __init__(
self,
charset: str | None = None,
env: Mapping[str, str] | None = None,
echo_stdin: bool = False,
mix_stderr: bool = True,
) -> None:
"""
CLI test runner for isolated command execution.
Parameters:
- charset: Character encoding for input/output
- env: Environment variables for test execution
- echo_stdin: Echo stdin to stdout for debugging
- mix_stderr: Mix stderr into stdout in results
Usage:
runner = click.testing.CliRunner()
result = runner.invoke(my_command, ['--help'])
"""
def invoke(
self,
cli: BaseCommand,
args: str | Iterable[str] | None = None,
input: bytes | str | IO[Any] | None = None,
env: Mapping[str, str] | None = None,
catch_exceptions: bool = True,
color: bool = False,
**extra: Any,
) -> Result:
"""
Invoke a CLI command and return results.
Parameters:
- cli: Command or group to invoke
- args: Command arguments (list or string)
- input: Input data for stdin
- env: Environment variables (merged with runner env)
- catch_exceptions: Catch and store exceptions instead of raising
- color: Enable colored output
- **extra: Additional keyword arguments
Returns:
Result object with execution details
Usage:
# Basic command invocation
result = runner.invoke(hello_command)
# With arguments
result = runner.invoke(hello_command, ['--name', 'World'])
# With input
result = runner.invoke(interactive_command, input='y\\n')
# With environment
result = runner.invoke(env_command, env={'DEBUG': '1'})
"""
def isolated_filesystem(self) -> ContextManager[str]:
"""
Create isolated temporary filesystem for testing.
Returns:
Context manager yielding temporary directory path
Usage:
with runner.isolated_filesystem():
# Create test files
with open('test.txt', 'w') as f:
f.write('test content')
# Run command that operates on files
result = runner.invoke(process_command, ['test.txt'])
assert result.exit_code == 0
"""
def get_default_prog_name(self, cli: BaseCommand) -> str:
"""Get default program name for command."""
def make_env(self, overrides: Mapping[str, str] | None = None) -> dict[str, str]:
"""Create environment dict with overrides."""Object containing the results of CLI command execution.
class Result:
runner: CliRunner
exit_code: int
exception: Any
exc_info: Any | None
stdout_bytes: bytes
stderr_bytes: bytes
def __init__(
self,
runner: CliRunner,
stdout_bytes: bytes,
stderr_bytes: bytes,
exit_code: int,
exception: Any,
exc_info: Any | None = None,
) -> None:
"""
Result of CLI command execution.
Attributes:
- runner: CliRunner that produced this result
- exit_code: Command exit code
- exception: Exception that occurred (if any)
- exc_info: Exception info tuple
- stdout_bytes: Raw stdout bytes
- stderr_bytes: Raw stderr bytes
"""
@property
def output(self) -> str:
"""
Combined stdout output as string.
Returns:
Decoded stdout (and stderr if mixed)
"""
@property
def stdout(self) -> str:
"""
Stdout output as string.
Returns:
Decoded stdout
"""
@property
def stderr(self) -> str:
"""
Stderr output as string.
Returns:
Decoded stderr
"""Utilities for creating test input streams.
def make_input_stream(input: bytes | str | IO[Any] | None, charset: str) -> BinaryIO:
"""
Create input stream for testing.
Parameters:
- input: Input data
- charset: Character encoding
Returns:
Binary input stream
Usage:
# Usually used internally by CliRunner
stream = make_input_stream('test input\\n', 'utf-8')
"""
class EchoingStdin:
"""
Stdin wrapper that echoes input to output for debugging.
Used internally when CliRunner.echo_stdin is True.
"""
def __init__(self, input: BinaryIO, output: BinaryIO) -> None: ...
def read(self, n: int = ...) -> bytes: ...
def readline(self, n: int = ...) -> bytes: ...
def readlines(self) -> list[bytes]: ...Basic Command Testing:
import click
from click.testing import CliRunner
@click.command()
@click.option('--count', default=1, help='Number of greetings')
@click.argument('name')
def hello(count, name):
"""Simple greeting command."""
for _ in range(count):
click.echo(f'Hello {name}!')
def test_hello_command():
runner = CliRunner()
# Test basic functionality
result = runner.invoke(hello, ['World'])
assert result.exit_code == 0
assert 'Hello World!' in result.output
# Test with options
result = runner.invoke(hello, ['--count', '3', 'Alice'])
assert result.exit_code == 0
assert result.output.count('Hello Alice!') == 3
# Test help
result = runner.invoke(hello, ['--help'])
assert result.exit_code == 0
assert 'Simple greeting command' in result.outputTesting Interactive Commands:
@click.command()
def interactive_setup():
"""Interactive setup command."""
name = click.prompt('Your name')
age = click.prompt('Your age', type=int)
if click.confirm('Save configuration?'):
click.echo(f'Saved config for {name}, age {age}')
else:
click.echo('Configuration not saved')
def test_interactive_setup():
runner = CliRunner()
# Test with confirmations
result = runner.invoke(interactive_setup, input='John\\n25\\ny\\n')
assert result.exit_code == 0
assert 'Saved config for John, age 25' in result.output
# Test with rejection
result = runner.invoke(interactive_setup, input='Jane\\n30\\nn\\n')
assert result.exit_code == 0
assert 'Configuration not saved' in result.outputTesting File Operations:
@click.command()
@click.argument('filename', type=click.Path(exists=True))
def process_file(filename):
"""Process a file."""
with open(filename, 'r') as f:
content = f.read()
lines = len(content.splitlines())
click.echo(f'File has {lines} lines')
def test_process_file():
runner = CliRunner()
with runner.isolated_filesystem():
# Create test file
with open('test.txt', 'w') as f:
f.write('line 1\\nline 2\\nline 3\\n')
# Test command
result = runner.invoke(process_file, ['test.txt'])
assert result.exit_code == 0
assert 'File has 3 lines' in result.output
# Test missing file
result = runner.invoke(process_file, ['missing.txt'])
assert result.exit_code != 0Testing Environment Variables:
@click.command()
def env_command():
"""Command that uses environment variables."""
debug = os.environ.get('DEBUG', 'false').lower() == 'true'
if debug:
click.echo('Debug mode enabled')
else:
click.echo('Debug mode disabled')
def test_env_command():
runner = CliRunner()
# Test without environment
result = runner.invoke(env_command)
assert 'Debug mode disabled' in result.output
# Test with environment
result = runner.invoke(env_command, env={'DEBUG': 'true'})
assert 'Debug mode enabled' in result.outputTesting Exception Handling:
@click.command()
@click.argument('number', type=int)
def divide_by_number(number):
"""Divide 100 by the given number."""
if number == 0:
raise click.BadParameter('Cannot divide by zero')
result = 100 / number
click.echo(f'100 / {number} = {result}')
def test_divide_by_number():
runner = CliRunner()
# Test normal operation
result = runner.invoke(divide_by_number, ['5'])
assert result.exit_code == 0
assert '100 / 5 = 20.0' in result.output
# Test error handling
result = runner.invoke(divide_by_number, ['0'])
assert result.exit_code != 0
assert 'Cannot divide by zero' in result.output
# Test with catch_exceptions=False to see actual exception
with pytest.raises(click.BadParameter):
runner.invoke(divide_by_number, ['0'], catch_exceptions=False)Testing Groups and Subcommands:
@click.group()
def cli():
"""Main CLI group."""
pass
@cli.command()
def status():
"""Show status."""
click.echo('Status: OK')
@cli.command()
@click.argument('name')
def greet(name):
"""Greet someone."""
click.echo(f'Hello {name}!')
def test_cli_group():
runner = CliRunner()
# Test group help
result = runner.invoke(cli, ['--help'])
assert result.exit_code == 0
assert 'Main CLI group' in result.output
# Test subcommand
result = runner.invoke(cli, ['status'])
assert result.exit_code == 0
assert 'Status: OK' in result.output
# Test subcommand with args
result = runner.invoke(cli, ['greet', 'World'])
assert result.exit_code == 0
assert 'Hello World!' in result.outputTesting with Fixtures:
import pytest
@pytest.fixture
def runner():
"""CLI runner fixture."""
return CliRunner()
@pytest.fixture
def temp_config(runner):
"""Temporary config file fixture."""
with runner.isolated_filesystem():
config = {'host': 'localhost', 'port': 8080}
with open('config.json', 'w') as f:
json.dump(config, f)
yield 'config.json'
def test_with_fixtures(runner, temp_config):
result = runner.invoke(load_config_command, [temp_config])
assert result.exit_code == 0Install with Tessl CLI
npx tessl i tessl/pypi-types-click