The problem
I manage my site with make and a hodgepodge of shell scripts. This approach sort of collapsed under its own weight the other day. I needed to update the “ask Hugo to create a new post or note” scripts to accommodate a change in folder layout. I wrote one of the scripts in bash, and the other in Perl. All of it managed by make
, with a string of .PHONY
targets.
Not that there’s anything wrong with that.
A solution: Invoke
But I want to try a more unified approach. I also decided that 2020 is my year of Python.
I could use the Pyinvoke tool created by Jeff Forcier for this unified approach.
Invoke gives you a task runner and support library for managing those tasks. It works a bit like Make, except that you define tasks with Python. Decorate some functions in a tasks.py
file and you’re ready to go!
I like Invoke’s approach to little annoyances like options and external commands, too. I no longer need to care about argparse or subprocess for so many little projects.
Let’s get started.
Installing it
Mercifully simple. Do you have Homebrew? Use that.
You can also use pip
if you want Invoke in a particular Python environment.
Using Invoke with or near pyenv
No matter how I installed Invoke, I couldn’t get it to run in any environment at first. That’s more likely my mistake than any fault of pyenv or Invoke. Installing pyenv-which-ext fixed that problem. pyenv-which-ext looks for an executable in your regular path if it can’t be found in your pyenv stubs.
Thought I’d mention it in case you saw similar issues.
The tasks.py file
Tasks are defined in a tasks.py
file. Seems reasonable so far. Import the task decorator to use Invoke’s powers.
A “Hello World” task
There’s not much boilerplate to a task, but there’s still a little.
@task
is important. It tells Invoke to pay attention. Without the decoration, hello
is just another function. That’s great for adding support logic, but not so much for “Hello World.”
The c
argument is Invoke’s context. We’ll get to that. The thing to remember for now is that every task gets called with context as its first argument.
To run that task?
My Makefile uses a clever bit of sed to list available targets when you call make help
. With Invoke? No cleverness needed.
I strongly prefer Invoke’s built-in behavior to my sed
one-liner.
Documenting the task
This list could be more helpful, though. We know what the hello
task does because I wrote it a few minutes ago. What about in a few months, when I have dozens of tasks?
Trust me. In a few months I will have dozens of tasks. Maybe in a few hours.
Add a docstring!
Invoke takes the first line from that docstring and uses it to summarize our task in --list
.
Nice.
TIP
Since Invoke only uses the docstring’s first line for the summary, keep it short and to the point. Deep dives and technical explanations can go in following paragraphs. But you should be doing that anyways. It’s a good documentation habit.
Task parameters
Your task is a Python function. Add arguments to your function and you’ve added parameters to the task.
Invoke accepts both long and short form parameters. Either --name
or -n
count for name
. I’ll use the long form today to keep things clear.
If we ask Invoke about a specific task, it tells us about available parameters — along with the rest of the task function’s docstring.
We can document the parameters by handing a dictionary of names and summary strings to the decorator.
Optional parameters
Right now, the name
option is required. Invoke gets confused when we skip it.
Positional? Well yeah. You don’t need the name for a required parameter.
I prefer to be explicit about things. But we weren’t talking about positional parameters. Not on purpose anyways.
It’s reasonable to want a default parameter for your task. Do that by giving your function argument a default value.
Now we have a default name. Setting it as a variable makes it easier to identify and update later. We even noted the default in name
documentation, as a special favor to future us.
And hey — we can invoke the hello
task without a name!
It gets confusing again if we try a positional parameter though.
And that’s why I prefer explicit invocations. But if you must know:
- Parameters without defaults can be specified by position rather than name.
- Parameters with defaults must be specified by name.
Running multiple tasks
Thankfully Invoke supports another useful feature of Make: requesting more than one task at a time.
NOTE
Took me years to learn
make build && make test && make install
could be saidmake build test install
Let’s add a setup task.
We can ask Invoke to run both of them.
Pre-tasks
If we find ourselves running the same tasks in the same sequence all the time, we may be describing a dependency. Invoke lets us make the dependency explicit.
The decorator takes pre
as a list of task names. Invoke calls each of these pre-tasks in order — using their default options if any — before calling the specified task.
NOTE
Make sure your pre-tasks have been defined before listing them in
pre
!
Pre-tasks can be chained: if setup
had its own dependencies, they would be called before setup
. Invoke’s documentation on how tasks run explains task dependencies much better than I could.
Setting the default task
We have default parameters. What about default tasks?
Sure thing. Just let the decorator know.
Run invoke
without specifying a task, and it calls hello
using default parameters.
Thankfully Invoke mentions its default when listing tasks.
You can’t pass task arguments to the default task. Why? Well, once you add an argument you’re no longer asking for default behavior.
A useful task
Here’s what we have for our “Hello World” tasks.py
file.
tasks.py
It’s all nice and educational, but there isn’t anything useful yet. That’s the problem with “Hello World”. It can only give us the general idea.
I’ll wrap up today by starting a fresh tasks.py
for my Hugo site.
What am I doing most often while developing my site? At some point I need to preview the site, right? Need to make sure the layout and content looks how I expect.
That’s the answer I was looking for: running the Hugo server in drafts mode. That will be my first site task. Let’s make it the default while we’re at it.
Hugo’s built-in development server takes many options, but I care about these:
That way I can see the post I’m writing, and can preview from my phone if needed. Assuming my phone is on local wifi, which it usually is.
We’re finally using the context object! Invoke’s context holds details about the environment our tasks run in. Most important for today, they give us a way to run shell commands.
Technically a Runner subclass takes care of running the shell command. We don’t have to care about that though. We can pass a command string to c.run
and let it care.
Now I feel like I can post this.
What’s next?
How about the rest of the workflow? Let me get back to you on that. I need to reread my Makefile.
As for you, why not check out Invoke and set up a tasks.py
to drive your own workflow? It’s fun!
Backlinks
Added to vault 2024-01-15. Updated on 2024-02-01