manicmaniac/backquotes

View on GitHub
backquotes.py

Summary

Maintainability
A
2 hrs
Test Coverage
A
100%
#!/usr/bin/env python
# -*- coding:ascii -*-

import contextlib
import inspect
import locale
import optparse
import os
import string
import subprocess
import sys
import tempfile
import tokenize
import warnings


__all__ = ['shell', 'preprocess']
__version__ = '0.1.0'


def shell(argstring):
    r"""Invoke shell commands substituted by variables in the current scope.
    """
    frame = inspect.currentframe().f_back
    variables = frame.f_globals.copy()
    variables.update(frame.f_locals)
    command = string.Template(argstring).substitute(variables)
    process = subprocess.Popen(command,
                               stdout=subprocess.PIPE,
                               shell=True)
    out, _err = process.communicate()
    if sys.version_info < (3,):
        return out
    else:
        return out.decode(locale.getpreferredencoding())


def preprocess(filename, readline):
    r"""Preprocess Python source code using backquotes into plain Python code.

    .. warning:: preprocess() blocks while processing entire source codes.
    """
    tokens = []
    inside_backquotes = False
    quote_start = 0
    for token in tokenize.generate_tokens(readline):
        type, string, (srow, scol), (erow, ecol), line = token
        if string == '`':
            if inside_backquotes:
                # print(`ls`.splitlines())
                #          ^
                quote_end = scol
                quoted_string = line[quote_start:quote_end]
                if _is_quoted(quoted_string):
                    quoted_string = quoted_string[1:-1]
                tokens.extend([
                    (tokenize.STRING, _triple_quote(quoted_string)),
                    (tokenize.OP, ')'),
                ])
            else:
                # print(`ls`.splitlines())
                #       ^
                quote_start = ecol
                tokens.extend([
                    (tokenize.NAME, 'backquotes'),
                    (tokenize.OP, '.'),
                    (tokenize.NAME, 'shell'),
                    (tokenize.OP, '('),
                ])
            inside_backquotes ^= True
        else:
            if inside_backquotes:
                # print(`ls`.splitlines())
                #        ^^
                # quoted string will be extracted at the end of the quotation
                pass
            else:
                # print(`ls`.splitlines())
                # ^^^^^^    ^^^^^^^^^^^^^^
                tokens.append((type, string))
    return tokenize.untokenize(tokens)


@contextlib.contextmanager
def _append_to_python_path(path):
    current_python_path = os.getenv('PYTHONPATH', '')
    if current_python_path:
        os.environ['PYTHONPATH'] = ':'.join((current_python_path, path))
    else:
        os.environ['PYTHONPATH'] = path
    yield
    os.environ['PYTHONPATH'] = current_python_path


def _detect_environment(frame):
    r"""Detect how Python source code is executed.
    """
    if frame.f_code.co_filename == '<stdin>':
        if frame.f_locals.get('__file__') is None:
            return 'repl'
        else:
            return 'redirect'
    outer_frame = frame.f_back
    if outer_frame and outer_frame.f_locals.get('__name__') != '__main__':
        return 'module'
    else:
        return 'script'


def _exec(object, globals, locals):
    r"""A wrapper function to provide consistent interface among Python 2/3.
    """
    if sys.version_info < (3,):
        exec('exec object in globals, locals')
    else:
        exec(object, globals, locals)


def _is_quoted(s):
    r"""Returns whether if string is surrouded by quotations.
    """
    return s[0] in ('"', "'") and s[0] == s[-1]


def _triple_quote(s):
    r"""Returns raw triple single-quoted string.
    """
    return "r'''" + s + "'''"


def _main(argv=sys.argv[1:]):
    r"""Main entry point of this script.
    """
    usage = 'Usage: %prog -m backquotes [options] [FILE] [ARG, ...]'
    prog = os.path.basename(sys.executable)
    parser = optparse.OptionParser(usage=usage, version=__version__, prog=prog)
    parser.add_option(
        '-E',
        '--no-exec',
        dest='execute',
        action='store_false',
        default=True,
        help='stop after preprocessing stage and print preprocessed source')
    opts, args = parser.parse_args(argv)
    try:
        infile = open(args.pop(0), 'r')  # not 'rb'
    except IndexError:
        infile = sys.stdin
    with contextlib.closing(infile):
        preprocessed_source = preprocess(infile.name, infile.readline)
    if opts.execute:
        with tempfile.NamedTemporaryFile(mode='w+') as f:
            f.write(preprocessed_source)
            f.seek(0)
            with _append_to_python_path(os.path.dirname(infile.name)):
                return subprocess.call([sys.executable, f.name] + args)
    else:
        sys.stdout.write(preprocessed_source)
        return 0


if __name__ == '__main__':
    sys.exit(_main())
else:
    frame = inspect.currentframe().f_back
    while frame.f_code.co_filename.startswith('<frozen importlib'):
        frame = frame.f_back
    environment = _detect_environment(frame)
    if environment in ('redirect', 'repl'):
        warnings.warn("backquotes doesn't work on REPL.")
    elif environment == 'module':
        warnings.warn(
            "backquotes doesn't work when imported by another script")
    elif sys.version_info < (3,):
        with open(frame.f_code.co_filename, 'rb') as f:
            source = preprocess(f.name, f.readline)
        _exec(source, frame.f_globals, frame.f_locals)
        sys.exit()