magicinvoke
Documentation¶
An invoke
extension that adds support for lots of goodies. See
magicinvoke.py for more doc.
-
magicinvoke.
magictask
(*args, **kwargs)¶ An
invoke.task
replacement that supports make-like file dependencies and convenient arg defaulting from the ctx.Important
List of extras over
invoke.Task
You can configure your Tasks just like you configure invoke’s
run
function. Seemagicinvoke.get_params_from_ctx()
for more:@magictask def thisisatask(ctx, arg1): print(arg1) ns.configure({'arg1':'default for a positional arg! :o'})
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 onmagicinvoke.get_params_from_ctx()
has been renamed toparams_from
in this decorator for clearer code.This decorator is just a wrapper for the sequence:
@task @get_params_from_ctx @skippable def mytask(ctx): pass
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.
Parameters: - 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'}
.
Examples:
@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 howinvoke.config.Lazy()
works under the hood. That is, this is a valid function:@get_params_from_ctx def myfuncname(ctx, namedparam0=Lazy('ctx.mynamedparam0'), 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:
@task 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
andnamedparam2
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):: @task(name=myfuncname) 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.:
@task @get_params_from_ctx 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:
- directly passed (i.e.
task(ctx, 'arghere')
or--arg arghere
on cmd line - from config (
ctx
arg) (defaults to ctx.__module__.func.__name__) - 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.
- path (str) –
-
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:
A parameter name has ‘input’, or ‘output’ in it. For example:
@skippable def mytask(input_path, outputpath1, myoutput): pass
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.A parameter is type-annotated with
magicinvoke.InputPath
/magicinvoke.OutputPath
or[InputPath/OutputPath]
:from magicinvoke import InputPath, skippable @skippable def mytask(path1: InputPath, input_paths: [InputPath]): pass
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 anInputPath
.:@skippable def run_complex_binary(config_path, binary_path, output_paths): pass
would be interpreted by this decorator as
{'inputs': [config_path, binary_path], 'outputs': output_path}
@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): pass
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): pass
New in version 0.1.
-
class
magicinvoke.
Path
¶ - Export of vendored (for Py2)
- pathlib.Path
-
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 requiringcolorama
on Windows.You should also check out tbvaccine