pw_cli

This directory contains the pw command line interface (CLI) that facilitates working with Pigweed. The CLI module adds several subcommands prefixed with pw, and provides a mechanism for other Pigweed modules to behave as “plugins” and register themselves as pw commands as well. After activating the Pigweed environment, these commands will be available for use.

pw includes the following commands by default:

doctor        Check that the environment is set up correctly for Pigweed.
format        Check and fix formatting for source files.
help          Display detailed information about pw commands.
logdemo       Show how logs look at various levels.
module-check  Check that a module matches Pigweed's module guidelines.
test          Run Pigweed unit tests built using GN.
watch         Watch files for changes and rebuild.

To see an up-to-date list of pw subcommands, run pw --help.

Invoking pw

pw subcommands are invoked by providing the command name. Arguments prior to the command are interpreted by pw itself; all arguments after the command name are interpreted by the command.

Here are some example invocations of pw:

# Run the doctor command
$ pw doctor

# Run format --fix with debug-level logs
$ pw --loglevel debug format --fix

# Display help for the pw command
$ pw -h watch

# Display help for the watch command
$ pw watch -h

Registering pw plugins

Projects can register their own Python scripts as pw commands. pw plugins are registered by providing the command name, module, and function in a PW_PLUGINS file. PW_PLUGINS files can add new commands or override built-in commands. Since they are accessed by module name, plugins must be defined in Python packages that are installed in the Pigweed virtual environment.

Plugin registrations in a PW_PLUGINS file apply to the their directory and all subdirectories, similarly to configuration files like .clang-format. Registered plugins appear as commands in the pw tool when pw is run from those directories.

Projects that wish to register commands might place a PW_PLUGINS file in the root of their repo. Multiple PW_PLUGINS files may be applied, but the pw tool gives precedence to a PW_PLUGINS file in the current working directory or the nearest parent directory.

PW_PLUGINS file format

PW_PLUGINS contains one plugin entry per line in the following format:

# Lines that start with a # are ignored.
<command name> <Python module> <function>

The following example registers three commands:

# Register the presubmit script as pw presubmit
presubmit my_cool_project.tools run_presubmit

# Override the pw test command with a custom version
test my_cool_project.testing run_test

# Add a custom command
flash my_cool_project.flash main

Defining a plugin function

Any function without required arguments may be used as a plugin function. The function should return an int, which the pw uses as the exit code. The pw tool uses the function docstring as the help string for the command.

Typically, pw commands parse their arguments with the argparse module. pw sets sys.argv so it contains only the arguments for the plugin, so plugins can behave the same whether they are executed independently or through pw.

Example

This example shows a function that is registered as a pw plugin.

# my_package/my_module.py

def _do_something(device):
    ...

def main() -> int:
    """Do something to a connected device."""

    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--device', help='Set which device to target')
    return _do_something(**vars(parser.parse_args()))


if __name__ == '__main__':
    logging.basicConfig(format='%(message)s', level=logging.INFO)
    sys.exit(main())

This plugin is registered in a PW_PLUGINS file in the current working directory or a parent of it.

# Register my_commmand
my_command my_package.my_module main

The function is now available through the pw command, and will be listed in pw’s help. Arguments after the command name are passed to the plugin.

$ pw

 ▒█████▄   █▓  ▄███▒  ▒█    ▒█ ░▓████▒ ░▓████▒ ▒▓████▄
  ▒█░  █░ ░█▒ ██▒ ▀█▒ ▒█░ █ ▒█  ▒█   ▀  ▒█   ▀  ▒█  ▀█▌
  ▒█▄▄▄█░ ░█▒ █▓░ ▄▄░ ▒█░ █ ▒█  ▒███    ▒███    ░█   █▌
  ▒█▀     ░█░ ▓█   █▓ ░█░ █ ▒█  ▒█   ▄  ▒█   ▄  ░█  ▄█▌
  ▒█      ░█░ ░▓███▀   ▒█▓▀▓█░ ░▓████▒ ░▓████▒ ▒▓████▀

usage: pw [-h] [-C DIRECTORY] [-l LOGLEVEL] [--no-banner] [command] ...

