eric6/DebugClients/Python/DebugUtilities.py

Thu, 08 Apr 2021 18:27:47 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Thu, 08 Apr 2021 18:27:47 +0200
changeset 8205
4a0f1f896341
parent 7923
91e843545d9a
child 8217
385f60c94548
permissions
-rw-r--r--

Applied some code simplifications suggested by the new Simplify checker.

# -*- coding: utf-8 -*-

# Copyright (c) 2015 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing utilities functions for the debug client.
"""

import json
import os
import traceback
import sys

#
# Taken from inspect.py of Python 3.4
#

from collections import namedtuple
from inspect import iscode, isframe

# Create constants for the compiler flags in Include/code.h
# We try to get them from dis to avoid duplication, but fall
# back to hardcoding so the dependency is optional
try:
    from dis import COMPILER_FLAG_NAMES
except ImportError:
    CO_OPTIMIZED, CO_NEWLOCALS = 0x1, 0x2
    CO_VARARGS, CO_VARKEYWORDS = 0x4, 0x8
    CO_NESTED, CO_GENERATOR, CO_NOFREE = 0x10, 0x20, 0x40
else:
    mod_dict = globals()
    for k, v in COMPILER_FLAG_NAMES.items():
        mod_dict["CO_" + v] = k

ArgInfo = namedtuple('ArgInfo', 'args varargs keywords locals')


def getargvalues(frame):
    """
    Function to get information about arguments passed into a
    particular frame.
    
    @param frame reference to a frame object to be processed
    @type frame
    @return tuple of four things, where 'args' is a list of the argument names,
        'varargs' and 'varkw' are the names of the * and ** arguments or None
        and 'locals' is the locals dictionary of the given frame.
    @exception TypeError raised if the input parameter is not a frame object
    """
    if not isframe(frame):
        raise TypeError('{0!r} is not a frame object'.format(frame))

    args, varargs, kwonlyargs, varkw = _getfullargs(frame.f_code)
    return ArgInfo(args + kwonlyargs, varargs, varkw, frame.f_locals)


def _getfullargs(co):
    """
    Protected function to get information about the arguments accepted
    by a code object.
    
    @param co reference to a code object to be processed
    @type code
    @return tuple of four things, where 'args' and 'kwonlyargs' are lists of
        argument names, and 'varargs' and 'varkw' are the names of the
        * and ** arguments or None.
    @exception TypeError raised if the input parameter is not a code object
    """
    if not iscode(co):
        raise TypeError('{0!r} is not a code object'.format(co))

    nargs = co.co_argcount
    names = co.co_varnames
    nkwargs = co.co_kwonlyargcount
    args = list(names[:nargs])
    kwonlyargs = list(names[nargs:nargs + nkwargs])

    nargs += nkwargs
    varargs = None
    if co.co_flags & CO_VARARGS:
        varargs = co.co_varnames[nargs]
        nargs = nargs + 1
    varkw = None
    if co.co_flags & CO_VARKEYWORDS:
        varkw = co.co_varnames[nargs]
    return args, varargs, kwonlyargs, varkw


def formatargvalues(args, varargs, varkw, localsDict,
                    formatarg=str,
                    formatvarargs=lambda name: '*' + name,
                    formatvarkw=lambda name: '**' + name,
                    formatvalue=lambda value: '=' + repr(value)):
    """
    Function to format an argument spec from the 4 values returned
    by getargvalues.
    
    @param args list of argument names
    @type list of str
    @param varargs name of the variable arguments
    @type str
    @param varkw name of the keyword arguments
    @type str
    @param localsDict reference to the local variables dictionary
    @type dict
    @param formatarg argument formatting function
    @type func
    @param formatvarargs variable arguments formatting function
    @type func
    @param formatvarkw keyword arguments formatting function
    @type func
    @param formatvalue value formating functtion
    @type func
    @return formatted call signature
    @rtype str
    """
    specs = []
    for i in range(len(args)):
        name = args[i]
        specs.append(formatarg(name) + formatvalue(localsDict[name]))
    if varargs:
        specs.append(formatvarargs(varargs) + formatvalue(localsDict[varargs]))
    if varkw:
        specs.append(formatvarkw(varkw) + formatvalue(localsDict[varkw]))
    argvalues = '(' + ', '.join(specs) + ')'
    if '__return__' in localsDict:
        argvalues += " -> " + formatvalue(localsDict['__return__'])
    return argvalues


def prepareJsonCommand(method, params):
    """
    Function to prepare a single command or response for transmission to
    the IDE.
    
    @param method command or response name to be sent
    @type str
    @param params dictionary of named parameters for the command or response
    @type dict
    @return prepared JSON command or response string
    @rtype str
    """
    commandDict = {
        "jsonrpc": "2.0",
        "method": method,
        "params": params,
    }
    return json.dumps(commandDict) + '\n'

###########################################################################
## Things related to monkey patching below
###########################################################################


PYTHON_NAMES = ["python", "pypy"]


def isWindowsPlatform():
    """
    Function to check, if this is a Windows platform.
    
    @return flag indicating Windows platform
    @rtype bool
    """
    return sys.platform.startswith(("win", "cygwin"))


def isExecutable(program):
    """
    Function to check, if the given program is executable.
    
    @param program program path to be checked
    @type str
    @return flag indicating an executable program
    @rtype bool
    """
    return os.access(os.path.abspath(program), os.X_OK)


def startsWithShebang(program):
    """
    Function to check, if the given program start with a Shebang line.
    
    @param program program path to be checked
    @type str
    @return flag indicating an existing and valid shebang line
    @rtype bool
    """
    try:
        if os.path.exists(program):
            with open(program) as f:
                for line in f:
                    line = line.strip()
                    if line:
                        for name in PYTHON_NAMES:
                            if line.startswith(
                                '#!/usr/bin/env {0}'.format(name)
                            ):
                                return True
                            elif line.startswith('#!') and name in line:
                                return True
                        return False
        else:
            return False
    except UnicodeDecodeError:
        return False
    except Exception:
        traceback.print_exc()
        return False


def isPythonProgram(program):
    """
    Function to check, if the given program is a Python interpreter or
    program.
    
    @param program program to be checked
    @type str
    @return flag indicating a Python interpreter or program
    @rtype bool
    """
    if not program:
        return False
    
    prog = os.path.basename(program).lower()
    for pyname in PYTHON_NAMES:
        if pyname in prog:
            return True
    
    return (
        not isWindowsPlatform() and
        isExecutable(program) and
        startsWithShebang(program)
    )


def removeQuotesFromArgs(args):
    """
    Function to remove quotes from the arguments list.
    
    @param args list of arguments
    @type list of str
    @return list of unquoted strings
    @rtype list of str
    """
    if isWindowsPlatform():
        newArgs = []
        for x in args:
            if len(x) > 1 and x.startswith('"') and x.endswith('"'):
                x = x[1:-1]
            newArgs.append(x)
        return newArgs
    else:
        return args


def quoteArgs(args):
    """
    Function to quote the given list of arguments.
    
    @param args list of arguments to be quoted
    @type list of str
    @return list of quoted arguments
    @rtype list of str
    """
    if isWindowsPlatform():
        quotedArgs = []
        for x in args:
            if x.startswith('"') and x.endswith('"'):
                quotedArgs.append(x)
            else:
                if ' ' in x:
                    x = x.replace('"', '\\"')
                    quotedArgs.append('"{0}"'.format(x))
                else:
                    quotedArgs.append(x)
        return quotedArgs
    else:
        return args


def patchArguments(debugClient, arguments, noRedirect=False):
    """
    Function to patch the arguments given to start a program in order to
    execute it in our debugger.
    
    @param debugClient reference to the debug client object
    @type DebugClient
    @param arguments list of program arguments
    @type list of str
    @param noRedirect flag indicating to not redirect stdin and stdout
    @type bool
    @return modified argument list
    @rtype list of str
    """
    debugClientScript = os.path.join(
        os.path.dirname(__file__), "DebugClient.py")
    if debugClientScript in arguments:
        # it is already patched
        return arguments
    
    args = list(arguments[:])    # create a copy of the arguments list
    args = removeQuotesFromArgs(args)
    
    # support for shebang line
    program = os.path.basename(args[0]).lower()
    for pyname in PYTHON_NAMES:
        if pyname in program:
            break
    else:
        if not isWindowsPlatform() and startsWithShebang(args[0]):
            # insert our interpreter as first argument
            args.insert(0, sys.executable)
        elif isWindowsPlatform() and args[0].lower().endswith(".py"):
            # it is a Python script; insert our interpreter as first argument
            args.insert(0, sys.executable)
    
    # extract list of interpreter arguments, i.e. all arguments before the
    # first one not starting with '-'.
    interpreter = args.pop(0)
    interpreterArgs = []
    hasCode = False
    hasScriptModule = False
    while args:
        if args[0].startswith("-"):
            if args[0] in ("-W", "-X"):
                # take two elements off the list
                interpreterArgs.append(args.pop(0))
                interpreterArgs.append(args.pop(0))
            elif args[0] == "-c":
                # -c indicates code to be executed and ends the
                # arguments list
                args.pop(0)
                hasCode = True
                break
            elif args[0] == "-m":
                # -m indicates a module to be executed as a script
                # and ends the arguments list
                args.pop(0)
                hasScriptModule = True
                break
            else:
                interpreterArgs.append(args.pop(0))
        else:
            break
    
    (wd, host, port, exceptions, tracePython, redirect, noencoding
     ) = debugClient.startOptions[:7]
    
    modifiedArguments = [interpreter]
    modifiedArguments.extend(interpreterArgs)
    modifiedArguments.extend([
        debugClientScript,
        "-h", host,
        "-p", str(port),
        "--no-passive",
    ])
    
    if wd:
        modifiedArguments.extend(["-w", wd])
    if not exceptions:
        modifiedArguments.append("-e")
    if tracePython:
        modifiedArguments.append("-t")
    if noRedirect or not redirect:
        modifiedArguments.append("-n")
    if noencoding:
        modifiedArguments.append("--no-encoding")
    if debugClient.multiprocessSupport:
        modifiedArguments.append("--multiprocess")
    if hasCode:
        modifiedArguments.append("--code")
        modifiedArguments.append(args.pop(0))
    if hasScriptModule:
        modifiedArguments.append("--module")
        modifiedArguments.append(args.pop(0))
    modifiedArguments.append("--")
    # end the arguments for DebugClient
    
    # append the arguments for the program to be debugged
    modifiedArguments.extend(args)
    modifiedArguments = quoteArgs(modifiedArguments)
    
    return modifiedArguments


def stringToArgumentsWindows(args):
    """
    Function to prepare a string of arguments for Windows platform.
    
    @param args list of command arguments
    @type str
    @return list of command arguments
    @rtype list of str
    @exception RuntimeError raised to indicate an illegal arguments parsing
        condition
    """
    # see http:#msdn.microsoft.com/en-us/library/a1y7w461.aspx
    result = []
    
    DEFAULT = 0
    ARG = 1
    IN_DOUBLE_QUOTE = 2
    
    state = DEFAULT
    backslashes = 0
    buf = ''
    
    argsLen = len(args)
    for i in range(argsLen):
        ch = args[i]
        if ch == '\\':
            backslashes += 1
            continue
        elif backslashes != 0:
            if ch == '"':
                while backslashes >= 2:
                    backslashes -= 2
                    buf += '\\'
                if backslashes == 1:
                    if state == DEFAULT:
                        state = ARG
                    
                    buf += '"'
                    backslashes = 0
                    continue
            else:
                # false alarm, treat passed backslashes literally...
                if state == DEFAULT:
                    state = ARG
                
                while backslashes > 0:
                    backslashes -= 1
                    buf += '\\'
        
        if ch in (' ', '\t'):
            if state == DEFAULT:
                # skip
                continue
            elif state == ARG:
                state = DEFAULT
                result.append(buf)
                buf = ''
                continue
        
        if state not in (DEFAULT, ARG, IN_DOUBLE_QUOTE):
            raise RuntimeError('Illegal condition')
        
        if state == IN_DOUBLE_QUOTE:
            if ch == '"':
                if i + 1 < argsLen and args[i + 1] == '"':
                    # Undocumented feature in Windows:
                    # Two consecutive double quotes inside a double-quoted
                    # argument are interpreted as a single double quote.
                    buf += '"'
                    i += 1
                elif len(buf) == 0:
                    result.append("\"\"")
                    state = DEFAULT
                else:
                    state = ARG
            else:
                buf += ch
        
        else:
            if ch == '"':
                state = IN_DOUBLE_QUOTE
            else:
                state = ARG
                buf += ch
    
    if len(buf) > 0 or state != DEFAULT:
        result.append(buf)
    
    return result


def patchArgumentStringWindows(debugClient, argStr):
    """
    Function to patch an argument string for Windows.
    
    @param debugClient reference to the debug client object
    @type DebugClient
    @param argStr argument string
    @type str
    @return patched argument string
    @rtype str
    """
    args = stringToArgumentsWindows(argStr)
    if not args or not isPythonProgram(args[0]):
        return argStr
    
    argStr = ' '.join(patchArguments(debugClient, args))
    return argStr

eric ide

mercurial