Launcher Plugins¶
Kloch comes with a basic plugin system that allow you to add new launchers. The workflow can be simply described as:
create a new python module at an arbitrary location
subclass
BaseLauncher
andBaseLauncherSerialized
insidemake sure its parent location is registred in the PYTHONPATH so they can be imported
append the module name to the
launcher_plugins
configuration key
Which could be illustrated by the following bash commands:
cd /d/dev/my-kloch-plugins/
touch kloch_mylauncher.py
# edit its content ^
export PYTHONPATH=$PYTHONPATH:/d/dev/my-kloch-plugins/
export KLOCH_CONFIG_LAUNCHER_PLUGINS=kloch_mylauncher
# check if we succesfully registred our plugin
kloch plugins
As example you can check:
{root}/tests/data/plugins-behr
{root}/tests/data/plugins-tyfa
Creating the module¶
A plugin for kloch is a regular python module that will be imported and parsed to find specific object.
So you can create a regular python file at any location, or if you prefer
you can also ship your plugin as a python package by creating a directory
and an __init__.py
file.
Tip
As kloch plugins need to be in the PYTHONPATH, every python code will be
able to import them so make sure to pick a name that will not conflict
with other modules. Prefixing them with kloch_
is recommended.
Creating the subclasses¶
When creating the a new launcher you start by creating a dataclass
object
that declare what are the user-configurable options and how are they “launched”.
This is done by subclassing BaseLauncher
.
Next you need to declare how that dataclass must be serialized, by creating
a subclass of BaseLauncherSerialized
. This act as a high-level
controller for serialization.
For the granular control over how each field of the dataclass is serialized you
must created another dataclass subclass, but of BaseLauncherFields
.
Which will just miror the BaseLauncher
field structure, but where each of
its field provide more metadata to unserialize the field.
Here is an example which subclass both of those class, in which we create a launcher for “git cloning”:
import dataclasses
import http.client
import subprocess
from pathlib import Path
from typing import List
from typing import Optional
from kloch.launchers import BaseLauncher
from kloch.launchers import BaseLauncherSerialized
from kloch.launchers import BaseLauncherFields
@dataclasses.dataclass
class GitCloneLauncher(BaseLauncher):
name = "git-clone"
# all field must have a default value, but we make it
# required using the bellow `required_fields` class attribute
remote_url: str = ""
required_fields = ["remote_url"]
def execute(self, tmpdir: Path, command: Optional[List[str]] = None):
# we consider `command` are just extra args to git clone command
_command = ["git", "clone", self.remote_url] + command
subprocess.run(_command, env=self.environ, cwd=self.cwd)
@dataclasses.dataclass(frozen=True)
class GitCloneLauncherFields(BaseLauncherFields):
# field name must be the same as in the BaseLauncher subclass above
remote_url: str = dataclasses.field(
# this is the expected key name in the serialized representation
default="remote-url",
# this if for automated documentation generation
metadata={
"description": "An URL to a valid remote git repository.",
"required": True,
},
)
def does_url_exists(url: str) -> bool:
connection = http.client.HTTPConnection(url)
connection.request("HEAD", "")
return connection.getresponse().status < 400
class GitCloneLauncherSerialized(BaseLauncherSerialized):
# the class it serialize
source = GitCloneLauncher
# we can pick a different name but we keep it similar for simplicity
identifier = GitCloneLauncher.name
fields = GitCloneLauncherFields
# short one line description of the launcher
summary = "Just clone a repository from a git remote."
# full documentation of an arbitrary length for the launcher
description = "From a git remote repository url, git clone it in the current working directory."
def validate(self):
super().validate()
remote_url = self.fields.remote_url
assert remote_url in self, f"'{remote_url}': missing or empty attribute."
assert does_url_exists(
self[remote_url]
), f"'{remote_url}': url provided doesn't exists: {self[remote_url]}."
# we override for type-hint
def unserialize(self) -> GitCloneLauncher:
# noinspection PyTypeChecker
return super().unserialize()
Tip
When implementing a field for the user to have control on the launcher,
it’s best to implement it as dict
type over list
because it
allow users to override one item in particular using the token system.
Registering¶
Then add the kloch_gitclone.py
parent location in your PYTHONPATH
so the plugin system can do an import kloch_gitclone
.
Tip
PYTHONPATH is the standard mechanism by python to discover and import modules but nothing prevent you to use other tools or methods.
You could for example create a pyproject.toml
which declare kloch
and your plugin as dependency and let a tool like uv
or poetry create the venv.
As long as it can be import my_plugin_module_name
it will work !
The last step is to add the module name in the list of launcher plugins to use. You do this by modifying the kloch configuration, which is explained in Configuration
A quick way to do it is just to set the corresponding environment variable before starting kloch.
export KLOCH_CONFIG_LAUNCHER_PLUGINS=kloch_gitclone
You can check your plugin is registred by calling:
kloch plugins