Plugins
This page describes how to use and write plugins for checker pipelines.
You can refer to the course-template repository for examples of plugins usage and custom plugins development.
What is the Plugin
tasks_pipeline:
- name: "Check forbidden regexps"
fail: fast # fast, after_all, never
run: "check_regexps"
args:
origin: "${{ global.temp_dir }}/${{ task.task_sub_path }}"
patterns: ["**/*.py"]
regexps: ["exit(0)"]
- name: "Run linter"
run_if: ${{ parameters.run_linting }}
fail: after_all # fast, after_all, never
run: "run_script"
args:
origin: ${{ global.temp_dir }}
script: "python -m ruff --config=pyproject.toml ${{ task.task_sub_path }}"
Plugin is a single stage of the pipeline, have arguments, return exclusion result.
In a nutshell, it is a Python class overriding abstract class checker.plugins.PluginABC
:
Bases:
ABC
Abstract base class for plugins. :ivar name: str plugin name, searchable by this name
Source code in
checker/plugins/base.py
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74class PluginABC(ABC): """Abstract base class for plugins. :ivar name: str plugin name, searchable by this name """ name: str class Args(BaseModel): """Base class for plugin arguments. You have to subclass this class in your plugin. """ pass def run(self, args: dict[str, Any], *, verbose: bool = False) -> PluginOutput: """Run the plugin. :param args: dict plugin arguments to pass to subclass Args :param verbose: if True should print teachers debug info, if False student mode :raises BadConfig: if plugin arguments are invalid :raises ExecutionFailedError: if plugin failed :return: PluginOutput with stdout/stderr and percentage """ args_obj = self.Args(**args) return self._run(args_obj, verbose=verbose) @classmethod def validate(cls, args: dict[str, Any]) -> None: """Validate the plugin arguments. :param args: dict plugin arguments to pass to subclass Args :raises BadConfig: if plugin arguments are invalid :raises BadStructure: if _run method is not implemented """ try: cls.Args(**args) except ValidationError as e: raise BadConfig(f"Plugin {cls.name} arguments validation error:\n{e}") if not hasattr(cls, "_run"): raise BadStructure(f"Plugin {cls.name} does not implement _run method") @abstractmethod def _run(self, args: Args, *, verbose: bool = False) -> PluginOutput: """Actual run the plugin. You have to implement this method in your plugin. In case of failure, raise ExecutionFailedError with an error message and output. :param args: plugin arguments, see Args subclass :param verbose: if True should print teachers debug info, if False student mode :return: PluginOutput with stdout/stderr and percentage :raises ExecutionFailedError: if plugin failed """ pass
Args
Bases:
BaseModel
Base class for plugin arguments. You have to subclass this class in your plugin.
Source code in
checker/plugins/base.py
30 31 32 33 34 35class Args(BaseModel): """Base class for plugin arguments. You have to subclass this class in your plugin. """ pass
run(args, *, verbose=False)
Run the plugin. :param args: dict plugin arguments to pass to subclass Args :param verbose: if True should print teachers debug info, if False student mode :raises BadConfig: if plugin arguments are invalid :raises ExecutionFailedError: if plugin failed :return: PluginOutput with stdout/stderr and percentage
Source code in
checker/plugins/base.py
37 38 39 40 41 42 43 44 45 46 47def run(self, args: dict[str, Any], *, verbose: bool = False) -> PluginOutput: """Run the plugin. :param args: dict plugin arguments to pass to subclass Args :param verbose: if True should print teachers debug info, if False student mode :raises BadConfig: if plugin arguments are invalid :raises ExecutionFailedError: if plugin failed :return: PluginOutput with stdout/stderr and percentage """ args_obj = self.Args(**args) return self._run(args_obj, verbose=verbose)
validate(args)
classmethod
Validate the plugin arguments. :param args: dict plugin arguments to pass to subclass Args :raises BadConfig: if plugin arguments are invalid :raises BadStructure: if _run method is not implemented
Source code in
checker/plugins/base.py
49 50 51 52 53 54 55 56 57 58 59 60 61 62@classmethod def validate(cls, args: dict[str, Any]) -> None: """Validate the plugin arguments. :param args: dict plugin arguments to pass to subclass Args :raises BadConfig: if plugin arguments are invalid :raises BadStructure: if _run method is not implemented """ try: cls.Args(**args) except ValidationError as e: raise BadConfig(f"Plugin {cls.name} arguments validation error:\n{e}") if not hasattr(cls, "_run"): raise BadStructure(f"Plugin {cls.name} does not implement _run method")
Note that each plugin should override checker.plugins.PluginABC.Args
class to provide arguments validation. Otherwise, empty arguments will be passed to run
method.
Each plugin output checker.plugins.PluginOutput
class when executed successfully.
Plugin output dataclass. :ivar output: str plugin output :ivar percentage: float plugin percentage
Source code in
checker/plugins/base.py
12 13 14 15 16 17 18 19 20@dataclass class PluginOutput: """Plugin output dataclass. :ivar output: str plugin output :ivar percentage: float plugin percentage """ output: str percentage: float = 1.0
In case of error, checker.exceptions.PluginExecutionFailed
have to be raised.
Note
Base Plugin class will handle all ValidationErrors of Args and raise error by itself.
So try to move all arguments validation to Args
class in pydantic
way.
How to use plugins
Plugins are used in the pipelines described in .checker.yml
file. When running a pipeline the checker will validate plugin arguments and run it.
The following plugins are available out of the box, here is the list with their arguments:
-
run_script
- execute any script with given argumentsBases:
Args
Source code in
checker/plugins/scripts.py
14 15 16 17 18
class Args(PluginABC.Args): origin: str script: Union[str, list[str]] # as pydantic does not support | in older python versions timeout: Union[float, None] = None # as pydantic does not support | in older python versions env_whitelist: Optional[list[str]] = None
-
safe_run_script
- execute script withing firejail sandboxBases:
Args
Source code in
checker/plugins/firejail.py
24 25 26 27 28 29 30 31 32
class Args(PluginABC.Args): origin: str script: Union[str, list[str]] # as pydantic does not support | in older python versions timeout: Union[float, None] = None # as pydantic does not support | in older python versions env_whitelist: list[str] = list() paths_whitelist: list[str] = list() lock_network: bool = True allow_fallback: bool = False
-
check_regexps
- error if given regexps are found in the filesBases:
Args
Source code in
checker/plugins/regex.py
12 13 14 15
class Args(PluginABC.Args): origin: str patterns: list[str] regexps: list[str]
-
aggregate
- aggregate results of other plugins (e.g. sum/mean/mul scores)Bases:
Args
Source code in
checker/plugins/aggregate.py
14 15 16 17
class Args(PluginABC.Args): scores: list[float] weights: Union[list[float], None] = None # as pydantic does not support | in older python versions strategy: Literal["mean", "sum", "min", "max", "product"] = "mean"
-
report_score_manytask
- report score to manytaskBases:
Args
Source code in
checker/plugins/manytask.py
26 27 28 29 30 31 32 33 34 35
class Args(PluginABC.Args): origin: Optional[str] = None # as pydantic does not support | in older python versions patterns: list[str] = ["*"] username: str task_name: str score: float # TODO: validate score is in [0, 1] (bonus score is higher than 1) report_url: AnyUrl report_token: str check_deadline: bool send_time: datetime = datetime.now().astimezone()
-
check_gitlab_merge_request
- [WIP] check gitlab MR is valid (no conflicts, no extra files, has label etc.)Bases:
Args
Source code in
checker/plugins/gitlab.py
13 14 15 16 17 18
class Args(PluginABC.Args): token: str task_dir: str repo_url: AnyUrl requre_approval: bool = False search_for_score: bool = False
-
collect_score_gitlab_merge_request
- [WIP] search for score by tutor in gitlab MR commentBases:
Args
Source code in
checker/plugins/gitlab.py
34 35 36 37 38 39
class Args(PluginABC.Args): token: str task_dir: str repo_url: AnyUrl requre_approval: bool = False search_for_score: bool = False
How to write a custom plugin
To write a custom plugin you need to create a class inheriting from checker.plugins.PluginABC
and override _run
method, Args
inner class and set name
class attribute.
from random import randint
from checker.plugins import PluginABC, PluginOutput
from checker.exceptions import PluginExecutionFailed
from pydantic import AnyUrl
class PrintUrlPlugin(PluginABC):
"""Plugin to print url"""
name = "print_url"
class Args(PluginABC.Args):
url: AnyUrl
def _run(self, args: Args, *, verbose: bool = False) -> PluginOutput:
if randint(0, 1):
if verbose:
raise PluginExecutionFailed("Verbose error, we got randint=1")
else:
raise PluginExecutionFailed("Random error")
return PluginOutput(
output=f"Url is {args.url}",
percentage=1.0, # optional, default 1.0 on success
)
Important
The Plugin must implement verbose
functionality!
If verbose
is True
the plugin should provide all info and possible debug info.
If verbose
is False
the plugin should provide only public-friendly info, e.g. excluding private test output.
Note
It is a nice practice to write a small tests for your custom plugins to be sure that it works as expected.