The Pigweed command line interface (CLI).

...

supported commands:
  doctor        Check that the environment is set up correctly for Pigweed.
  format        Check and fix formatting for source files.
  help          Display detailed information about pw commands.
  ...
  my_command    Do something to a connected device.

$ pw my_command -h

 ▒█████▄   █▓  ▄███▒  ▒█    ▒█ ░▓████▒ ░▓████▒ ▒▓████▄
  ▒█░  █░ ░█▒ ██▒ ▀█▒ ▒█░ █ ▒█  ▒█   ▀  ▒█   ▀  ▒█  ▀█▌
  ▒█▄▄▄█░ ░█▒ █▓░ ▄▄░ ▒█░ █ ▒█  ▒███    ▒███    ░█   █▌
  ▒█▀     ░█░ ▓█   █▓ ░█░ █ ▒█  ▒█   ▄  ▒█   ▄  ░█  ▄█▌
  ▒█      ░█░ ░▓███▀   ▒█▓▀▓█░ ░▓████▒ ░▓████▒ ▒▓████▀

usage: pw my_command [-h] [--device DEVICE]

Do something to a connected device.

optional arguments:
  -h, --help       show this help message and exit
  --device DEVICE  Set which device to target

Branding Pigweed’s tooling

An important part of starting a new project is picking a name, and in the case of Pigweed, designing a banner for the project. Pigweed supports configuring the banners by setting environment variables:

  • PW_BRANDING_BANNER - Absolute path to a filename containing a banner to display when running the pw commands. See the example below.

  • PW_BRANDING_BANNER_COLOR - Color of the banner. Possible values include: red, bold_red, yellow, bold_yellow, green, bold_green, blue, cyan, magenta, bold_white, black_on_white. See pw_cli.colors for details.

The below example shows how to manually change the branding at the command line. However, these environment variables should be set in the project root’s bootstrap.sh before delegating to Pigweed’s upstream bootstrap.sh.

$ cat foo-banner.txt

 ▒██████  ░▓██▓░  ░▓██▓░
  ▒█░    ▒█   ▒█ ▒█   ▒█
  ▒█▄▄▄▄ ▒█ █ ▒█ ▒█ █ ▒█
  ▒█▀    ▒█   ▒█ ▒█   ▒█
  ▒█      ░▓██▓░  ░▓██▓░

$ export PW_BRANDING_BANNER="$(pwd)/foo-banner.txt"
$ export PW_BRANDING_BANNER_COLOR="bold_red"
$ pw logdemo

 ▒██████  ░▓██▓░  ░▓██▓░
  ▒█░    ▒█   ▒█ ▒█   ▒█
  ▒█▄▄▄▄ ▒█ █ ▒█ ▒█ █ ▒█
  ▒█▀    ▒█   ▒█ ▒█   ▒█
  ▒█      ░▓██▓░  ░▓██▓░

20200610 12:03:44 CRT This is a critical message
20200610 12:03:44 ERR There was an error on our last operation
20200610 12:03:44 WRN Looks like something is amiss; consider investigating
20200610 12:03:44 INF The operation went as expected
20200610 12:03:44 OUT Standard output of subprocess

The branding is not purely visual; it serves to make it clear which project an engineer is working with.

Making the ASCII / ANSI art

The most direct way to make the ASCII art is to create it with a text editor. However, there are some tools to make the process faster and easier.

  • Patorjk’s ASCII art generator - A great starting place, since you can copy and paste straight from the browser into a file, and then point PW_BRANDING_BANNER at it. Most of the fonts use normal ASCII characters; and fonts with extended ASCII characters use the Unicode versions of them (needed for modern terminals).

  • Online ANSII Edit by Andy Herbert - Browser based editor that can export to mixed UTF-8 and ANSII color. It’s also open source. What’s nice about this editor is that you can create a multi-color banner, and save it with the File –> Export as ANSi (UTF-8) option, and use it directly as a Pigweed banner. One caveat is that the editor uses UTF-8 box drawing characters, which don’t work well with all terminals. However, the box drawing characters look so slick on terminals that support them that we feel this is a worthwhile tradeoff.

