An extension to Click with additional features like automatic async support, aliasing and a modular decorator system.
Project description
Click Extended
An extension of the Click library with additional features like aliasing, asynchronous support, an extended decorator API and more.
Features
- Decorator API: Extend the functionality your command line by adding custom data sources, data processing pipelines, and more.
- Aliasing: Use aliases for groups and commands to reduce boilerplate and code repetition.
- Tags: Use tags to group several data sources together to apply batch processing.
- Async Support: Native support for declaring functions and methods asynchronous.
- Environment Variables: Built-in support for loading and using environment variables as a data source.
- Full Type Support: Built with type-hinting from the ground up, meaning everything is fully typed.
- Improved Errors: Improved error output like tips, debugging, and more.
- Short Flag Concatenation: Automatically support concatenating short hand flags where
-r -fis the same as-rf. - Global state: Access global state through the context's
dataproperty. - Hook API: Hook into various points and run custom functions in the lifecycle.
- Shell Completion: Opt-in tab-completion for Bash, Zsh, and Fish with a single function call.
Installation
pip install click-extended
Requirements
- Python: 3.10 or higher
Quick Start
Basic Command
A simple command with a positional argument and an option. The command is also accessible via the alias greet.
from click_extended import command, argument, option
@command(aliases="greet")
@argument("name")
@option("count", "-n", type=int, default=1, help="Number of times to greet.")
def hello(name: str, count: int) -> None:
"""Greet someone by name."""
for _ in range(count):
print(f"Hello, {name}!")
if __name__ == "__main__":
hello()
$ python cli.py "Alice"
Hello, Alice!
$ python cli.py "Alice" -n 3
Hello, Alice!
Hello, Alice!
Hello, Alice!
Command Group
A group that organises multiple subcommands. Commands can have aliases to reduce boilerplate.
from click_extended import group, command, argument, option
@group()
def cli() -> None:
"""A simple task manager."""
@cli.command(aliases=["ls"])
@option("status", default="all", help="Filter by status: all, open, done.")
def list_tasks(status: str) -> None:
"""List all tasks."""
print(f"Listing tasks with status: {status}")
@cli.command(aliases=["add"])
@argument("title")
@option("priority", "-p", default="medium", help="Task priority: low, medium, high.")
def create_task(title: str, priority: str) -> None:
"""Create a new task."""
print(f"Created task '{title}' with priority '{priority}'.")
if __name__ == "__main__":
cli()
$ python cli.py list-tasks --status open
Listing tasks with status: open
$ python cli.py add "Buy groceries" -p high
Created task 'Buy groceries' with priority 'high'.
Multi-File CLI
For anything beyond a few commands, splitting each command into its own file keeps the project manageable. Use group.add() to wire everything together at the entrypoint.
cli/entrypoint.py
from click_extended import group
from .commands.users import users
from .commands.deploy import deploy
@group()
def cli() -> None:
"""Production deployment tool."""
cli.add(users)
cli.add(deploy)
if __name__ == "__main__":
cli()
cli/commands/users.py
from click_extended import group, command, argument, option
from click_extended.decorators import is_email, length, not_empty, strip
@group(aliases=["u"])
def users() -> None:
"""Manage users."""
@users.command(aliases=["add"])
@argument("email")
@is_email()
@option("role", "-r", default="viewer", help="Role: viewer, editor, admin.")
def create_user(email: str, role: str) -> None:
"""Create a new user."""
print(f"Created user '{email}' with role '{role}'.")
@users.command(aliases=["rm"])
@argument("email")
@is_email()
def remove_user(email: str) -> None:
"""Remove a user."""
print(f"Removed user '{email}'.")
cli/commands/deploy.py
from typing import Any
from click_extended import command, option
from click_extended.decorators import to_path, load_json
@command(aliases=["d"])
@option("config", "-c", required=True, help="Path to deployment config.")
@to_path(exists=True, extensions=["json"])
@load_json()
@option("env", "-e", default="staging", help="Target environment.", choices=("staging", "production"))
def deploy(config: dict[str, Any], env: str) -> None:
"""Deploy the application."""
host = config.get("host", "localhost")
print(f"Deploying to {host} ({env}).")
$ python -m cli.entrypoint users add "alice@example.com" -r admin
Created user 'alice@example.com' with role 'admin'.
$ python -m cli.entrypoint deploy -c ./config.json -e production
Deploying to api.example.com (production).
See the Splitting Files guide for more patterns.
Shell Completion
Call register_completion on any command or group to add built-in tab-completion support. Supports Bash, Zsh, and Fish, shell is auto-detected.
from click_extended import group, register_completion
@group()
def cli() -> None:
"""My CLI."""
register_completion(cli)
if __name__ == "__main__":
cli()
$ mycli --install-completion
Installed Bash completion for 'mycli'.
$ source ~/.bashrc
$ mycli <TAB>
greet send
See the Shell Completion guide for all available flags and installation details.
Input Validation
Chain built-in decorators to validate and transform values before they reach the function.
from click_extended import command, option
from click_extended.decorators import (
strip,
not_empty,
is_email,
length,
dependencies,
)
@command()
@dependencies("username", "email", "password")
@option("username", "-u", required=True)
@strip()
@not_empty()
@option("email", "-e", required=True)
@is_email()
@option("password", "-p", required=True)
@length(min=8, max=64)
def register(username: str, email: str, password: str) -> None:
"""Register a new user account."""
print(f"Registered '{username}' with email '{email}'.")
$ python cli.py -u " alice " -e "alice@example.com" -p "hunter42"
Registered 'alice' with email 'alice@example.com'.
$ python cli.py -u "alice" -e "not-an-email" -p "hunter42"
ValueError (register): 'not-an-email' is not a valid email address.
$ python cli.py -u "alice" -e "alice@example.com" -p "short"
ValueError (register): Value must be at least 8 characters long.
Environment Variables
Load credentials or configuration from environment variables rather than command-line flags.
from click_extended import command, env
from click_extended.decorators import not_empty
@command()
@env("DATABASE_URL", required=True)
@not_empty()
@env("LOG_LEVEL", default="info")
def serve(database_url: str, log_level: str) -> None:
"""Start the application server."""
print(f"Connecting to {database_url} (log level: {log_level})")
$ python cli.py
ValueError (serve): Required environment variable 'DATABASE_URL' is not set.
$ DATABASE_URL="postgresql://localhost/mydb" python cli.py
Connecting to postgresql://localhost/mydb (log level: info)
Loading a JSON Config File
Use @to_path to validate the path and @load_json to parse the file contents.
from typing import Any
from click_extended import command, option
from click_extended.decorators import to_path, load_json
@command()
@option("config", "-c", required=True, help="Path to a JSON config file.")
@to_path(exists=True, extensions=["json"])
@load_json()
def deploy(config: dict[str, Any]) -> None:
"""Deploy using a JSON config file."""
host = config.get("host", "localhost")
port = config.get("port", 8080)
print(f"Deploying to {host}:{port}")
$ python cli.py -c ./config.json
Deploying to api.example.com:443
Cross-Parameter Constraints
Use @requires, @conflicts, and @exclusive to express relationships between parameters.
from click_extended import command, option
from click_extended.decorators import requires, conflicts, exclusive
@command()
@exclusive("token", "username")
@option("token", "-t", help="API token for authentication.")
@conflicts("username")
@option("username", "-u", help="Username for basic authentication.")
@requires("password")
@option("password", "-p", help="Password for basic authentication.")
def login(token: str | None, username: str | None, password: str | None) -> None:
"""Authenticate with either a token or username and password."""
if token:
print(f"Logged in with token.")
else:
print(f"Logged in as '{username}'.")
$ python cli.py -t "mytoken"
Logged in with token.
$ python cli.py -t "mytoken" -u "alice"
ValueError (login): '--token' and '--username' are mutually exclusive.
$ python cli.py -u "alice"
ValueError (login): '--username' requires '--password' to be provided.
Custom Child Node
If the built-in decorators do not cover your use case, you can implement your own child node.
from typing import Any
from click_extended import command, option
from click_extended.classes import ChildNode
from click_extended.types import Context, Decorator
class Slugify(ChildNode):
def handle_str(
self,
value: str,
context: Context,
*args: Any,
**kwargs: Any,
) -> str:
separator: str = kwargs.get("separator", "-")
return separator.join(value.lower().split())
def slugify(separator: str = "-") -> Decorator:
"""Convert a string to a URL-friendly slug."""
return Slugify.as_decorator(separator=separator)
@command()
@option("title", required=True, help="Blog post title.")
@slugify()
def publish(title: str) -> None:
"""Publish a blog post."""
print(f"Published at /posts/{title}")
$ python cli.py --title "Hello World"
Published at /posts/hello-world
$ python cli.py --title "My New Post"
Published at /posts/my-new-post
Documentation
The full documentation is available here and goes through the full library, from explaining design choices, how to use the library, and much more.
Contributing
Contributors are more than welcome to work on this project. Read the contribution documentation to learn more.
License
This project is licensed under the MIT License, see the license file for details.
Acknowledgements
This project is built on top of the Click library.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file click_extended-1.4.0.tar.gz.
File metadata
- Download URL: click_extended-1.4.0.tar.gz
- Upload date:
- Size: 114.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1cb64e2c0c9cff41430a6ce2c16f655fc837bab5e8d84d12d46d629bedc33f8a
|
|
| MD5 |
7e0959ef6114aa7531d2bde827ef12eb
|
|
| BLAKE2b-256 |
34617eaadbad6b233eec908dabbccdd198c10775cb4248fa7ef1f89a26fe17e7
|
File details
Details for the file click_extended-1.4.0-py3-none-any.whl.
File metadata
- Download URL: click_extended-1.4.0-py3-none-any.whl
- Upload date:
- Size: 189.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0aacbf5c9a3744828145e2489bc72f4615bd7fcdaf46803c2398a93520b04e53
|
|
| MD5 |
0c6737ed31c4248f762ebedf0778f9e9
|
|
| BLAKE2b-256 |
6ef00e7d7ed660b50f04a64b0959bf5f795f90c666b6f531d89544fac0864322
|