Skip to main content
Metaist

Changed My Mind: Lifecycle Events and call-style Tasks

I still don't like 'em, so I'll make it opt-in.

This is another post about building the latest release of ds. There were two features that many task runners support that I specifically had chosen to not support: lifecycle events and call-style tasks. However, the latest release gave me a chance to try two different approaches to supporting these features.

Lifecycle Events #

Many task runners have the concept of pre- and post-task events. Like if you run pnpm publish it will also call pnpm prepublish before the task runs and pnpm postpublish afterwards.

The main reason I don't like this feature is that it obscures the relationship between tasks. In ds, you can be more explicit about the relationship:

[tool.ds.scripts]
publish = ['prepublish', 'publish-magic-happens-here', 'postpublish']

So in previous version of ds, I simply said you have to be more explicit. However, I've been watching how uv has been working on experimental features and how they support explicit opt-in for features that they don't support philosophically. For example, they added --system as a way of finding the system-level python which goes against their general philosophy of detecting and activating a virtual environment.

So I decided to add two experimental options --pre and --post to let you control whether those tasks run. pnpm has a very complex mechanism for selecting which pre-/post-tasks run, but I'll start simple: either you get them all or none.

I still don't do any language-specific lifecycle events (e.g., ds install doesn't call some magical list of other tasks).

call-style tasks #

Which brings me to another file & language-specific feature: call tasks. These usually let you load and invoke a function in an installed package. For example, in pdm you can write:

[tool.pdm.scripts]
foobar = {call = "foo_package.bar_module:main"}

Before I created file-specific parsers, there were two reasons I didn't support these kinds of tasks.

First, it obscures what actually gets called. That example above would be converted into:

python -c 'import sys; import foo_package.bar_module as _1; sys.exit(_1.main())

And second, how could I know ahead of time which language was being invoked? After all, composer.json has a similar style for invoking php methods.

Switching to file-specific parsers solved this problem. If you use pyproject.toml, you must want python and in composer.json it's php. And if you use a generic file like ds.toml, then I'll throw a SyntaxError because it's unclear what you're trying to do.