API Reference#

pw_cli: Enhance and accelerate custom command-line tooling

pw_cli.decorators#

Common helpful decorators for Python code.

pw_cli.decorators.deprecated(deprecation_note: str)#

Deprecation decorator.

Emits a depreciation warning when the annotated function is used.

An additional deprecation note is required to redirect users to the appropriate alternative.

pw_cli.file_filter#

Class for describing file filter patterns.

class pw_cli.file_filter.FileFilter(
*,
exclude: Iterable[Pattern | str] = (),
endswith: Iterable[str] = (),
name: Iterable[Pattern | str] = (),
suffix: Iterable[str] = (),
)#

Allows checking if a path matches a series of filters.

Positive filters (e.g. the file name matches a regex) and negative filters (path does not match a regular expression) may be applied.

__init__(
*,
exclude: Iterable[Pattern | str] = (),
endswith: Iterable[str] = (),
name: Iterable[Pattern | str] = (),
suffix: Iterable[str] = (),
) None#

Creates a FileFilter with the provided filters.

Parameters:
  • endswith – True if the end of the path is equal to any of the passed strings

  • exclude – If any of the passed regular expresion match return False. This overrides and other matches.

  • name – Regexs to match with file names(pathlib.Path.name). True if the resulting regex matches the entire file name.

  • suffix – True if final suffix (as determined by pathlib.Path) is matched by any of the passed str.

matches(path: str | Path) bool#

Returns true if the path matches any filter but not an exclude.

If no positive filters are specified, any paths that do not match a negative filter are considered to match.

If ‘path’ is a Path object it is rendered as a posix path (i.e. using “/” as the path seperator) before testing with ‘exclude’ and ‘endswith’.

pw_cli.git_repo#

Helpful commands for working with a Git repository.

exception pw_cli.git_repo.GitError(args: Iterable[str], message: str, returncode: int)#

A Git-raised exception.

__init__(args: Iterable[str], message: str, returncode: int) None#
class pw_cli.git_repo.GitRepo(root: Path, tool_runner: ToolRunner)#

Represents a checked out Git repository that may be queried for info.

__init__(root: Path, tool_runner: ToolRunner)#
commit_author(commit: str = 'HEAD') str#

Returns the author of the specified commit.

Defaults to HEAD if no commit specified.

Returns:

Commit author as a string.

commit_change_id(commit: str = 'HEAD') str | None#

Returns the Gerrit Change-Id of the specified commit.

Defaults to HEAD if no commit specified.

Returns:

Change-Id as a string, or None if it does not exist.

commit_date(commit: str = 'HEAD') datetime#

Returns the datetime of the specified commit.

Defaults to HEAD if no commit specified.

Returns:

Commit datetime as a datetime object.

commit_hash(commit: str = 'HEAD', short: bool = True) str#

Returns the hash associated with the specified commit.

Defaults to HEAD if no commit specified.

Returns:

Commit hash as a string.

commit_message(commit: str = 'HEAD') str#

Returns the commit message of the specified commit.

Defaults to HEAD if no commit specified.

Returns:

Commit message contents as a string.

current_branch() str | None#

Returns the current branch, or None if it cannot be determined.

has_uncommitted_changes() bool#

Returns True if this Git repo has uncommitted changes in it.

Note: This does not check for untracked files.

Returns:

True if the Git repo has uncommitted changes in it.

list_files(commit: str | None = None, pathspecs: Collection[Path | str] = ()) list[Path]#

Lists files modified since the specified commit.

If commit is not found in the current repo, all files in the repository are listed.

Arugments:

commit: The Git hash to start from when listing modified files pathspecs: Git pathspecs use when filtering results

Returns:

A sorted list of absolute paths.

list_submodules(excluded_paths: Collection[Pattern | str] = ()) list[Path]#

Query Git and return a list of submodules in the current project.

Parameters:

excluded_paths – Pattern or string that match submodules that should not be returned. All matches are done on posix-style paths relative to the project root.

Returns:

List of “Path”s which were found but not excluded. All paths are absolute.

root() Path#

The root file path of this Git repository.

Returns:

The repository root as an absolute path.

tracking_branch(fallback: str | None = None) str | None#

Returns the tracking branch of the current branch.

Since most callers of this function can safely handle a return value of None, suppress exceptions and return None if there is no tracking branch.

Returns:

the remote tracking branch name or None if there is none

pw_cli.git_repo.describe_git_pattern(
working_dir: Path,
commit: str | None,
pathspecs: Collection[Path | str],
exclude: Collection[Pattern],
tool_runner: ToolRunner,
project_root: Path | None = None,
) str#

Provides a description for a set of files in a Git repo.

Example

files in the pigweed repo - that have changed since origin/main..HEAD - that do not match 7 patterns (…)

The unit tests for this function are the source of truth for the expected output.

Returns:

A multi-line string with descriptive information about the provided Git pathspecs.

pw_cli.git_repo.find_git_repo(path_in_repo: Path, tool_runner: ToolRunner) GitRepo#

Tries to find the root of the Git repo that owns path_in_repo.

Raises:

GitError – The specified path does not live in a Git repository.

Returns:

A GitRepo representing the the enclosing repository that tracks the specified file or folder.

pw_cli.git_repo.is_in_git_repo(p: Path, tool_runner: ToolRunner) bool#

Returns true if the specified path is tracked by a Git repository.

Returns:

True if the specified file or folder is tracked by a Git repository.

pw_cli.log#

Tools for configuring Python logging.