There are other options, but these require additional work to put into Pigweed since they only export in the traditional ANS or ICE formats. The old ANS formats do not have a converter (contributions welcome!). Here are some of the options as of mid-2020:

  • Playscii - Actively maintained.

  • Moebius - Actively maintained.

  • SyncDraw - Actively maintained, in 2020, in a CVS repository.

  • PabloDraw - Works on most desktop machines thanks to being written in .NET. Not maintained, but works well. Has an impresive brush system for organic style drawing.

  • TheDraw - One of the most popular ANSI art editors back in the 90s. Requires DOSBox to run on modern machines, but otherwise works. It has some of the most impressive capabilities, including supporting full-color multi-character fonts.

Future branding improvements

Branding the pw tool is a great start, but more changes are planned:

  • Supporting branding the bootstrap/activate banner, which for technical reasons is not the same code as the banner printing from the Python tooling. These will use the same PW_BRANDING_BANNER and PW_BRANDING_BANNER_COLOR environment variables.

  • Supporting renaming the pw command to something project specific, like foo in this case.

  • Re-coloring the log headers from the pw tool.

pw_cli Python package

The pw_cli Pigweed module includes the pw_cli Python package, which provides utilities for creating command line tools with Pigweed.

pw_cli.log

Tools for configuring Python logging.

pw_cli.log.all_loggers() Iterator[logging.Logger]

Iterates over all loggers known to Python logging.

pw_cli.log.c_to_py_log_level(c_level: int) int

Converts pw_log C log-level macros to Python logging levels.

pw_cli.log.install(level: Union[str, int] = 20, use_color: Optional[bool] = None, hide_timestamp: bool = False, log_file: Optional[Union[str, pathlib.Path]] = None, logger: Optional[logging.Logger] = None) None

Configures the system logger for the default pw command log format.

If you have Python loggers separate from the root logger you can use pw_cli.log.install to get the Pigweed log formatting there too. For example:

import logging

import pw_cli.log

pw_cli.log.install(
    level=logging.INFO,
    use_color=True,
    hide_timestamp=False,
    log_file=(Path.home() / 'logs.txt'),
    logger=logging.getLogger(__package__),
)
Parameters
  • level – The logging level to apply. Default: logging.INFO.

  • use_color – When True include ANSI escape sequences to colorize log messages.

  • hide_timestamp – When True omit timestamps from the log formatting.

  • log_file – File to save logs into.

  • logger – Python Logger instance to install Pigweed formatting into. Defaults to the Python root logger: logging.getLogger().

pw_cli.log.main() None

Shows how logs look at various levels.

pw_cli.log.set_all_loggers_minimum_level(level: int) None

Increases the log level to the specified value for all known loggers.

pw_cli.plugins

pw_cli.plugins provides general purpose plugin functionality. The module can be used to create plugins for command line tools, interactive consoles, or anything else. Pigweed’s pw command uses this module for its plugins.

To use plugins, create a pw_cli.plugins.Registry. The registry may have an optional validator function that checks plugins before they are registered (see pw_cli.plugins.Registry.__init__()).

Plugins may be registered in a few different ways.

  • Direct function call. Register plugins by calling pw_cli.plugins.Registry.register() or pw_cli.plugins.Registry.register_by_name().

    registry = pw_cli.plugins.Registry()
    
    registry.register('plugin_name', my_plugin)
    registry.register_by_name('plugin_name', 'module_name', 'function_name')
    
  • Decorator. Register using the pw_cli.plugins.Registry.plugin() decorator.

    _REGISTRY = pw_cli.plugins.Registry()
    
    # This function is registered as the "my_plugin" plugin.
    @_REGISTRY.plugin
    def my_plugin():
        pass
    
    # This function is registered as the "input" plugin.
    @_REGISTRY.plugin(name='input')
    def read_something():
        pass
    

    The decorator may be aliased to give a cleaner syntax (e.g. register = my_registry.plugin).

  • Plugins files. Plugins files use a simple format:

    # Comments start with "#". Blank lines are ignored.
    name_of_the_plugin module.name module_member
    
    another_plugin some_module some_function
    

    These files are placed in the file system and apply similarly to Git’s .gitignore files. From Python, these files are registered using pw_cli.plugins.Registry.register_file() and pw_cli.plugins.Registry.register_directory().

