Plugin API Reference

This is the plugin API of spin. It contains functions and classes that are necessary for plugins to register themselves with spin, e.g. task(), and convenience APIs that aim to simplify plugin implementation.

spin’s task management (aka subcommands) is just a thin wrapper on top of the venerable package click, so to create any slightly advanced command line interfaces for plugins you want to make yourself comfortable with click’s documentation.

Defining tasks

csspin.task(*args: Any, **kwargs: Any) Callable[source]

Decorator that creates a task. This is a wrapper around Click’s click.command() decorator, with some extras:

  • a string keyword argument when adds the task to the list of commands to run using invoke()

  • aliases is a list of aliases for the command (e.g. “tests” is an alias for “test”)

  • noenv=True registers the command as a global command, that can run without a provisioned environment

task introspects the signature of the decorated function and handles certain argument names automatically:

  • ctx will pass the Click context object into the task; this is rarely useful for spin tasks

  • cfg will automatically pass the configuration tree; this is very useful most of the time, except for the simplest of tasks

  • args will simply pass through all command line arguments by using the ignore_unknown_options and allow_extra_args options of the Click context; this is often used for tasks that launch a specific command line tool to enable arbitrary arguments

All other arguments to the task must be annotated with either option() or argument(). They both support the same arguments as the corresponding decorators click.option() and click.argument().

A simple example:

1@task()
2def simple_task(cfg, args):
3    foo("do something")

This would make simple_task available as a new subcommand of spin.

More elaborate examples can be found in the built-in plugins shipping with spin.

csspin.group(*args: Any, **kwargs: Any) Callable[source]

Decorator for task groups, to create nested commands.

This works like click.Group, but additionally supports subcommand aliases, that can be set via the aliases keyword argument to task(). Example:

@group()
def foo():
    pass


@foo.task()
def bar():
    pass

The above example creates a spin foo bar command.

csspin.argument(**kwargs: Any) Callable[source]

Annotations task arguments.

This works just like click.argument(), accepting all the same parameters. Example:

1@task()
2def mytask(outfile: argument(type="...", help="...")):
3    foo("do something")
csspin.option(*args: Any, **kwargs: Any) Callable[source]

Annotations for task options.

This works just like click.option(), accepting the same parameters. Example:

 1@task()
 2def mytask(
 3    outfile: option(
 4        "-o",
 5        "outfile",
 6        default="-",
 7        type=click.File("w"),
 8        help="... usage information ...",
 9    ),
10):
11    foo("do something")

Interacting with spin

csspin.config(*args: Any | None, **kwargs: Any) ConfigTree[source]

config creates a configuration subtree:

