Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sagesmc
Path: blob/master/src/bin/sage-dev
8814 views
#!/usr/bin/env python

import argparse
import inspect
import sys
from textwrap import dedent

DISABLED_MESSAGE = """
Developer interface disabled.

To develop for Sage you must compile Sage from its git
repository. This just amounts to running

    git clone git://github.com/sagemath/sage.git
    cd sage
    make

See http://trac.sagemath.org/wiki/QuickStartSageGit for slightly more
information.
"""

try:
    from sage.all import dev as DEV
except ImportError:
    print(DISABLED_MESSAGE.strip())
    sys.exit(0)


class SageHelpFormatter(argparse.HelpFormatter):
    """
    Special formatter class that is better at reading
    terminal width, and doesn't print off all possible
    subcommands in curly brackets (since it gets ugly)
    """
    def __init__(self, *args, **kwds):
        super(SageHelpFormatter, self).__init__(
                width=DEV._sagedev._UI._get_dimensions()[1]-2, *args, **kwds)

    def _format_args(self, action, default_metavar):
        get_metavar = self._metavar_formatter(action, default_metavar)
        if action.nargs is None:
            result = '%s' % get_metavar(1)
        elif action.nargs == argparse.OPTIONAL:
            result = '[%s]' % get_metavar(1)
        elif action.nargs == argparse.ZERO_OR_MORE:
            result = '[%s [%s ...]]' % get_metavar(2)
        elif action.nargs == argparse.ONE_OR_MORE:
            result = '%s [%s ...]' % get_metavar(2)
        elif action.nargs == argparse.REMAINDER:
            result = '...'
        elif action.nargs == argparse.PARSER:
            result = 'subcommand ...'
            #result = '%s ...' % get_metavar(1)
        else:
            formats = ['%s' for _ in range(action.nargs)]
            result = ' '.join(formats) % get_metavar(action.nargs)
        return result

    def _format_action(self, action):
        # determine the required width and the entry label
        help_position = min(self._action_max_length + 2,
                            self._max_help_position)
        help_width = self._width - help_position
        action_width = help_position - self._current_indent - 2
        action_header = self._format_action_invocation(action)

        # ho nelp; start on same line and add a final newline
        if not action.help:
            tup = self._current_indent, '', action_header
            action_header = '%*s%s\n' % tup

        # short action name; start on the same line and pad two spaces
        elif len(action_header) <= action_width:
            tup = self._current_indent, '', action_width, action_header
            action_header = '%*s%-*s  ' % tup
            indent_first = 0

        # long action name; start on the next line
        else:
            tup = self._current_indent, '', action_header
            action_header = '%*s%s\n' % tup
            indent_first = help_position

        # collect the pieces of the action help
        parts = []
        if action.nargs != argparse.PARSER:
            parts.append(action_header)
        #parts = [action_header]

        # if there was help for the action, add lines of help text
        if action.help:
            help_text = self._expand_help(action)
            help_lines = self._split_lines(help_text, help_width)
            parts.append('%*s%s\n' % (indent_first, '', help_lines[0]))
            for line in help_lines[1:]:
                parts.append('%*s%s\n' % (help_position, '', line))

        # or add a newline if the description doesn't end with one
        elif not action_header.endswith('\n'):
            parts.append('\n')

        # if there are any sub-actions, add their help as well
        for subaction in self._iter_indented_subactions(action):
            import sage.dev.sagedev_wrapper
            if subaction.dest.replace("-","_") in sage.dev.sagedev_wrapper.obsolete_commands:
                continue
            parts.append(self._format_action(subaction))

        # return a single string
        return self._join_parts(parts)

def get_help(method):
    """
    gets the short help for a method

    Returns a dictionary whose keys are the arguments
    of method, and whose values are the help strings.

    EXAMPLE::

        >>> help_dict = get_help(get_help)
        >>> for k in sorted(help_dict.keys()):  print k, '---', help_dict[k]
        method --- method to get help (test: "quote"). can continue input description with a line break
        return --- gets the short help for a method

    INPUT:

    - ``method`` -- method to get help (test: ``quote``). 
      can continue input description with a line break
    """
    if method.__doc__ is None:
        return None

    docstring = method.__doc__.strip('\n')
    docstring = (line.rstrip() for line in dedent(docstring).splitlines())

    tmp = []
    for line in docstring:
        if line:
            tmp.append(line)
        else:
            break
    ret = {'return':' '.join(tmp).replace('``', '"')}

    for line in docstring:
        if line == 'INPUT:':
            break
    else:
        return ret

    tmp = []
    for line in docstring:
        if not line:
            if tmp:
                ret[arg] = ' '.join(tmp)
                tmp = []
            continue
        elif line.startswith('-') and '--' in line:
            arg = line.split('``')[1]
            tmp.append(line.split('--')[1].strip().replace('``', '"'))
        elif tmp:
            tmp.append(line.strip().replace('``', '"'))
        elif not line.startswith(' '):
            break
    if tmp:
        ret[arg] = ' '.join(tmp)
    return ret