pw_cli.log.all_loggers() Iterator[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: str | int = 20,
use_color: bool | None = None,
hide_timestamp: bool = False,
log_file: str | Path | None = None,
logger: Logger | None = None,
debug_log: str | Path | None = None,
time_format: str = '%Y%m%d %H:%M:%S',
msec_format: str = '%s,%03d',
include_msec: bool = False,
message_format: str = '%(levelname)s %(message)s',
) 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 send logs into instead of the terminal.

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

  • debug_log – File to log to from all levels, regardless of chosen log level. Logs will go here in addition to the terminal.

  • time_format – Default time format string.

  • msec_format – Default millisecond format string. This should be a format string that accepts a both a string %s and an integer %d. The default Python format for this string is %s,%03d.

  • include_msec – Whether or not to include the millisecond part of log timestamps.

  • message_format – The message format string. By default this includes levelname and message. The asctime field is prepended to this unless hide_timestamp=True.

pw_cli.log.main() int#

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().

    import pw_cli
    
    _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.

    import pw_cli
    
    _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().

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: Path | None = 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: Path | None = None) None#

Creates a plugin for the provided target.

classmethod from_name(name: str, module_name: str, member_name: str, source: Path | None) 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: ~typing.Callable[[~pw_cli.plugins.Plugin], ~typing.Any] = <function Registry.<lambda>>)#

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

__init__(validator: ~typing.Callable[[~pw_cli.plugins.Plugin], ~typing.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: Callable | None = None,
*,
name: str | None = None,
) Callable[[Callable], Callable]#

Decorator that registers a function with this plugin registry.

register(name: str, target: Any) Plugin | None#

Registers an object as a plugin.

register_by_name(
name: str,
module_name: str,
member_name: str,
source: Path | None = None,
) Plugin | None#

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

register_config(config: dict, path: Path | None = None) None#

Registers plugins from a Pigweed config.

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

register_directory(directory: Path, file_name: str, restrict_to: Path | None = 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: 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: 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: Path) Iterator[Path]#

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

pw_cli.plugins.find_in_parents(name: str, path: Path) Path | None#

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

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

Imports the submodules of a package.

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

pw_cli.plural#

Utilities for handling singular/plural forms in logs and tooling.

pw_cli.plural.plural(
items_or_count,
singular: str,
count_format='',
these: bool = False,
number: bool = True,
are: bool = False,
exist: bool = False,
) str#

Returns the singular or plural form of a word based on a count.

Parameters:
  • items_or_count – Number of items or a collection of items

  • singular – Singular form of the name of the item

  • count_format – .format()-style specification for items_or_count

  • these – Prefix the string with “this” or “these”, depending on number

  • number – Include the number in the return string (e.g., “3 things” vs. “things”)

  • are – Suffix the string with “is” or “are”, depending on number

  • exist – Suffix the string with “exists” or “exist”, depending on number

pw_cli.status_reporter#

Attractive status output to the terminal (and other places if you want).

class pw_cli.status_reporter.LoggingStatusReporter(logger: Logger)#

Print status lines to logs instead of to the terminal.

__init__(logger: Logger) None#
class pw_cli.status_reporter.StatusReporter#

Print user-friendly status reports to the terminal for CLI tools.

You can instead redirect these lines to logs without formatting by substituting LoggingStatusReporter. Consumers of this should be designed to take any subclass and not make assumptions about where the output will go. But the reason you would choose this over plain logging is because you want to support pretty-printing to the terminal.

This is also “themable” in the sense that you can subclass this, override the methods with whatever formatting you want, and supply the subclass to anything that expects an instance of this.

Key:

  • info: Plain ol’ informational status.

  • ok: Something was checked and it was okay.

  • new: Something needed to be changed/updated and it was successfully.

  • wrn: Warning, non-critical.

  • err: Error, critical.

This doesn’t expose the %-style string formatting that is used in idiomatic Python logging, but this shouldn’t be used for performance-critical logging situations anyway.

demo()#

Run this to see what your status reporter output looks like.

pw_cli.tool_runner#

A subprocess wrapper that enables injection of externally-provided tools.

class pw_cli.tool_runner.ToolRunner#

A callable interface that runs the requested tool as a subprocess.

This class is used to support subprocess-like semantics while allowing injection of wrappers that enable testing, finer granularity identifying where tools fail, and stricter control of which binaries are called.

By default, all subprocess output is captured.

__call__(
tool: str,
args: Iterable[str | Path],
stdout: int | None = -1,
stderr: int | None = -1,
**kwargs,
) CompletedProcess#

Calls tool with the provided args.

**kwargs are forwarded to the underlying subprocess.run() for the requested tool.

By default, all subprocess output is captured.

Returns:

The subprocess.CompletedProcess result of running the requested tool.

static _custom_args() Iterable[str]#

List of additional keyword arguments accepted by this tool.

By default, all kwargs passed into a tool are forwarded to subprocess.run(). However, some tools have extra arguments custom to them, which are not valid for subprocess.run(). Tools requiring these custom args should override this method, listing the arguments they accept.

To make filtering custom arguments possible, they must be prefixed with pw_.

abstract _run_tool(tool: str, args, **kwargs) CompletedProcess#

Implements the subprocess runner logic.

Calls tool with the provided args. **kwargs not listed in _custom_args are forwarded to the underlying subprocess.run() for the requested tool.

Returns:

The subprocess.CompletedProcess result of running the requested tool.

class pw_cli.tool_runner.BasicSubprocessRunner#

A simple ToolRunner that calls subprocess.run().