Writing a plugin
An essential part of a plugin are the tasks that come with it. The tasks are the blocks of build logic that a plugin can use to act during the build process. We shall start with simple task declarations and then move on to the additions necessary to a plugin.
A simple task
Begin by creating a new folder with a
build.py descriptor in it.
build.py with a simple function called
task decorator from
pybuilder.core to declare the
hello function to a PyBuilder task.
Our simple task might look like so:
We can now list the available tasks using the
As you can see PyBuilder has picked up our task using the function name. Note that task names should be unique! We can control the tasks’ name by adding it as an argument to the decorator. Let us rename the task into “foo” without changing the function name.
When we list the tasks again, we can see that the task is renamed to “foo” now:
As you can see above, our task has no description currently. We can change that by adding one through the description keyword argument :
Listing tasks will now yield a description of our task :
Dependencies between tasks is easy. You can use the
depends decorator to express that a task requires another one to run.
In the following example we create a task foo, which depends on the successful execution of task bar.
Now every time the task foo is run, PyBuilder will ensure that bar will have run first. Dependencies on more than one task are expressed by increasing the arity of the depends decorator like so :
If a task wishes to show output to the user, it can request the use of the PyBuilder logger by accepting a
logger argument :
Most decisions a task can take (e.G. configuration) require the knowledge of the project. A task can be made aware of the project by accepting a
project argument. Since those are named arguments, nothing is stopping us from accepting both a logger and a project (the order does not matter) :
Anatomy of a plugin
A plugin can be broken down in essentially two parts :
- Initialization of the plugin
- Tasks the plugin provides
The initialization of a plugin involves setting default configuration. It is done from within an initializer
The initializer can use the project object to set property values.
This is suitable for defaults because the initializer of the plugin will run before any user-provided initializers in
build.py. Thus you can set a property (the default) which can then be overridden by the user.
Setting properties is done through the
set_property method of project :
We can then use this property in our tasks through the project object :
Properties can be of any type (usually strings, integers, booleans or lists).
If a property is mandatory and there is no default, then
project.get_mandatory_property is more suitable since it also raises an error in case the property is unset. See the project API for even more possibilities!
Requiring external libraries
If the plugin requires external libraries installable through pip, the project object can be used to add this as a dependency :
Ensuring that programs are installed
If the plugin relies on an external program like pylint then you should ensure that the program is available through the
assert_can_execute function. The aim is to provide quick feedback to users why the plugin is not working.
Compliance with the verbose flag
PyBuilder is designed to focus on the big picture of the build process.
There is a
verbose flag though. When it is present, your plugin should
provide information about errors using the logger.
For most plugins using the helper function
pybuilder.plugins.python.python_plugin_helper will suffice. It will display the output of the tool, provided the property
$name_verbose_output is set to true. The
$name is simply the name argument passed to the function. You can set this property in your task based on the verbose property of the project :
The tasks section of a plugin is the declaration of one or possibly more tasks. When a plugin is activated, its tasks are made available. There are three ways to get a task to run :
- direct invocation
- indirect invocation through a dependency
- indirect invocation through naming
This is the most common approach. The user will use
pyb my_task which will run the task.
Indirect invocation through a dependency
When there is a dependency chain between tasks, for example if foo requires bar to run first.
If the user executes
pyb foo this will run bar and foo in that order.
Indirect invocation through naming
As we saw before, tasks are identified by their name. When there are multiple tasks with the same name, then all of those tasks will run.
For instance the core plugin defines many generic tasks like
package that are actually empty.
Plugins that want to run during verification can then simply use
verify as a task name.
See the plugins that ship with PyBuilder. A very nice example is the flake8 plugin.