Clypi

repository·master·Indexed 41 days ago

https://github.com/danimelchor/clypi

A lightweight, asynchronous, and type-safe framework for building production-ready command-line interfaces in Python. Version 1.9.1 features a dataclass-like approach to argument definition using type annotations, support for subcommands via type unions, and a comprehensive suite of UI components including spinners, colored text (cprint), and layout helpers like boxed() and indented(). It also includes mdtest, a utility for verifying Python code blocks within Markdown documentation.

Tokens
22.9K
Snippets
109
Records
117
Agent score
50%

What's inside Clypi

  1. What is mdtest?

    master
    mdtest is a tiny CLI tool designed to ensure that code blocks within Markdown documentation are runnable and free of typos. It works by identifying Python code blocks annotated with specific HTML comments and executing them to verify their correctness.
  2. Add subcommands to group functionality

    master

    Subcommands allow you to group related commands under a single entry point. To implement subcommands, create separate Command classes for each subcommand and define a class-level attribute named subcommand in your main Command class. The type hint for subcommand should be a Union of the subcommand classes.

    from clypi import Command, Positional, arg
    from typing_extensions import override
    
    class Greet(Command):
        """Say hi to someone"""
        name: Positional[str] = arg(help="Your name")
    
        @override
        async def run(self):
            print(f"Hello, {self.name}")
    
    class Wave(Command):
        """Wave at someone"""
        name: Positional[str] = arg(help="Your name")
    
        @override
        async def run(self):
            print(f"👋 {self.name}")
    
    class Cli(Command):
        """A very simple CLI"""
        subcommand: Greet | Wave
    
    if __name__ == '__main__':
        cmd = Cli.parse()
        cmd.start()
  3. Use argument inheritance for shared CLI options

    master

    To avoid redefining the same arguments across multiple subcommands, you can use argument inheritance. Define an argument in a parent Command and use arg(inherited=True) in the child commands. The child commands will automatically inherit the full definition (including help text, short flags, and groups) from the parent.

    Note: The parent command must define the argument with its full configuration (help, short flags, etc.) for the child to receive the complete definition.

    from clypi import Command, Positional, arg
    from typing_extensions import override
    
    class Wave(Command):
        """Wave at someone"""
        name: Positional[str]
        # Inherits the full definition from the parent 'Cli' command
        verbose: bool = arg(inherited=True)
    
        @override
        async def run(self) -> None:
            print(f"👋 Hey {self.name}")
            if self.verbose:
                print(f"👋👋👋 HEYYY {self.name}")
    
    class Cli(Command):
        """A very simple CLI"""
        subcommand: Wave | None
        verbose: bool = arg(
            False, short="v", help="Whether to show verbose output", group="global"
        )
    
    if __name__ == "__main__":
        cmd = Cli.parse()
        cmd.start()
  4. Define Subcommands

    master

    Subcommands are defined by adding a subcommand field to a Command class. The type hint determines if the subcommand is mandatory or optional, and which specific subcommands are allowed.

    • Mandatory single: subcommand: MySubcommand
    • Optional single: subcommand: MySubcommand | None
    • Mandatory choice: subcommand: MySubcommand | MyOtherSubcommand
    • Optional choice: subcommand: MySubcommand | MyOtherSubcommand | None
    from clypi import Command
    
    class MySubcommand(Command):
        pass
    
    class MyOtherSubcommand(Command):
        pass
    
    class MyCommand(Command):
        # An optional subcommand choice
        subcommand: MySubcommand | MyOtherSubcommand | None
  5. Define input parsers with `Parser[T]`

    master

    A Parser[T] is a callable that takes any input (typically a string from the user) and returns a value of type T. You can use built-in types like int or str, user-defined functions, or parsers from external libraries to validate and transform user input.

    # Example of a custom parser
    Parser: TypeAlias = Callable[[Any], T] | type[T]
  6. Customize UI elements using the Theme and Styler classes

    master

    The Theme class allows you to map specific CLI UI components to a Styler object. Each Styler defines the visual properties (like foreground color fg and bold) for that component.

    Available Theme keys include:

    • usage: General usage string styling
    • usage_command: The command part of the usage string
    • usage_args: The arguments part of the usage string
    • section_title: Titles for help sections
    • subcommand: Subcommand names
    • long_option: Long-form options (e.g., --option)
    • short_option: Short-form options (e.g., -o)
    • positional: Positional arguments
    • placeholder: Placeholders within arguments
    • prompts: Interactive prompt styling
    # Example of styling a single element
    usage=Styler(fg="green", bold=True)
  7. Defer argument prompting with arg(defer=True)

    master

    For complex CLIs where an argument's necessity depends on other flags, use arg(defer=True).

    When an argument is deferred, Clypi will not prompt the user for the value or require it to be passed during the initial parsing phase. Instead, the prompt or validation only occurs at the moment the attribute is actually accessed in your code. This allows you to implement conditional logic where certain arguments are only requested if specific conditions are met during execution.

    from clypi import Command, arg
    from typing_extensions import override
    
    class Cli(Command):
        single_threaded: bool = arg(False)
        num_cores: int = arg(
            defer=True,
            prompt="How many CPU cores do you want to use?"
        )
    
        @override
        async def run(self):
            # Accessing single_threaded does not trigger the num_cores prompt
            print("Running single threaded:", self.single_threaded)
            
            if self.single_threaded:
                print("Running single threaded...")
            else:
                # The prompt for 'num_cores' is triggered here because the attribute is accessed
                threads = self.num_cores // 4
                print("Running with threads:", threads)
    
    if __name__ == "__main__":
        cmd = Cli.parse()
        cmd.start()
  8. Create intuitive subcommands (command groups)

    master

    Subcommands are created by composing Command instances. You can use type unions (e.g., Lint | Run) to define which subcommand should be executed. Arguments can be shared across subcommands using the inherited=True flag in the arg helper.

    from clypi import Command, arg
    
    class Lint(Command):
        """Lint a set of files"""
        verbose: bool = arg(inherited=True)  # Inherits the argument def from `MyCli`
    
    class Run(Command):
        """Run a set of files"""
    
    class MyCli(Command):
        """A simple CLI to lint and run files"""
        subcommand: Lint | Run
        verbose: bool = arg(False, help="Whether to show more output")
    
    cli = MyCli.parse()
    cli.start()
  9. Create user input (stdin) tests

    master

    To test code that requires user interaction via standard input, use the <!-- mdtest-stdin <input_string> --> annotation. The specified string will be provided to the input() function during execution.

    <!-- mdtest-stdin hello world -->
    ```python
    import sys
    assert input() == 'hello world', f"Expected the stdin to be 'hello world'"