English | Русский
A powerful Python framework for building feature-rich command-line interfaces with declarative syntax, advanced output formatting, localization, and asynchronous support. Create professional CLIs with minimal code.
- Declarative API: Easily define commands using decorators
- Type Safety: Automatic type validation and conversion
- Asynchronous Support: Built-in
async/awaitsupport for I/O operations - Enhanced Output: Colored text, tables, progress bars with stream support
- Internationalization: Built-in localization support with proper caching
- Configuration Management: Store and load settings with file locking and hierarchical access
- Lifecycle Hooks: Extend behavior at specific execution points
- Auto-generation: Create CLI from existing classes and functions
- Modular Architecture: Replaceable components for maximum flexibility
- Python 3.8+ (uses
typing.get_origin/get_argsfrom standard library) - Optional:
jsonschema(for configuration validation)
The framework is not yet available on PyPI. To install:
# Clone the repository
git clone https://github.com/PsinaDev/Python-CLI-Framework.git
# Install optional dependencies
pip install jsonschema # optional, for config validationfrom cli import CLI, echo
# Create CLI instance
cli = CLI(name='myapp')
@cli.command()
@cli.argument('name', help='Name to greet')
@cli.option('--greeting', '-g', default='Hello', help='Greeting text')
def greet(name, greeting):
"""Greet a user"""
echo(f"{greeting}, {name}!", 'success')
if __name__ == '__main__':
cli.run()Run the command:
python example.py greet World --greeting="Hi"
# Output: Hi, World!Get help:
python example.py help greetInteractive mode:
python example.py
# Opens interactive console with tab completionFull documentation is available in DOCS.md.
- Quick Start — Get started in 5 minutes
- Command Decorators — Define commands, arguments, options
- Formatted Output — Colors, tables, progress bars
- Configuration — Persistent settings with hierarchical access
- Localization — Multi-language support
- Async Commands — Async/await support
- Middleware & Hooks — Extend functionality
- CLI Context — Share data between middleware and commands
- API Reference — Complete API documentation
- Examples — Real-world examples
- Troubleshooting — Common issues and solutions
from cli import CLI, echo
cli = CLI(name='greeter')
@cli.command()
@cli.argument('name', help='User name')
def hello(name):
"""Greet a user"""
echo(f'Hello, {name}!', 'success')
if __name__ == '__main__':
cli.run()from cli import CLI, echo
cli = CLI(name='filetools')
@cli.command()
@cli.argument('path', help='Directory path')
@cli.option('--recursive', '-r', is_flag=True, help='Recursive listing')
@cli.option('--hidden', is_flag=True, help='Show hidden files')
def list_files(path, recursive, hidden):
"""List files in directory"""
import os
for item in os.listdir(path):
if not hidden and item.startswith('.'):
continue
echo(item)
if recursive:
echo('Recursive mode enabled', 'info')
if __name__ == '__main__':
cli.run()from cli import CLI, echo
cli = CLI(name='filetools')
@cli.group(name='file', help='File operations')
class FileCommands:
@cli.command()
@cli.argument('path', help='Directory path')
def list(self, path):
"""List files"""
import os
for item in os.listdir(path):
echo(item)
@cli.command()
@cli.argument('source', help='Source file')
@cli.argument('dest', help='Destination file')
@cli.option('--force', '-f', is_flag=True, help='Overwrite if exists')
def copy(self, source, dest, force):
"""Copy file"""
import shutil
if force or not os.path.exists(dest):
shutil.copy2(source, dest)
echo(f'Copied: {source} -> {dest}', 'success')
else:
echo(f'File exists: {dest}', 'warning')
if __name__ == '__main__':
cli.run()from cli import CLI, echo
import asyncio
cli = CLI(name='fetcher')
@cli.command()
@cli.argument('url', help='URL to download')
async def download(url):
"""Async download"""
echo(f'Downloading {url}...', 'info')
await asyncio.sleep(1) # Simulate download
echo('Done!', 'success')
if __name__ == '__main__':
cli.run()from cli import CLI, echo, table, progress_bar
import time
cli = CLI(name='demo')
@cli.command()
def demo():
"""Demonstrate output capabilities"""
# Colored text
echo('Success!', 'success')
echo('Warning!', 'warning')
echo('Error!', 'error')
# Table
headers = ['Name', 'Status', 'Progress']
rows = [
['Task 1', 'Complete', '100%'],
['Task 2', 'In Progress', '45%'],
['Task 3', 'Pending', '0%']
]
table(headers, rows)
# Progress bar
total = 100
update = progress_bar(total, prefix='Loading:')
for i in range(total + 1):
update(i)
time.sleep(0.02)
if __name__ == '__main__':
cli.run()from cli import CLI, echo
import time
cli = CLI(name='myapp')
# Timing middleware
async def timing_middleware(next_handler):
start = time.time()
result = await next_handler()
elapsed = time.time() - start
echo(f'Executed in {elapsed:.2f}s', 'debug')
return result
# Authentication middleware with context
async def auth_middleware(next_handler):
cli.set_context(user_id=123, username='admin')
result = await next_handler()
return result
cli.use(auth_middleware)
cli.use(timing_middleware)
@cli.command()
def status():
"""Check status"""
ctx = cli.get_context()
username = ctx.get('username', 'guest')
echo(f'Status OK (user: {username})', 'success')
if __name__ == '__main__':
cli.run()from cli import CLI, echo
from cli.interfaces import Hook
cli = CLI(name='myapp')
class AuditHook(Hook):
async def on_before_parse(self, args):
echo(f'Parsing: {args}', 'debug')
return args
async def on_after_parse(self, parsed):
return parsed
async def on_before_execute(self, command, kwargs):
echo(f'Executing: {command}', 'info')
async def on_after_execute(self, command, result, exit_code):
echo(f'Completed with code: {exit_code}', 'info')
async def on_error(self, command, error):
echo(f'Error: {error}', 'error')
cli.add_hook(AuditHook())Define commands with simple decorators:
@cli.command(name='hello', aliases=['hi'])
@cli.argument('name', help='User name', type=str)
@cli.option('--loud', '-l', is_flag=True, help='Shout!')
@cli.example('hello World --loud')
def hello_command(name, loud):
"""Say hello"""
msg = f'HELLO, {name.upper()}!' if loud else f'Hello, {name}'
echo(msg, 'success' if loud else 'info')Automatic type validation and conversion:
@cli.option('--count', type=int, default=1)
@cli.option('--items', type=list, help='JSON array or CSV')
@cli.option('--config', type=dict, help='JSON object or key=val')
@cli.option('--verbose', is_flag=True)
def process(count, items, config, verbose):
# Types are automatically validated and converted
for i in range(count):
echo(f'Processing item {i}')Supported types: str, int, float, bool, list, dict, tuple, Optional[T]
Thread-safe configuration with file locking:
# Hierarchical keys
cli.config.set('database.host', 'localhost')
cli.config.set('database.port', 5432)
# Nested updates
cli.config.update({
'app': {
'theme': 'dark',
'language': 'en'
}
})
cli.config.save()All output functions support custom streams:
import sys
# Output to stderr
echo('Error occurred', 'error', file=sys.stderr)
table(headers, rows, file=sys.stderr)
# Progress to custom stream
with open('log.txt', 'w') as f:
update = progress_bar(100, file=f, force_inline=False)
for i in range(101):
update(i)Rich terminal output:
# Predefined styles
echo('Success!', 'success') # Green
echo('Error!', 'error') # Red
echo('Warning!', 'warning') # Yellow
echo('Info', 'info') # Blue
# Tables with auto-sizing
table(['Name', 'Value'], [['foo', '1'], ['bar', '2']])
# Progress bars with colors
update = progress_bar(
100,
prefix='Loading:',
char='█',
color_low='yellow',
color_mid='blue',
color_high='green'
)- Stream Support: All output functions now accept
fileparameter for custom streams - Optional Arguments:
@argumentdecorator now supportsoptionalparameter - Reserved Names Protection: Clear errors for reserved names (
help,h, etc.) - Improved Type Hints: Better handling of
List[T],Dict[K,V]type hints - Better Cache: Message cache now properly handles
defaultparameter - Lifecycle Hooks: New Hook system for extending behavior at specific points
- Explicit Interfaces:
JsonConfigProvidernow explicitly inheritsConfigProvider - Improved Docs: Complete API reference with all parameters documented
Most code will work without changes. Key updates:
-
Optional Arguments: Now supported via
optional=Trueparameter@cli.argument('output', optional=True) def cmd(output=None): pass
-
Reserved Names: Will raise errors if used (previously silent issues)
# Avoid: 'help', 'h', '_cli_help', '_cli_show_help' -
Stream Support: Add
fileparameter for custom outputtable(headers, rows, file=sys.stderr)
This project is licensed under the MIT License - see the LICENSE file for details.
Psinadev - CLI Framework v1.1.0