magicinvoke Documentation

An invoke extension that adds support for lots of goodies. See for more doc.

magicinvoke.magictask(*args, **kwargs)

An invoke.task replacement that supports make-like file dependencies and convenient arg defaulting from the ctx.


List of extras over invoke.Task

  1. You can configure your Tasks just like you configure invoke’s run function. See magicinvoke.get_params_from_ctx() for more:

    def thisisatask(ctx, arg1):
    ns.configure({'arg1':'default for a positional arg! :o'})
  2. Your task won’t run if its output files are newer than its input files. See magicinvoke.skippable()’s documentation for more details and help.

Note the path argument on magicinvoke.get_params_from_ctx() has been renamed to params_from in this decorator for clearer code.

This decorator is just a wrapper for the sequence:

def mytask(ctx):

New in version 0.1.

magicinvoke.get_params_from_ctx(func=None, path=None, derive_kwargs=None)

Derive parameters for this function from ctx, if possible.

  • path (str) –

    A path in the format 'ctx.arbitraryname.unpackthistomyparams' to use to find defaults for the function. Default: 'ctx.mymodulename.myfuncname'

    It’s good to pass this explicitly to make it clear where your arguments are coming from.

  • derive_kwargs (Callable) – Overkill. Is passed ctx as first arg, expected to return dict of the format {'argname': 'defaultvalueforparam'}.


@get_params_from_ctx(path='ctx.randompath')  # just 'ctx' works as well
def myfuncname(ctx, requiredparam1, namedparam2='trulyoptional'):
    print(requiredparam1, namedparam2)

If your default is a callable we will call it with args[0]. This is how invoke.config.Lazy() works under the hood. That is, this is a valid function:

def myfuncname(ctx,
               namedparam1=lambda ctx: ctx.myvalue * 4):
    print(namedparam1)  # 4, if myvalue == 1 :)

Why do I need this? Suppose this configuration:

ctx = {"myfuncname" : {"requiredparam1" : 392, "namedparam2" : 199}}

And this task, where it’s important that we always have a value for some parameter, but we don’t always want to supply it from the command-line:

def myfuncname(ctx, requiredparam=None, namedparam2=None):
    requiredparam1 = requiredparam1 or ctx.myfuncname.requiredparam1
    if not requiredparam1:
        raise ValueError("Need a value for requiredparam1, but didn't want
                        user to always have to give one.")
    namedparam2 = namedparam2 or ctx.myfuncname.namedparam2
    print(requiredparam1, namedparam2)

This task can be invoked from the command line like so:

$ invoke myfuncname
(392, 199)

Other functions/tasks can re-use our task with custom parameters, and the cmd-line user can override our config’s defaults if he or she wishes.

However, the semantics of this function are hidden behind the boilerplate of finding values for each argument.

Requiredparam1 and namedparam2 are really required, we just can’t reveal that in the function signature, or `invoke` will force the user to give one every time they call our task, even though we have a default in the config we defined.

One solution is something like this:

def myfuncname(ctx, requiredparam1, namedparam2):
    print(param1, requiredparam1)::

def myfuncname_task(ctx, requiredparam1=None, namedparam2=None)
    requiredparam1 = namedparam1 or ctx.myfuncname.namedparam1
    namedparam2 = namedparam2 or ctx.myfuncname.namedparam2
    myfuncname(ctx, requiredparam1, namedparam2)

This solution decouples the core of your code from invoke, which could be seen as a plus. However, if we were going to write this much boiler-plate and passing stuff around, we could have stuck with argparse.

Also, notice that each parameter name appears 6 times, and the function name appears 3 times. Maybe it’s not the worst nightmare for maintainability, but it sure gives writing a new re-usable task quite a lot of friction, so most just won’t do it. They’ll write the task, and you’ll either get runtime Nones because you forgot to load a newly added param from the ctx, or you’ll have a cmd-line experience so painful that people generate calls to your task from their own configs and scripts.

Here’s a better solution. It mirrors the logic of the above pair of functions, but with a simple decorator instead.:

def myfuncname(ctx, requiredparam1, namedparam2='trulyoptional')
    print(requiredparam1, namedparam2)

ns.configure({"tasks": {"myfuncname" : {"requiredparam1" : 392}}})

The semantics of the raw python function now match the cmd-line task:

  • You can call it with no arguments, and as long as a proper value is found in ctx or in the signature of the function, it will run just like you called it from the cmd-line.
  • If no value was passed, and no default can be found, you will get a normal Python error.

The cascading order for finding an argument value is as follows:

  1. directly passed (i.e. task(ctx, 'arghere') or --arg arghere on cmd line
  2. from config (ctx arg) (defaults to ctx.__module__.func.__name__)
  3. function defaults (def myfunc(ctx, default=1)) - default parameter values that are callable are called with callable(ctx) to get the value that should be used for a default.

New in version 0.1.

magicinvoke.skippable(func, decorator=None)

Decorator to skip function if input files are older than output files.

You can use this to write functions that are like make-targets.

We derive your input and output paths from a combo of three ways:

  1. A parameter name has ‘input’, or ‘output’ in it. For example:

    def mytask(input_path, outputpath1, myoutput):

    would be interpreted by this decorator as

    {'inputs': [input_path], 'outputs': [outputpath1, myoutput]}

    but note that in the future we might also require path to be in the name to allow passing open file descriptors or other things that happen to be named similarly.

  2. A parameter is type-annotated with magicinvoke.InputPath/ magicinvoke.OutputPath or [InputPath/OutputPath]:

    from magicinvoke import InputPath, skippable
    def mytask(path1: InputPath, input_paths: [InputPath]):
  3. Params that aren’t otherwise classified, but that have path/file in their name, will be classified as ‘inputs’ for the sake of determining if we should run your function or not. This is a concession to Py2, but also helps if you’re taking the path of an executable and don’t want to annotate it as an InputPath.:

    def run_complex_binary(config_path, binary_path, output_paths):

    would be interpreted by this decorator as

    {'inputs': [config_path, binary_path], 'outputs': output_path}

  4. @skippable(extra_deps=lambda ctx: ctx.buildconfig.important_executable_path) TODO support this For now, you must just list a .pre which takes that path as an input (not as painful as you might imagine) thanks to get_params_from_ctx

How it works: We use the timestamps of your input filenames and output filenames in much the same way as make (i.e. just compare modified timestamp) However, we also put a file in $TMPDIR that records the arguments given to your function. That way, a function like this works as expected:

def get_output(input_path, output_path, flag_that_changes_output):

By default, _all_ arguments that aren’t filepaths are considered significant (except first arg if named c/ctx i.e. invoke-style). However, you can mark an argument as not-significant to the output by starting its name with an underscore:

def get_output(input_path, output_path, _flag_that_doesnt_affect_output):

New in version 0.1.

class magicinvoke.Path
Export of vendored (for Py2)
class magicinvoke.dotdict
Export of vendored
class magicinvoke.colored_traceback

Importing magicinvoke tries to import this library to turn on colored tracebacks. If the library is unavailable, nothing is different. It’s a great tool with no real downsides except for requiring colorama on Windows.

You should also check out tbvaccine