Developer guide

The program was implemented knowing that not every situation can be solved with a one-size-fits-all solution. Thus, it was built to be as pluggable as possible, providing developers with the option to extend the basic functionality depending on their specific needs by creating custom plugins. The main functionality of the program is contained within the Default plugin.

Plugin options

Each plugin can define custom command-line arguments. It is recommended for these to be in their specific argument group. The program also defines arguments outside of its default plugin. Program arguments mainly define debug options and paths needed for plugin discovery.

Data setup

Plugins are also able to define DataSetup class. Methods in this can class initialise macros file initial wrapper XML. Example of data setup class:

class DefaultDataSetup(DataSetup):
"""
Defines data setup of the default plugin. During data setup,
macros are created and default xml is initialized
"""
def __init__(self, args: Any, assets: str):
    super().__init__(args)
    self.assets_path = assets

def initialize_xml_tree(self, xml_tree: ET.ElementTree) -> ET.ElementTree:
    return ET.parse(os.path.join(self.assets_path, "template.xml"))

def initialize_macros(self, macros_factory: MacrosFactory) -> MacrosFactory:
    macros_factory.add_token("tool_version",
                                       self.args.tool_version)
    for name, version in parse_argument_comma_sep_list(self.args.requirements):
        macros_factory.add_requirement(name, version)

    return macros_factory

Strategies

The last and the most important part of the plugin are the plugin strategies. Strategies define wrapper creation steps performed in the order specified by their stage and order parameters. The creation of the wrapper file is divided into eight consecutive stages; HEADER defines the overall structure of the wrapper and possibly imports macros, PARAMS, OUTPUTS, COMMAND, TESTS, HELP, CITATIONS and POST_PROCESSING adds elements that don’t fit into any of the previous actions or require data created in the previous stages. Plugins can define multiple strategies for each stage, and the order of their execution depends on their order parameters.

Example strategy:

class DefaultOutputs(Strategy):
"""
This is a very basic Output strategy, that generates outputs for
stdout and stderr redirects
"""
def __init__(self, args: Any, macros: Macros):
    super().__init__(args, macros, StrategyStage.OUTPUTS)

def apply_strategy(self, xml_output: ET.ElementTree) -> Any:
    if self.args.dont_redirect_output:
        return xml_output
    outputs = xml_output.find(".//outputs")
    create_element(outputs, "data", {"name": "stdout",
                                       "label": "STD out output",
                                       "format": "txt"})
    create_element(outputs, "data", {"name": "stderr",
                                     "label": "STD err output",
                                     "format": "txt"})

    return xml_output

Plugin configuration and discovery

If the user provides a path to the directory containing custom plugins, they will be loaded by the program and used during the wrapper generation process. The path is set by the command-line argument ‘–plugins path’. The plugin discovery python module traverses this directory, looking for YAML files containing the plugin configuration metadata. This configuration file contains the plugin’s name, a list of requirements, a path from the configuration file to the python file containing the Plugin class, execution order and a path to the plugin’s assets.

Example configuration file:

plugin:
    name: Default Plugin
    # order of plugin is by default 0, this definition is here is just for clarity
    order: 0
    requirements:
        - lxml
    path: default.py
    assets: assets