>>> config(a="alpha", b="beta)
{"a": "alpha", "b": "beta}

Plugins use config to declare their defaults tree.

csspin.invoke(hook: str, *args: Any, **kwargs: Any) None[source]

invoke() invokes the tasks that have the when hook hook. As an example, here is the implementation of test:

@task(aliases=["tests"])
def test(cfg, coverage: option("--coverage", "coverage", is_flag=True)):
    """Run all tests defined in this project"""
    invoke("test", coverage=coverage)

The way a task that uses invoke is invoking other tasks is part of the call interface contract: all tasks initialized like @task(when="test") must support the coverage argument as part of their Python function signature (albeit not necessarily the same command line flag --coverage).

csspin.get_tree() ConfigTree[source]

Return the global configuration tree.

csspin.interpolate1(literal: str | Path, *extra_dicts: dict, interpolate_environ: bool = True) str | Path[source]

Interpolate a string or path against the configuration tree and the environment.

Example:

>>> interpolate1("{SHELL}")
'/usr/bin/zsh'

If literal is not a string or path, it will be converted to a string prior interpolating.

To avoid interpolation for literals or specific parts of a literal, curly braces can be used to escape curly braces, like regular f-string interpolation.

Example:

>>> interpolate1(
...     '{{"header": {{"language": "en", "data": "{SPIN_DATA}"}}}}'
... )
'{"header": {"language": "en", "data": "/home/developer/.local/share/spin"}}'

It may be necessary to omit the interpolation against the environment, in that case the parameter interpolate_environ can be set to False.

Example:

>>> interpolate1("{spin.version} and {PATH}", interpolate_environ=False)
"1.0.2.dev5 and {PATH}"

Attention

Do not use csspin.interpolate1() in a plugins’ top-level, as the one can’t rely on the configuration tree at import time of the module.

Negative example: How not to use csspin.interpolate1()
1from csspin import config, interpolate1
2
3defaults = config(key=interpolate1("{some.property}"))

Attention

If the interpolated property is not set, not NoneType but “None” as a string is returned.

csspin.interpolate(literals: Iterable, *extra_dicts: dict) list[source]

Interpolate an iterable of hashable items against the configuration tree.

csspin.namespaces(*nslist: dict) Generator[source]

Add namespaces for interpolation.

csspin.toporun(cfg: ConfigTree, *fn_names: Any, reverse: bool = False) None[source]

Run plugin functions named in ‘fn_names’ in topological order.

csspin.EXPORTS: list[tuple[str, str]] = []

EXPORTS is a list that contains all (key, value) tuples of environment variables that got set or unset via csspin.setenv() during the current spin execution.

The value of a given element is already fully interpolated, except for parts that look like environment variables. So any plugin using EXPORTS is able to lazily evaluate the value of a variable in cases, where it has been set multiple times.

A case where that’s relevant can be seen in the following example:

Example:

>>> os.environ.getenv("PATH")
"/usr/bin:/bin"
>>> setenv(PATH="{spin.project_root}/bin:{PATH}")
>>> setenv(PATH="{python.scriptdir}:{PATH}")
>>> EXPORTS
[("PATH", "/home/foo/project/bin:{PATH}"), ("PATH", "/home/foo/project/.spin/venv/bin:{PATH})]

As can be seen, the real value of PATH should be "/home/foo/project/.spin/venv/bin:/home/foo/project/bin:"/usr/bin:/bin", which any plugin could now generate.

Communication with the user

class csspin.Verbosity(value)[source]

enum.IntEnum defining four verbosity levels:

csspin.echo(*msg: str, resolve: bool = False, **kwargs: Any) None[source]

Print a message to the console by joining the positional arguments msg with spaces.

echo is meant for messages that explain to the user what spin is doing (e.g. echoing commands launched). It will remain silent though when spin is run with the --quiet flag. If the parameter resolve is set to True, the arguments are interpolated against the configuration tree.

echo supports the same keyword arguments as Click’s click.echo().

csspin.info(*msg: str, **kwargs: Any) None[source]

Print a message to the console by joining the positional arguments msg with spaces.

Arguments are interpolated against the configuration tree. info will remain silent unless spin is run with the --verbose flag. info is meant for messages that provide additional details.

info supports the same keyword arguments as Click’s click.echo().

csspin.debug(*msg: str, resolve: bool = False, **kwargs: Any) None[source]

Print a message to the console by joining the positional arguments msg with spaces.

Arguments are interpolated against the configuration tree if resolve evaluates to True. debug will remain silent unless spin is run with the -vv flag. debug is meant for messages that provide internal details.

debug supports the same keyword arguments as Click’s click.echo().

csspin.warn(*msg: str, **kwargs: Any) None[source]

Print a warning message to the console by joining the positional arguments msg with spaces.

Arguments are interpolated against the configuration tree. The output is written to standard error.

warn supports the same keyword arguments as Click’s click.echo().

csspin.error(*msg: str, resolve: bool = True, **kwargs: Any) None[source]

Print an error message to the console by joining the positional arguments msg with spaces.

Arguments are interpolated against the configuration tree if resolve evaluates to True. The output is written to standard error.

error supports the same keyword arguments as Click’s click.echo().

csspin.die(*msg: Any, resolve: bool = True) None[source]

Terminates spin with a non-zero return code and print the error message msg.

Arguments are interpolated against the configuration tree if resolve evaluates to True.

Handling Processes

csspin.sh(*cmd: Any, **kwargs: Any) subprocess.CompletedProcess | None[source]

Run a program by building a command line from cmd.

When multiple positional arguments are given, each is treated as one element of the command. When just one positional argument is used, sh assumes it to be a single command and splits it into multiple arguments using shlex.split(). The cmd arguments are interpolated against the configuration tree. When silent is False, the resulting command line will be echoed. When shell is True, the command line is passed to the system’s shell.

Other keyword arguments are passed into subprocess.run().

All positional arguments are interpolated against the configuration tree.

>>> sh("ls", "{HOME}")
csspin.setenv(*args: Any, **kwargs: Any) None[source]

Set or unset one or more environment variables. The values of keyword arguments are interpolated against the configuration tree.

Passing None as a value removes the environment variable.

Variables that have been set during or before configure() will be patched into the activation scripts of the Python virtual environment.

On Windows, all passed environment variable keys will be set in upper case.

>>> setenv(FOO="{foo.bar}", BAZ="some-value")
class csspin.Command(*cmd: str)[source]

Create a function that is a shrink-wrapped shell command.

The callable returned behaves like sh(), accepting additional arguments for the wrapper command as positional parameters. All positional arguments are interpolated against the configuration tree.

Example:

>>> install = Command("pip", "install")
>>> install("spin")

Handling state

class csspin.Memoizer(fn: str | Path)[source]

Maintain a persistent base of simple facts.

Facts are loaded from file fn. The argument is interpolated against the configuration tree. If fn does not exist, there are no facts.

The Memoizer class stores and retrieves Python objects from the binary file named fn. The argument is interpolated against the configuration tree. Memoizer can be used to keep a simple “database”. Spin internally uses Memoizers for e.g. keeping track of packages installed in a virtual environment.

To ease the handling in spin scripts, there also is context manager called memoizer (note the lower case “m”). The context manager retrieves the database from the file and saves it back when the context is closed:

>>> with memoizer(fn) as m:
...    if m.check("test"): ...

There are no precautions for simultaneous access from multiple processes, writes will likely silently become lost.

csspin.memoizer(fn: str) Generator[source]

Context manager for creating a Memoizer that automatically saves the fact base.

>>> with memoizer("facts.memo") as m:
...   m.add("fact1")
...   m.add("fact2")

Files and Path handling

csspin.cd(path: str | Path) DirectoryChanger[source]

Change directory.

The path argument is interpolated against the configuration tree.

cd can be used either as a function or as a context manager. When used as a context manager, the working directory is changed back to what it was before the with block.

You can do this:

>>> cd("{spin.project_root}")

… or that:

>>> with cd("{spin.project_root}"):
    <do something in this directory>
csspin.copy(source: str | Path, target: str | Path) None[source]

Copy a file or directory recursively from source to target in case the target exists.

csspin.exists(path: str | Path) bool[source]

Check whether path exists. The argument is interpolated against the configuration tree.

csspin.mkdir(path: str | Path) str[source]

Ensure that path exists.

If necessary, directories are recursively created to make path available. The argument is interpolated against the configuration tree.

csspin.mv(source: str | Path, target: str | Path) None[source]

Move a file or directory recursively from source to target in case the target exists, otherwise rename source to target.

csspin.rmtree(path: str | Path) None[source]

Recursively remove path and everything it contains. Can also remove single files. The argument is interpolated against the configuration tree.

Obviously, this should be used with care.

csspin.download(url: str, location: str | Path, headers: dict | None = None) None[source]

Download data from url to location using optional headers.

csspin.abspath(*args: str | Path) str[source]

Interpolate and return an absolute path as str

csspin.normpath(*args: str | Path) str[source]

Interpolate and return a normalized path as str

csspin.appendtext(fn: str | Path, data: str) int[source]

Append data, which is text (Unicode object of type str) to the file named fn.

The file name argument is interpolated against the configuration tree.

csspin.getmtime(fn: str | Path) float[source]

Get the modification of file fn.

fn is interpolated against the configuration tree.

csspin.persist(fn: str | Path, data: Type[object]) int[source]

Persist the Python object(s) in data using pickle.

csspin.readbytes(fn: str | Path) bytes[source]

readbytes reads binary data. The file name argument is interpolated against the configuration tree.

csspin.readlines(fn: str | Path) list[str][source]
csspin.readtext(fn: str | Path) str[source]

Read an UTF8 encoded text from the file ‘fn’.

The file name argument is interpolated against the configuration tree.

csspin.readyaml(fname: str | Path) ConfigTree[source]

Read a YAML file.

csspin.unpersist(fn: str, default: Any | None = None) Any | None[source]

Load pickled Python object(s) from the file fn.

csspin.writebytes(fn: str | Path, data: bytes) int[source]

Write data` to the file named fn.

Data is binary data (bytes). The file name argument is interpolated against the configuration tree.

csspin.writelines(fn: str | Path, lines: str) None[source]
csspin.writetext(fn: str | Path, data: str) int[source]

Write data, which is text (Unicode object of type str) to the file named fn.

The file name argument is interpolated against the configuration tree.