def exposed_methods(obj):
    """
    Lists the callable, public attributes of ``obj``.
    """
    for name in dir(obj):
        if name.startswith('_'):
            continue
        method = getattr(obj, name)
        if hasattr(method, 'func_name'):
            yield name.replace('_','-'), method

def getargspec(func):
    """
    Returns the argspec of func, omitting self if func is a bound method.
    """
    from sage.misc.sageinspect import sage_getargspec
    argspec = sage_getargspec(func)
    if argspec.args and argspec.args[0]=='self':
        del argspec.args[0]
    return argspec

def parser_from_method(parser, func, help_dict):
    """
    Constructs an argument parser based on the signature of func.
    """
    argspec = getargspec(func)
    default_start = len(argspec.args) - len(argspec.defaults or [])
    for ix, arg in enumerate(argspec.args):
        extra = {}
        if ix < default_start:
            name = [arg]
            extra['type'] = str_to_value
        else:
            name = ['--'+n for n in arg.replace('_','-').split('-or-')]
            extra['dest'] = arg

            default_value = argspec.defaults[ix - default_start]
            if default_value is False:
                extra['action'] = 'store_true'
            else:
                extra['default'] = default_value

                if isinstance(default_value, int) :
                    extra['type'] = int
                elif isinstance(default_value, bool):
                    extra['type'] = bool
                else:
                    extra['type'] = str_to_value

        if arg in help_dict:
            extra['help'] = help_dict[arg]

        parser.add_argument(*name, **extra)
    if argspec.varargs is not None:
        extra = {'nargs': '*',
                 'type': str_to_value}
        if argspec.varargs in help_dict:
            extra['help'] = help_dict[argspec.varargs]
        parser.add_argument(argspec.varargs, **extra)
    if argspec.keywords is not None:
        extra = {'nargs':argparse.REMAINDER}
        if argspec.keywords in help_dict:
            extra['help'] = help_dict[argspec.keywords]
        parser.add_argument(argspec.keywords, **extra)

def str_to_value(s):
    """
    Returns s as none, a boolean, or an int, if possible.  Otherwise,
    returns s itself.
    """
    if not isinstance(s, str):
        return s

    slower = s.lower()
    if slower in ('true', 'false'):
        return slower == 'true'
    if slower == 'none':
        return None

    try:
        return int(s)
    except ValueError:
        return s

def parser_from_object(obj, *args, **kwds):
    kwds['formatter_class'] = SageHelpFormatter
    if 'description' not in kwds:
        kwds['description'] = get_help(obj._sagedev)['return']

    parser = argparse.ArgumentParser(*args, **kwds)
    subparsers = parser.add_subparsers(title='subcommands')

    def help(args=None, namespace=None):
        if args is None:
            args = sys.argv[2:]
        else:
            args = list(args)[1:]
        args.append('--help')
        parser.parse_args(args=args, namespace=namespace)

    helped = False

    for name, method in exposed_methods(obj):
        if name > 'help' and not helped:
            subparser = subparsers.add_parser('help',
                    help='show help message and exit',
                    formatter_class=SageHelpFormatter)
            subparser.set_defaults(method=help)
            subparser.add_argument('subcommand',
                    help='show help message for given command',
                    nargs=argparse.REMAINDER)
            helped = True

        help_dict = get_help(method)
        if help_dict is None:
            continue
        subparser = subparsers.add_parser(name,
                help=help_dict['return'], description=help_dict['return'],
                formatter_class=SageHelpFormatter)
        subparser.set_defaults(method=method)
        parser_from_method(subparser, method, help_dict)

    def run(args=None, namespace=None):
        parsed = vars(parser.parse_args(args=args, namespace=namespace))
        method = parsed['method']
        del parsed['method']
        if method == help:
            # help subcommand
            help(args=args, namespace=namespace)

        argspec = getargspec(method)
        args = []
        for arg in argspec.args:
            args.append(parsed[arg])
            del parsed[arg]
        if argspec.varargs:
            args.extend(str_to_value(parsed[argspec.varargs]))
            del parsed[argspec.varargs]

        if argspec.keywords:
            kwds = map(lambda k: k.split('=')[0],
                       filter(lambda k: k.startswith('--'),
                              parsed[argspec.keywords]))
            kwdparser = argparse.ArgumentParser(formatter_class=SageHelpFormatter)
            for kwdarg in kwds:
                kwdparser.add_argument(kwdarg, type=str_to_value)
            parsed.update(vars(kwdparser.parse_args(kwds)))
            del parsed[argspec.keywords]

        kwds = {key: str_to_value(value) for key, value in parsed.items()}

        method(*args,**kwds)
    parser.run = run

    return parser

if __name__ == '__main__':
    if "--profile" in sys.argv:
        sys.argv.remove("--profile")
        import cProfile
        profiler = cProfile.Profile()
        profiler.enable()
    else:
        profiler = None
    try:
        parser = parser_from_object(DEV)
        parser.run()
    finally:
        if profiler is not None:
            profiler.disable()
            profiler.print_stats(sort='cumulative')