pw_cli.plugins module reference

Provides general purpose plugin functionality.

As used in this module, a plugin is a Python object associated with a name. Plugins are registered in a Registry. The plugin object is typically a function, but can be anything.

Plugins may be loaded in a variety of ways:

  • Listed in a plugins file in the file system (e.g. as “name module target”).

  • Registered in a Python file using a decorator (@my_registry.plugin).

  • Registered directly or by name with function calls on a registry object.

This functionality can be used to create plugins for command line tools, interactive consoles, or anything else. Pigweed’s pw command uses this module for its plugins.

exception pw_cli.plugins.Error

Indicates that a plugin is invalid or cannot be registered.

class pw_cli.plugins.Plugin(name: str, target: Any, source: Optional[pathlib.Path] = None)

Represents a Python entity registered as a plugin.

Each plugin resolves to a Python object, typically a function.

__init__(name: str, target: Any, source: Optional[pathlib.Path] = None) None

Creates a plugin for the provided target.

classmethod from_name(name: str, module_name: str, member_name: str, source: Optional[pathlib.Path]) pw_cli.plugins.Plugin

Creates a plugin by module and attribute name.

Parameters
  • name – the name of the plugin

  • module_name – Python module name (e.g. ‘foo_pkg.bar’)

  • member_name – the name of the member in the module

  • source – path to the plugins file that declared this plugin, if any

help(full: bool = False) str

Returns a description of this plugin from its docstring.

run_with_argv(argv: Iterable[str]) int

Sets sys.argv and calls the plugin function.

This is used to call a plugin as if from the command line.

class pw_cli.plugins.Registry(validator: Callable[[pw_cli.plugins.Plugin], Any] = <function Registry.<lambda>>)

Manages a set of plugins from Python modules or plugins files.

__init__(validator: Callable[[pw_cli.plugins.Plugin], Any] = <function Registry.<lambda>>) None

Creates a new, empty plugins registry.

Parameters

validator – Function that checks whether a plugin is valid and should be registered. Must raise plugins.Error is the plugin is invalid.

detailed_help(plugins: Iterable[str] = ()) Iterator[str]

Yields lines of detailed information about commands.

plugin(function: Optional[Callable] = None, *, name: Optional[str] = None) Callable[[Callable], Callable]

Decorator that registers a function with this plugin registry.

register(name: str, target: Any) Optional[pw_cli.plugins.Plugin]

Registers an object as a plugin.

register_by_name(name: str, module_name: str, member_name: str, source: Optional[pathlib.Path] = None) Optional[pw_cli.plugins.Plugin]

Registers an object from its module and name as a plugin.

register_directory(directory: pathlib.Path, file_name: str, restrict_to: Optional[pathlib.Path] = None) None

Finds and registers plugins from plugins files in a directory.

Parameters
  • directory – The directory from which to start searching up.

  • file_name – The name of plugins files to look for.

  • restrict_to – If provided, do not search higher than this directory.

register_file(path: pathlib.Path) None

Registers plugins from a plugins file.

Any exceptions raised from parsing the file are caught and logged.

run_with_argv(name: str, argv: Iterable[str]) int

Runs a plugin by name, setting sys.argv to the provided args.

This is used to run a command as if it were executed directly from the command line. The plugin is expected to return an int.

Raises

KeyError if plugin is not registered.

short_help() str

Returns a help string for the registered plugins.

pw_cli.plugins.callable_with_no_args(plugin: pw_cli.plugins.Plugin) None

Checks that a plugin is callable without arguments.

May be used for the validator argument to Registry.

pw_cli.plugins.find_all_in_parents(name: str, path: pathlib.Path) Iterator[pathlib.Path]

Searches all parent directories of the path for files or directories.

pw_cli.plugins.find_in_parents(name: str, path: pathlib.Path) Optional[pathlib.Path]

Searches parent directories of the path for a file or directory.

pw_cli.plugins.import_submodules(module: module, recursive: bool = False) None

Imports the submodules of a package.

This can be used to collect plugins registered with a decorator from a directory.