KarrLab/wc_cli

View on GitHub
wc_cli/__main__.py

Summary

Maintainability
D
1 day
Test Coverage
A
98%
""" Whole-cell modeling command line interface

:Author: Jonathan Karr <jonrkarr@gmail.com>
:Date: 2018-05-15
:Copyright: 2018, Karr Lab
:License: MIT
"""

from .config.core import get_config
import cement
import copy
import importlib
import sys
import wc_cli


class BaseController(cement.Controller):
    """ Base controller for command line application """

    class Meta:
        label = 'base'
        description = "Whole-cell models and whole-cell modeling tools"
        help = "Whole-cell models and whole-cell modeling tools"
        arguments = [
            (['-v', '--version'], dict(action='version', version=wc_cli.__version__)),
        ]

    @cement.ex(hide=True)
    def _default(self):
        self._parser.print_help()


class ModelController(cement.Controller):
    """ Model controller """

    class Meta:
        label = 'model'
        description = 'Whole-cell models'
        help = 'Whole-cell models'
        stacked_on = 'base'
        stacked_type = 'nested'

    @cement.ex(hide=True)
    def _default(self):
        self._parser.print_help()


class ToolController(cement.Controller):
    """ Tool controller """

    class Meta:
        label = 'tool'
        description = 'Whole-cell modeling tools'
        help = 'Whole-cell modeling tools'
        stacked_on = 'base'
        stacked_type = 'nested'

    @cement.ex(hide=True)
    def _default(self):
        self._parser.print_help()


class App(cement.App):
    """ Command line application """
    class Meta:
        label = 'wc-cli'
        base_controller = 'base'
        handlers = []

    def __init__(self, argv=None, config=None):
        self.__class__.Meta.handlers = [
            BaseController,
            ModelController,
            ToolController,
        ]

        if not config:
            config = get_config()

        # tools
        for package_name, package_props in config['tool'].items():
            self.add_package_handlers(argv, 'tool', package_name, package_props['label'], package_props['description'])

        # models
        for package_name, package_props in config['model'].items():
            self.add_package_handlers(argv, 'model', package_name, package_props['label'], package_props['description'])

        # super class constructor
        super(App, self).__init__(argv=argv)

    @classmethod
    def add_package_handlers(cls, argv, stacked_on, package_name, label, description):
        """ Add a package to an application by adding its handlers

        Args:
            argv (:obj:`list` of :obj:`str`): command line arguments
            stacked_on (:obj:`str`): where the handlers for the package should be stacked
            package_name (:obj:`str`): name of the package whose handlers should be added the application
            label (:obj:`str`): root label for the handlers within the application
            description (:obj:`str`): root descipription for the handlers within the application

        Raises:
            :obj:`SystemExit`: if a handler is not a base or stacked on a base
        """
        if argv is None:
            argv = sys.argv[1:]

        # check if package needs to be imported
        if len(argv) < 1 or argv[0] != stacked_on:
            return
        if len(argv) >= 2 and argv[1][0] != '-' and argv[1] != label.replace('_', '-'):
            return

        # add handler(s) for package
        if importlib.util.find_spec(package_name):
            # add handlers for installed package
            module = importlib.import_module(package_name + '.__main__')

            for original_handler in module.App.Meta.handlers:
                attrs = dict(original_handler.__dict__)
                handler = type(original_handler.__class__.__name__, (original_handler, ), attrs)
                handler.Meta = type('Meta', (original_handler.Meta,), dict(original_handler.Meta.__dict__))

                if handler.Meta.label == 'base':
                    handler.Meta.label = label
                    handler.Meta.description = description
                    handler.Meta.help = description
                    handler.Meta.stacked_on = stacked_on
                    handler.Meta.stacked_type = 'nested'
                elif hasattr(handler.Meta, 'stacked_on'):
                    handler.Meta.aliases = [handler.Meta.label]
                    # handler.Meta.aliases_only = True # remove from cement 3
                    handler.Meta.label = label + '_' + handler.Meta.label.replace('-', '_')
                    if handler.Meta.stacked_on == 'base':
                        handler.Meta.stacked_on = label
                    else:
                        handler.Meta.stacked_on = label + '_' + handler.Meta.stacked_on.replace('-', '_')
                    handler.Meta.stacked_type = 'nested'
                else:
                    raise SystemExit('Invalid base handler: {}'.format(handler.Meta.label))  # pragma: no cover

                cls.Meta.handlers.append(handler)
        else:
            # add default handler for package that is not installed
            @cement.ex(hide=True)
            def _default(self):
                raise SystemExit(('{0} must be installed to use this command. '
                                 'Run `pip install {0}` to install the package'
                                 ).format(self.Meta.package_name))  # pragma: no cover # coverage doesn't capture this
            attrs = {
                'Meta': type('Meta', (), {
                    'package_name': package_name,
                    'label': label,
                    'description': '{} (Not installed)'.format(description),
                    'help': '{} (Not installed)'.format(description),
                    'stacked_on': stacked_on,
                    'stacked_type': 'nested',
                }),
                '_default': _default,
            }
            handler = type(package_name + '_Controller', (cement.Controller,), attrs)

            cls.Meta.handlers.append(handler)


def main():
    with App() as app:
        app.run()