Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Custom parser prints required toil args by default but --help-toil prints full list #2058

Closed
jsmedmar opened this issue Feb 9, 2018 · 1 comment

Comments

@jsmedmar
Copy link
Contributor

jsmedmar commented Feb 9, 2018

This is not an issue. We just found this use case and would like to leave it here in case owners may want to include it as a feature.

Many of our pipelines users dont understand much about the execution process, therefore most toil arguments result daunting for them. We wrote a custom parser with a --help-toil option. By default, --help only prints the required toil arguments whilst using --help-toil prints the full list of toil rocketry 🚀

darwin$ find_species_origin --help

    usage: find_species_origin [-h] [-v] [--help-toil] [TOIL OPTIONAL ARGS] jobStore

    optional arguments:
    -h, --help            show this help message and exit
    --help-toil           print help with full list of Toil arguments and exit

    toil arguments:
    TOIL OPTIONAL ARGS    see --help-toil for a full list of toil parameters
    jobStore              the location of the job store for the workflow [REQUIRED]

Parser

This is the implementation (see the tests section for some unit testing):

# parsers.py
import argparse

from toil.job import Job

# just used for presentation
CUSTOM_TOIL_ACTIONS = [
    argparse.Action(
        ["TOIL OPTIONAL ARGS"],
        dest="",
        default=argparse.SUPPRESS,
        help="see --help-toil for a full list of toil parameters",
    ),
    argparse.Action(
        [],
        dest="jobStore",
        help="the location of the job store for the workflow. "
        "See --help-toil for more information [REQUIRED]"
    )
]


class _ToilHelpAction(argparse._HelpAction):

    def __call__(self, parser, namespace, values, option_string=None):
        """Print parser help and exist whilst adding a flag to the parser."""
        parser.show_toil_groups = True
        parser.print_help()
        parser.exit()


class ShortToilArgumentParser(argparse.ArgumentParser):

    """
    A parser with only required toil args in --help with --help-toil option.

    Toil options are automatically added, but hidden by default in the help
    print. However, the `--help-toil` argument prints toil's full rocketry.
    """

    def __init__(self, **kwargs):
        """Add Toil options and `--help-toil` to parser."""
        # set ArgumentDefaultsHelpFormatter as the default formatter
        kwargs["formatter_class"] = kwargs.get(
            "formatter_class", argparse.ArgumentDefaultsHelpFormatter
        )

        super(ShortToilArgumentParser, self).__init__(**kwargs)

        self.add_argument(
            "--help-toil",
            action=_ToilHelpAction, default=argparse.SUPPRESS,
            help="print help with full list of Toil arguments and exit"
        )

        # add toil options
        Job.Runner.addToilOptions(self)

    def get_help_groups(self, show_toil_groups):
        """Decide whether to show toil options or not."""
        action_groups = []
        actions = []

        for action_group in self._action_groups:
            is_toil_group = action_group.title.startswith("toil")
            is_toil_group |= "Logging Options" in action_group.title

            if not is_toil_group or (is_toil_group and show_toil_groups):
                action_groups.append(action_group)
                actions += action_group._group_actions

        return actions, action_groups

    def format_help(self):
        """Include toil options if `self.show_toil_groups` is True."""
        formatter = self._get_formatter()

        # decide whether to show toil options or not
        show_toil_groups = getattr(self, "show_toil_groups", False)
        actions, action_groups = self.get_help_groups(show_toil_groups)

        # usage, the CUSTOM_TOIL_ACTIONS are just for display
        formatter.add_usage(
            self.usage,
            actions + ([] if show_toil_groups else CUSTOM_TOIL_ACTIONS),
            self._mutually_exclusive_groups
        )

        # description
        formatter.add_text(self.description)

        # positionals, optionals and user-defined groups
        for action_group in action_groups:
            formatter.start_section(action_group.title)
            formatter.add_text(action_group.description)
            formatter.add_arguments(action_group._group_actions)
            formatter.end_section()

        # add custom toil section
        if not show_toil_groups:
            formatter.start_section("toil arguments")
            formatter.add_arguments(CUSTOM_TOIL_ACTIONS)
            formatter.end_section()

        # epilog
        formatter.add_text(self.epilog)

        # determine help from format above
        return formatter.format_help()

✅ Tests

from cStringIO import StringIO
import sys


class Capturing(list):

    """
    Capture stdout of a function call.

    See: https://stackoverflow.com/questions/16571150

    Example:

        with Capturing() as output:
            do_something(my_object)
    """

    def __enter__(self):
        self._stdout = sys.stdout
        sys.stdout = self._stringio = StringIO()
        return self

    def __exit__(self, *args):
        self.extend(self._stringio.getvalue().splitlines())
        del self._stringio    # free up some memory
        sys.stdout = self._stdout


def test_help_toil():
    parser = parsers.ShortToilArgumentParser()
    with Capturing() as without_toil:
        try:
            parser.parse_args(["--help"])
        except SystemExit:
            pass

    with Capturing() as with_toil:
        try:
            parser.parse_args(["--help-toil"])
        except SystemExit:
            pass

    with_toil = "\n".join(with_toil)
    without_toil = "\n".join(without_toil)

    # By default toil options shouldn't be printed.
    assert "toil core options" not in without_toil
    assert "TOIL OPTIONAL ARGS" in without_toil
    assert "toil arguments" in without_toil

    # Check that toil options were added by default.
    assert "toil core options" in with_toil

┆Issue is synchronized with this JIRA Story
┆Issue Number: TOIL-57

@jsmedmar jsmedmar changed the title Custom parser prints only required toil args by default but --help-toil prints full list [Feature Suggestion] Custom parser prints only required toil args by default but --help-toil prints full list Feb 9, 2018
@jsmedmar jsmedmar changed the title [Feature Suggestion] Custom parser prints only required toil args by default but --help-toil prints full list [FEATURE] Custom parser prints only required toil args by default but --help-toil prints full list Feb 9, 2018
@jsmedmar jsmedmar changed the title [FEATURE] Custom parser prints only required toil args by default but --help-toil prints full list [FEATURE] Custom parser prints required toil args by default but --help-toil prints full list Feb 9, 2018
@ejacox
Copy link
Contributor

ejacox commented Feb 12, 2018

This is great! Thank you @jsmedmar

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants