plac

repository·master·Indexed 43 days ago

https://github.com/ialbert/plac

A Python package that automatically generates command line interfaces from function signatures. It allows developers to create CLI tools with minimal boilerplate using plac.call() and decorators like @plac.pos, @plac.opt, and @plac.flg. It also includes plac.Interpreter for creating interactive applications, supporting subcommands via command containers, and providing tools for automated functional testing with .placet files.

Tokens
9K
Snippets
33
Records
44
Agent score
54%

What's inside plac

  1. Create an interactive application with `plac.Interpreter`

    master

    If your script has a large startup time, you can wrap its main function in a plac.Interpreter to turn it into an interactive application. This allows the startup code to run only once, while you execute multiple commands in a loop.

    Use the .interact() method to start the interactive session. The session displays the main function's docstring as an introduction and uses a configurable prompt. The session ends when the user sends a CTRL-D (or CTRL-Z on Windows).

    Key features:

    • Command Abbreviations: The interpreter supports abbreviations (e.g., del for delete). If an abbreviation is ambiguous, plac raises a NameError.
    • Custom Prompt: You can specify a custom prompt string via the prompt argument.
    import plac
    import my_script
    
    # Wrap the main function
    interpreter = plac.Interpreter(my_script.main)
    
    # Start interactive mode with a custom prompt
    interpreter.interact(prompt='my-app> ')
  2. Handle threading and concurrency with plac.Interpreter

    master

    The plac interpreter loop is implemented via extended generators, which means interpreters are single-threaded. Calling .send() from a different thread than the one that created the interpreter will cause errors.

    Workaround: Using a Queue

    To interact with an interpreter from other threads or processes, use a queue.Queue (or multiprocessing.Queue) to pipe commands to a dedicated thread running the interpreter loop.

    import queue
    
    # input_queue receives commands from other threads
    # EXIT is a sentinel value to stop the loop
    
    with plac.Interpreter(main) as i:
        for input_value in iter(input_queue.get, EXIT):
            result = i.send(input_value)
            output_queue.put(result)
    with interpreter:
        for input_value in iter(input_queue.get, EXIT):
            output_queue.put(interpreter.send(input_value))
  3. Summary of core plac usage patterns

    master

    Depending on your goal, use the following primary patterns:

    1. Command-line scripts: Use plac.call.
    2. Command interpreters: Use plac.Interpreter.
      • For interactive mode: call the .interact() method.
      • For batch mode: call the .execute() method.
    3. Testing: Use Interpreter.check() in the appropriate context or use the Interpreter.doctest feature.
    4. Low-level control: Use Interpreter.send(), which returns a finished Task object.
    5. Background execution: For long-running commands, declare them in the thcommands (threads) or mpcommands (multiprocessing) lists.
    6. Server mode: Use the .start_server(port) method to start an asynchronous server (default port 2199).
  4. Use plac without external dependencies

    master

    The core functionality of plac is contained in a single module called plac_core.py. You can include this file directly in your project to avoid external dependencies. Once copied, import it as plac.

    from mypackage import plac_core as plac
  5. Implement subcommands using command containers

    master

    You can implement subcommands by providing a 'command container' to plac. A command container is any object that has a .commands attribute containing a set of valid functions or methods.

    When using a command container, plac provides several features:

    • Automatic Autocompletion: plac can provide autocompletion for the commands listed in the container.
    • Lifecycle Hooks: If the container implements __enter__ and __exit__, plac will invoke them to manage setup and teardown (e.g., opening/closing a database connection).
    • Dispatch Hooks: You can implement __missing__ to handle invalid command names.
    • Special Commands: plac automatically adds help and .last_tb to the command set.

    Note: If you implement your own custom help command in the container, the default help command will not be added.

  6. Use a Python module as a command container

    master

    You do not need to use classes to define command containers in plac. A Python module can act as a container if it has a .commands attribute. A common pattern is to alias the module itself to main so the runner can find it.

    Example implementation:

    # vcs.py
    def status(quiet=False):
        """A fake status command"""
        return True
    
    def __missing__(cmd):
        print(f"Command '{cmd}' does not exist")
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("ok")
    
    # Alias the module to 'main' for the plac runner
    main = __import__(__name__)

    When running via the runner, subcommands can have their own help via the -h flag:

    $ plac vcs.py status -h
    main = __import__(__name__)
  7. Use flags for boolean options

    master

    A flag is a boolean option that is True if passed on the command line and False if absent. You cannot specify a default value for a flag; its default is always False. For consistency, it is recommended to follow the Flag-Option-Required-Default (FORD) convention when defining your main function:

    1. Flags
    2. Options
    3. Required arguments
    4. Default arguments (including varargs)

    To enable GNU-style flag composition (e.g., -zxvf), provide a one-character abbreviation for your flags.

    # Example of a flag usage
    # If defined as a flag, it is True if present, False otherwise
    def main(verbose: "Enable verbose mode"):
        if verbose:
            print("Verbose mode enabled")
  8. How plac works: Declarative Argument Parsing

    master

    Instead of imperatively building a parser (like argparse), plac uses a declarative interface. It infers the command-line parser directly from the signature and annotations of your function. This allows you to convert a standard Python function into a command-line tool with minimal boilerplate.

    Key benefits:

    • Automatic Help Generation: Generates -h usage messages based on function signatures and annotations.
    • Automatic Error Handling: Manages missing or excessive arguments automatically.
    • Minimal Boilerplate: Reduces the standard if __name__ == '__main__': block to just two lines.
    import plac
    
    def main(arg1, arg2):
        print(f"{arg1} {arg2}")
    
    if __name__ == '__main__':
        plac.call(main)
  9. Understand the differences between plac and argparse

    master

    Plac is an opinionated wrapper around argparse. While it leverages much of argparse's power, it has specific design choices and limitations:

    • Destination Mapping: The destination of an argument always coincides with its name in the function signature. You cannot use the dest parameter to map an option to a different variable name. This means you cannot use Python keywords (like yield) as option names.
    • No Required Options: Plac does not support "required options" (options that must be present). It follows the philosophy that users expect options to be optional.
    • Boolean Flags: Plac only supports regular boolean flags (True/False). It does not support generalized two-value flags (e.g., --flag on/off).
    • No Direct nargs Support: While used internally for flag recognition, nargs is not directly exposed to the user.
    • No Direct Actions: Custom argparse actions are not directly supported, though custom annotation objects can mitigate this.
    • Subparsers: Supported, but requires a specific implementation pattern (see "Implementing subcommands").
  10. Quickstart: Generate CLI parameters from function signatures

    master

    You can automatically generate command line parameters by passing a function to plac.call(). plac inspects the function signature to determine positional arguments, optional arguments (with defaults), and flags (boolean arguments).

    To use it, define your function and call plac.call(your_function) within the if __name__ == '__main__': block.

    import plac
    
    def main(model, iter=100, debug=False):
        """
        A script for machine learning
        """
        print (model, iter, debug)
    
    if __name__ == '__main__':
        # Execute function via plac.call()
        plac.call(main)
  11. Run a plac server

    master

    Plac includes a built-in asynchronous server that converts a command-line interface into a socket-based interface. The server instantiates a separate interpreter for each client to ensure isolation.

    Important Notes:

    • Compatibility: The server currently does not work with Python 3.2+ due to changes in the asynchat module.
    • Concurrency: Because the server is asynchronous, any blocking commands should be run in a separate process or thread to avoid stalling the server.
    • Default Port: 2199.
    • End-of-connection: Send the EOF command to signal the end of a connection.

    To connect to a running server, you can use telnet.

    $ telnet localhost 2199
    i> import_file f1
    i> .list
    <ThreadedTask 1 [import_file f1] RUNNING>
    i> .out
    Imported 100 lines
    i> EOF
  12. Use the plac_runner.py runner script

    master

    The plac_runner.py script is a distribution-provided runner that can execute .plac scripts, .placet files, or Python modules containing a main object (function, command container object, or class).

    Running .plac scripts

    A .plac script's first line must specify the Python module containing the interpreter and the arguments required to instantiate it. If no object is specified, main is imported by default. You can specify a specific object using the :ObjectName syntax.

    Example .plac file content:

    ishelve:ShelveInterface
    setting a=1
    deleting a

    Run it using the --batch flag:

    $ plac_runner.py --batch ishelve.plac

    Running in Interactive Mode

    Use the -i flag to start an interactive session. This is useful for exploring commands or testing scripts manually.

    $ plac_runner.py -i ishelve.py:ShelveInterface

    Running .placet test files

    .placet files are files containing recorded interactive sessions. You can run them as tests using the --test (or -t) flag.

    $ plac_runner.py --test ishelve.placet

    To see the output of the tests, use the -v or --verbose flag.

    $ plac_runner.py --batch ishelve.plac
    $ plac_runner.py -i ishelve.py:ShelveInterface
    $ plac_runner.py --test ishelve.placet