Gather

Overview

The gather package allows gathering up plugins.

Entry points

Gathering depends on registering an entry point in the package. For example, with pyproject.toml:

[project.entry-points.gather]
ignored = "<ROOT_PACKAGE>"

Putting the package name there is enough – gather will automatically collect from any sub-modules, recursing any number of levels. These entry points are enough regardless of the plugin collector: collectors will only collect their own plugins.

Collectors

A Collector represents a “kind of plugin”. It is an object, usually defined at the top level of a module:

import gather
THINGS = gather.Collector()

Collecting all registered functions:

registered = THINGS.collect()

The return value is a dictionary, mapping names to sets of registered functions.

The function gather.unique takes a dictionary, and returns a dictionary mapping names to registered functions. It will raise a ValueError if multiple functions are registered to the same name.

Registering

In order to register a function as a plugin, decorate it using the relevant collector:

@THINGS.register()
def some_function():
    pass

The decorator always returns the function without modification. This allows, for example, using some_function in a unit test.

If an alternative name is needed for registration, one can be provided explicitly:

@THINGS.register(name='register_as_this_name')
def generic():
    pass

Transforms

Depending on the collector, it might expect some extra data. This should be documented as part of the collector. Supplying the data is done with the transform argument:

@THINGS.register(
    name='register_as_this_name',
    transform=things_transformer(flexibility=5),
)
def generic():
    pass

The collector can define a transformer using gather.Wrapper:

def things_transformer(flexibility):
    return gather.Wrapper.glue(flexibility)

When collecting, the value in the mapping returned in .collect() will be an object. The .original attribute will be the function. The .extra will be the arguments given to the glue function: in this case, for register_as_name, it will be 5.

API

Plugins

Gather – Collect all your plugins

Gather allows a way to register plugins. It features the ability to register the plugins from any module, in any package, in any distribution. A given module can register plugins of multiple types.

In order to have anything registered from a package, it needs to declare that it supports gather in its package metadata.

For example, with pyproject.toml:

[project.entry-points.gather]
ignored = "<ROOT_PACKAGE>"

The ROOT_PACKAGE should point to the Python name of the package: i.e., what users are expected to import at the top-level.

Note that while having special facilities to run functions as subcommands, Gather can be used to collect anything.

class gather.api.Collector(name=None, depth=1)[source]

A plugin collector.

A collector allows to register functions or classes by modules, and collect-ing them when they need to be used.

collect()[source]

Collect all registered functions or classes.

Returns a dictionary mapping names to registered elements.

register(name=None, transform=<function Collector.<lambda>>)[source]

Register a class or function

Parameters:
  • name (str) – optional. Name to register the class or function as. (default is name of object)

  • transform (callable) – optional. A one-argument function. Will be called, and the return value used in collection. Default is identity function

This is meant to be used as a decoator:

@COLLECTOR.register()
def specific_subcommand(args):
    pass

@COLLECTOR.register(name='another_specific_name')
def main(args):
    pass
class gather.api.Wrapper(original, extra)[source]

Add extra data to an object

classmethod glue(extra)[source]

Glue extra data to an object

Parameters:

extra – what to add

Returns:

function of one argument that returns a Wrapped

Return type:

callable

This method is useful mainly as the transform parameter of a register call.

gather.api.unique(mapping)[source]

Transform map to sets to map to single items.

Raises a ValueError if any of the values is not an iterable with exactly one item.

Parameters:

mapping – A mapping of keys to Iterables of 1

Returns:

A mapping of keys to the single value

Command dispatch

Registration and dispatch to sub-commands

gather.commands.run(*, parser, argv=sys.argv, env=os.environ, sp_run=subprocess.run)[source]

Parse arguments and run the command.

Pass non-default args in testing scenarios.

Parameters:
  • argv – sys.argv or something that looks like it

  • env – os.environ or something that looks like it

  • sp_run – subprocess.run or something that looks like it

Returns:

Return value from dispatched command

gather.commands.add_argument(*args, **kwargs)[source]

Add argument to a registered command.

See argparse.ArgumentParser.add_argument for a description of the argument semantics.

gather.commands.make_command_register(collector)[source]

Return a decorator that registers a command.

Parameters:

collector – Collector to add commands to

Returns:

Callable that expects positional add_argument arguments, and returns a decorator that registers the function to the collector.

gather.commands.set_parser(*, collected, parser=None)[source]

Set (or create) a parser.

The parser will dispatch to the functions collected. The parser will configure the argument parsing according to the function’s add_argument in the registration.

Parameters:
  • collected – Return value from Collector.collected

  • parser – an argument parser

Returns:

An argument parser

Script entry points

Abstractions for writing entrypoints.

This is meant to reduce the overhead when writing a command with subcommands.

In the example below, the assumption is that your code is in the package awesomeawesome.

In awesomeawesome/__init__.py:

from gather import entry
ENTRY_DATA = entry.EntryData.create(__name__)

In __main__.py:

from gather import entry
from . import ENTRY_DATA

entry.dunder_main(
    globals_dct=globals(),
    command_data=ENTRY_DATA,
)

Registering a new subcommand is done by adding the following to, say, awesomeawesome/commands.py:

from gather.commands import add_argument
from . import ENTRY_DATA
from commander_data import COMMAND


@ENTRY_DATA.register()
def hello(args):
    LOGGER.info("Hello world")


@ENTRY_DATA.register(
    add_argument("--no-dry-run"),
    add_argument("--a-thing", default="the-thing"),
)
def frobnicate(args):
    args.run(
        COMMAND.rm(recursive=None, force=None)
    ) # Will only run with --no-dry-run
    hello = args.safe_run(
        COMMAND.echo("hello")
    ).stdout.strip() # Will run regardless

Note that commands can be added in any file, as long as they are registered properly.

Optionally, you can add script entry points in pyproject.toml:

[project.scripts]
awesomeawesomectl = "awesomeawesome:ENTRY_DATA.main_command"
frobnicate = "awesome:ENTRY_DATA.sub_command"

In that case, the following will work:

  • python -m awesomeawesome hello

  • awesomeawesome hello

  • python -m awesomeawesome frobnicate

  • awesomeawesome frobnicate

  • frobincate

class gather.entry.EntryData(prefix: str, collector: Collector, register: Callable, main_command: Callable[[], None], sub_command: Callable[[], None])[source]

Data for the entry point.

classmethod create(package_name, prefix=None)[source]

Create a new instance from package_name and prefix

gather.entry.dunder_main(globals_dct, command_data, logger=<RootLogger root (WARNING)>)[source]

Call from __main__