# Source code for metakernel.magic

    import traceback
    import optparse
    import inspect
    import sys
    import os
    import shlex
    from ast import literal_eval as safe_eval

    try:
        _maxsize = sys.maxint
    except:
        # python3
        _maxsize = sys.maxsize

    PY3 = sys.version_info[0] == 3

    class MagicOptionParser(optparse.OptionParser):
        def error(self, msg):
            raise Exception('Magic Parse error: "%s"' % msg)

        def exit(self, status=0, msg=None):
            if msg:
                sys.stderr.write(msg)
            raise Exception(msg)

        ## FIXME: override help to also stop processing
        ## currently --help gives syntax error

    class Magic(object):
        """
        Base class to define magics for MetaKernel based kernels.

        Users can redefine the default magics provided by Metakernel
        by creating a module with the exact same name as the
        Metakernel magic.

        For example, you can override %matplotlib in your kernel by
        writing a new magic inside magics/matplotlib_magic.py
        """

        def __init__(self, kernel):
            self.kernel = kernel
            self.evaluate = True
            self.code = ''

        def get_args(self, mtype, name, code, args) :
            self.code = code
            old_args = args
            mtype = mtype.replace('sticky', 'cell')

            func = getattr(self, mtype + '_' + name)
            try:
                args, kwargs = _parse_args(func, args, usage=self.get_help(mtype, name))
            except Exception as e:
                self.kernel.Error(str(e))
                return self
            arg_spec = inspect.getfullargspec(func) if PY3 \
                       else inspect.getargspec(func)
            fargs = arg_spec.args
            if fargs[0] == 'self':
                fargs = fargs[1:]

            fargs = [f for f in fargs if not f in kwargs.keys()]
            if len(args) > len(fargs) and not arg_spec.varargs:
                extra = ' '.join(str(s) for s in (args[len(fargs) - 1:]))
                args = args[:len(fargs) - 1] + [extra]

            return (args, kwargs, old_args)

        def call_magic(self, mtype, name, code, args):
            self.code = code
            old_args = args
            mtype = mtype.replace('sticky', 'cell')

            func = getattr(self, mtype + '_' + name)
            try:
                args, kwargs = _parse_args(func, args, usage=self.get_help(mtype, name))
            except Exception as e:
                self.kernel.Error(str(e))
                return self

            arg_spec = inspect.getfullargspec(func) if PY3 \
                       else inspect.getargspec(func)
            fargs = arg_spec.args
            if fargs[0] == 'self':
                fargs = fargs[1:]

            fargs = [f for f in fargs if not f in kwargs.keys()]
            if len(args) > len(fargs) and not arg_spec.varargs:
                extra = ' '.join(str(s) for s in (args[len(fargs) - 1:]))
                args = args[:len(fargs) - 1] + [extra]

            try:
                try:
                    func(*args, **kwargs)
                except TypeError:
                    func(old_args)
            except Exception as exc:
                msg = "Error in calling magic '%s' on %s:\n    %s\n    args: %s\n    kwargs: %s" % (
                    name, mtype, str(exc), args, kwargs)
                self.kernel.Error(msg)
                self.kernel.Error(traceback.format_exc())
                self.kernel.Error(self.get_help(mtype, name))
                # return dummy magic to end processing:
                return Magic(self.kernel)
            return self

        def get_help(self, mtype, name, level=0):
            if hasattr(self, mtype + '_' + name):
                func = getattr(self, mtype + '_' + name)
                if level == 0:
                    if func.__doc__:
                        return _trim(func.__doc__)
                    else:
                        return "No help available for magic '%s' for %ss." % (name, mtype)
                else:
                    filename = inspect.getfile(func)
                    if filename and os.path.exists(filename):
                        with open(filename) as f: return f.read()
                    else:
                        return "No help available for magic '%s' for %ss." % (name, mtype)
            else:
                return "No such magic '%s' for %ss." % (name, mtype)

        def get_help_on(self, info, level=0):
            return "Sorry, no help is available on '%s'." % info['code']

        def get_completions(self, info):
                """
                Get completions based on info dict from magic.
                """
                return []

        def get_magics(self, mtype):
            magics = []
            for name in dir(self):
                if name.startswith(mtype + '_'):
                    magics.append(name.replace(mtype + '_', ''))
            return magics

        def get_code(self):
            return self.code

        def post_process(self, retval):
            return retval

    def option(*args, **kwargs):
        """Return decorator that adds a magic option to a function.
        """
        def decorator(func):
            help_text = ""
            if not getattr(func, 'has_options', False):
                func.has_options = True
                func.options = []
                help_text += 'Options:\n-------\n'
            try:
                option = optparse.Option(*args, **kwargs)
            except optparse.OptionError:
                help_text += args[0] + "\n"
            else:
                help_text += _format_option(option) + "\n"
                func.options.append(option)
            if func.__doc__:
                func.__doc__ += _indent(func.__doc__, help_text)
            else:
                func.__doc__ = help_text
            return func
        return decorator

    def _parse_args(func, args, usage=None):
        """Parse the arguments given to a magic function"""
        if isinstance(args, list):
            args = ' '.join(args)

        args = _split_args(args)

        kwargs = dict()
        if getattr(func, 'has_options', False):
            parser = MagicOptionParser(usage=usage, conflict_handler="resolve")
            parser.add_options(func.options)

            left = []
            value = None
            if '--' in args:
                left = args[:args.index('--')]
                value, args = parser.parse_args(args[args.index('--') + 1:])
            else:
                while args:
                    try:
                        value, args = parser.parse_args(args)
                    except Exception:
                        left.append(args.pop(0))
                    else:
                        break
            args = left + args
            if value:
                kwargs = value.__dict__

        new_args = []
        for arg in args:
            try:
                new_args.append(safe_eval(arg))
            except:
                new_args.append(arg)

        for (key, value) in kwargs.items():
            try:
                kwargs[key] = safe_eval(value)
            except:
                pass

        return new_args, kwargs

    def _split_args(args):
        try:
            # do not use posix mode, to avoid eating quote characters
            args = shlex.split(args, posix=False)
        except:
            # parse error; let's pass args along rather than crashing
            args = args.split()

        new_args = []
        temp = ''
        for arg in args:
            if arg.startswith('-'):
                new_args.append(arg)

            elif temp:

                arg = temp + ' ' + arg
                try:
                    safe_eval(arg)
                except:
                    temp = arg
                else:
                    new_args.append(arg)
                    temp = ''

            elif arg.startswith(('(', '[', '{')) or '(' in arg:
                try:
                    safe_eval(arg)
                except:
                    temp = arg
                else:
                    new_args.append(arg)

            else:
                new_args.append(arg)

        if temp:
            new_args.append(temp)

        return new_args

    def _format_option(option):
        output = ''
        if option._short_opts:
            output = option._short_opts[0] + ' '
        output += option.get_opt_string() + ' '
        output += ' ' * (15 - len(output))
        output += option.help + ' '
        if not option.default == ('NO', 'DEFAULT'):
            output += '[default: %s]' % option.default
        return output

    def _trim(docstring, return_lines=False):
        """
        Trim of unnecessary leading indentations.
        """
        # from: http://legacy.python.org/dev/peps/pep-0257/
        if not docstring:
            return ''
        # Convert tabs to spaces (following the normal Python rules)
        # and split into a list of lines:
        lines = docstring.expandtabs().splitlines()
        indent = _min_indent(lines)
        # Remove indentation (first line is special):
        trimmed = [lines[0].strip()]
        if indent < _maxsize:
            for line in lines[1:]:
                trimmed.append(line[indent:].rstrip())
        # Strip off trailing and leading blank lines:
        while trimmed and not trimmed[-1]:
            trimmed.pop()
        while trimmed and not trimmed[0]:
            trimmed.pop(0)
        if return_lines:
            return trimmed
        else:
            # Return a single string:
            return '\n'.join(trimmed)

    def _min_indent(lines):
        """
        Determine minimum indentation (first line doesn't count):
        """
        indent = _maxsize
        for line in lines[1:]:
            stripped = line.lstrip()
            if stripped:
                indent = min(indent, len(line) - len(stripped))
        return indent

    def _indent(docstring, text):
        """
        Returns text indented at appropriate indententation level.
        """
        if not docstring:
            return text
        lines = docstring.expandtabs().splitlines()
        indent = _min_indent(lines)
        if indent < _maxsize:
            newlines = _trim(text, return_lines=True)
            return "\n" + ("\n".join([(" " * indent) + line for line in newlines]))
        else:
            return "\n" + text