src/eric7/DebugClients/Python/DebugUtilities.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8925
8375eb895f70
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2015 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing utilities functions for the debug client.
8 """
9
10 import json
11 import os
12 import traceback
13 import sys
14
15 #
16 # Taken from inspect.py of Python 3.4
17 #
18
19 from collections import namedtuple
20 from inspect import iscode, isframe
21
22 # Create constants for the compiler flags in Include/code.h
23 # We try to get them from dis to avoid duplication, but fall
24 # back to hardcoding so the dependency is optional
25 try:
26 from dis import COMPILER_FLAG_NAMES
27 except ImportError:
28 CO_OPTIMIZED, CO_NEWLOCALS = 0x1, 0x2
29 CO_VARARGS, CO_VARKEYWORDS = 0x4, 0x8
30 CO_NESTED, CO_GENERATOR, CO_NOFREE = 0x10, 0x20, 0x40
31 else:
32 mod_dict = globals()
33 for k, v in COMPILER_FLAG_NAMES.items():
34 mod_dict["CO_" + v] = k
35
36 ArgInfo = namedtuple('ArgInfo', 'args varargs keywords locals')
37
38
39 def getargvalues(frame):
40 """
41 Function to get information about arguments passed into a
42 particular frame.
43
44 @param frame reference to a frame object to be processed
45 @type frame
46 @return tuple of four things, where 'args' is a list of the argument names,
47 'varargs' and 'varkw' are the names of the * and ** arguments or None
48 and 'locals' is the locals dictionary of the given frame.
49 @exception TypeError raised if the input parameter is not a frame object
50 """
51 if not isframe(frame):
52 raise TypeError('{0!r} is not a frame object'.format(frame))
53
54 args, varargs, kwonlyargs, varkw = _getfullargs(frame.f_code)
55 return ArgInfo(args + kwonlyargs, varargs, varkw, frame.f_locals)
56
57
58 def _getfullargs(co):
59 """
60 Protected function to get information about the arguments accepted
61 by a code object.
62
63 @param co reference to a code object to be processed
64 @type code
65 @return tuple of four things, where 'args' and 'kwonlyargs' are lists of
66 argument names, and 'varargs' and 'varkw' are the names of the
67 * and ** arguments or None.
68 @exception TypeError raised if the input parameter is not a code object
69 """
70 if not iscode(co):
71 raise TypeError('{0!r} is not a code object'.format(co))
72
73 nargs = co.co_argcount
74 names = co.co_varnames
75 nkwargs = co.co_kwonlyargcount
76 args = list(names[:nargs])
77 kwonlyargs = list(names[nargs:nargs + nkwargs])
78
79 nargs += nkwargs
80 varargs = None
81 if co.co_flags & CO_VARARGS:
82 varargs = co.co_varnames[nargs]
83 nargs += 1
84 varkw = None
85 if co.co_flags & CO_VARKEYWORDS:
86 varkw = co.co_varnames[nargs]
87 return args, varargs, kwonlyargs, varkw
88
89
90 def formatargvalues(args, varargs, varkw, localsDict,
91 formatarg=str,
92 formatvarargs=lambda name: '*' + name,
93 formatvarkw=lambda name: '**' + name,
94 formatvalue=lambda value: '=' + repr(value)):
95 """
96 Function to format an argument spec from the 4 values returned
97 by getargvalues.
98
99 @param args list of argument names
100 @type list of str
101 @param varargs name of the variable arguments
102 @type str
103 @param varkw name of the keyword arguments
104 @type str
105 @param localsDict reference to the local variables dictionary
106 @type dict
107 @param formatarg argument formatting function
108 @type func
109 @param formatvarargs variable arguments formatting function
110 @type func
111 @param formatvarkw keyword arguments formatting function
112 @type func
113 @param formatvalue value formating functtion
114 @type func
115 @return formatted call signature
116 @rtype str
117 """
118 specs = []
119 for i in range(len(args)):
120 name = args[i]
121 specs.append(formatarg(name) + formatvalue(localsDict[name]))
122 if varargs:
123 specs.append(formatvarargs(varargs) + formatvalue(localsDict[varargs]))
124 if varkw:
125 specs.append(formatvarkw(varkw) + formatvalue(localsDict[varkw]))
126 argvalues = '(' + ', '.join(specs) + ')'
127 if '__return__' in localsDict:
128 argvalues += " -> " + formatvalue(localsDict['__return__'])
129 return argvalues
130
131
132 def prepareJsonCommand(method, params):
133 """
134 Function to prepare a single command or response for transmission to
135 the IDE.
136
137 @param method command or response name to be sent
138 @type str
139 @param params dictionary of named parameters for the command or response
140 @type dict
141 @return prepared JSON command or response string
142 @rtype str
143 """
144 commandDict = {
145 "jsonrpc": "2.0",
146 "method": method,
147 "params": params,
148 }
149 return json.dumps(commandDict) + '\n'
150
151 ###########################################################################
152 ## Things related to monkey patching below
153 ###########################################################################
154
155
156 PYTHON_NAMES = ["python", "pypy"]
157
158
159 def isWindowsPlatform():
160 """
161 Function to check, if this is a Windows platform.
162
163 @return flag indicating Windows platform
164 @rtype bool
165 """
166 return sys.platform.startswith(("win", "cygwin"))
167
168
169 def isExecutable(program):
170 """
171 Function to check, if the given program is executable.
172
173 @param program program path to be checked
174 @type str
175 @return flag indicating an executable program
176 @rtype bool
177 """
178 return os.access(os.path.abspath(program), os.X_OK)
179
180
181 def startsWithShebang(program):
182 """
183 Function to check, if the given program start with a Shebang line.
184
185 @param program program path to be checked
186 @type str
187 @return flag indicating an existing and valid shebang line
188 @rtype bool
189 """
190 try:
191 if os.path.exists(program):
192 with open(program) as f:
193 for line in f:
194 line = line.strip()
195 if line:
196 for name in PYTHON_NAMES: # __IGNORE_WARNING_Y110__
197 if (
198 line.startswith(
199 '#!/usr/bin/env {0}'.format(name)) or
200 (line.startswith('#!') and name in line)
201 ):
202 return True
203 return False
204 else:
205 return False
206 except UnicodeDecodeError:
207 return False
208 except Exception:
209 traceback.print_exc()
210 return False
211
212
213 def isPythonProgram(program):
214 """
215 Function to check, if the given program is a Python interpreter or
216 program.
217
218 @param program program to be checked
219 @type str
220 @return flag indicating a Python interpreter or program
221 @rtype bool
222 """
223 if not program:
224 return False
225
226 prog = os.path.basename(program).lower()
227 if any(pyname in prog for pyname in PYTHON_NAMES):
228 return True
229
230 return (
231 not isWindowsPlatform() and
232 isExecutable(program) and
233 startsWithShebang(program)
234 )
235
236
237 def removeQuotesFromArgs(args):
238 """
239 Function to remove quotes from the arguments list.
240
241 @param args list of arguments
242 @type list of str
243 @return list of unquoted strings
244 @rtype list of str
245 """
246 if isWindowsPlatform():
247 newArgs = []
248 for x in args:
249 if len(x) > 1 and x.startswith('"') and x.endswith('"'):
250 x = x[1:-1]
251 newArgs.append(x)
252 return newArgs
253 else:
254 return args
255
256
257 def quoteArgs(args):
258 """
259 Function to quote the given list of arguments.
260
261 @param args list of arguments to be quoted
262 @type list of str
263 @return list of quoted arguments
264 @rtype list of str
265 """
266 if isWindowsPlatform():
267 quotedArgs = []
268 for x in args:
269 if x.startswith('"') and x.endswith('"'):
270 quotedArgs.append(x)
271 else:
272 if ' ' in x:
273 x = x.replace('"', '\\"')
274 quotedArgs.append('"{0}"'.format(x))
275 else:
276 quotedArgs.append(x)
277 return quotedArgs
278 else:
279 return args
280
281
282 def patchArguments(debugClient, arguments, noRedirect=False):
283 """
284 Function to patch the arguments given to start a program in order to
285 execute it in our debugger.
286
287 @param debugClient reference to the debug client object
288 @type DebugClient
289 @param arguments list of program arguments
290 @type list of str
291 @param noRedirect flag indicating to not redirect stdin and stdout
292 @type bool
293 @return modified argument list
294 @rtype list of str
295 """
296 debugClientScript = os.path.join(
297 os.path.dirname(__file__), "DebugClient.py")
298 if debugClientScript in arguments:
299 # it is already patched
300 return arguments
301
302 args = list(arguments[:]) # create a copy of the arguments list
303 args = removeQuotesFromArgs(args)
304
305 # support for shebang line
306 program = os.path.basename(args[0]).lower()
307 for pyname in PYTHON_NAMES:
308 if pyname in program:
309 break
310 else:
311 if (
312 (not isWindowsPlatform() and startsWithShebang(args[0])) or
313 (isWindowsPlatform() and args[0].lower().endswith(".py"))
314 ):
315 # 1. insert our interpreter as first argument if not Windows
316 # 2. insert our interpreter as first argument if on Windows and
317 # it is a Python script
318 args.insert(0, sys.executable)
319
320 # extract list of interpreter arguments, i.e. all arguments before the
321 # first one not starting with '-'.
322 interpreter = args.pop(0)
323 interpreterArgs = []
324 hasCode = False
325 hasScriptModule = False
326 while args:
327 if args[0].startswith("-"):
328 if args[0] in ("-W", "-X"):
329 # take two elements off the list
330 interpreterArgs.append(args.pop(0))
331 interpreterArgs.append(args.pop(0))
332 elif args[0] == "-c":
333 # -c indicates code to be executed and ends the
334 # arguments list
335 args.pop(0)
336 hasCode = True
337 break
338 elif args[0] == "-m":
339 # -m indicates a module to be executed as a script
340 # and ends the arguments list
341 args.pop(0)
342 hasScriptModule = True
343 break
344 else:
345 interpreterArgs.append(args.pop(0))
346 else:
347 break
348
349 (wd, host, port, exceptions, tracePython, redirect, noencoding
350 ) = debugClient.startOptions[:7]
351
352 modifiedArguments = [interpreter]
353 modifiedArguments.extend(interpreterArgs)
354 modifiedArguments.extend([
355 debugClientScript,
356 "-h", host,
357 "-p", str(port),
358 "--no-passive",
359 ])
360
361 if wd:
362 modifiedArguments.extend(["-w", wd])
363 if not exceptions:
364 modifiedArguments.append("-e")
365 if tracePython:
366 modifiedArguments.append("-t")
367 if noRedirect or not redirect:
368 modifiedArguments.append("-n")
369 if noencoding:
370 modifiedArguments.append("--no-encoding")
371 if debugClient.multiprocessSupport:
372 modifiedArguments.append("--multiprocess")
373 if hasCode:
374 modifiedArguments.append("--code")
375 modifiedArguments.append(args.pop(0))
376 if hasScriptModule:
377 modifiedArguments.append("--module")
378 modifiedArguments.append(args.pop(0))
379 modifiedArguments.append("--")
380 # end the arguments for DebugClient
381
382 # append the arguments for the program to be debugged
383 modifiedArguments.extend(args)
384 modifiedArguments = quoteArgs(modifiedArguments)
385
386 return modifiedArguments
387
388
389 def stringToArgumentsWindows(args):
390 """
391 Function to prepare a string of arguments for Windows platform.
392
393 @param args list of command arguments
394 @type str
395 @return list of command arguments
396 @rtype list of str
397 @exception RuntimeError raised to indicate an illegal arguments parsing
398 condition
399 """
400 # see http://msdn.microsoft.com/en-us/library/a1y7w461.aspx
401 result = []
402
403 DEFAULT = 0
404 ARG = 1
405 IN_DOUBLE_QUOTE = 2
406
407 state = DEFAULT
408 backslashes = 0
409 buf = ''
410
411 argsLen = len(args)
412 i = 0
413 while i < argsLen:
414 ch = args[i]
415 if ch == '\\':
416 backslashes += 1
417 i += 1
418 continue
419 elif backslashes != 0:
420 if ch == '"':
421 while backslashes >= 2:
422 backslashes -= 2
423 buf += '\\'
424 if backslashes == 1:
425 if state == DEFAULT:
426 state = ARG
427
428 buf += '"'
429 backslashes = 0
430 i += 1
431 continue
432 else:
433 # false alarm, treat passed backslashes literally...
434 if state == DEFAULT:
435 state = ARG
436
437 while backslashes > 0:
438 backslashes -= 1
439 buf += '\\'
440
441 if ch in (' ', '\t'):
442 if state == DEFAULT:
443 # skip
444 i += 1
445 continue
446 elif state == ARG:
447 state = DEFAULT
448 result.append(buf)
449 buf = ''
450 i += 1
451 continue
452
453 if state not in (DEFAULT, ARG, IN_DOUBLE_QUOTE):
454 raise RuntimeError('Illegal condition')
455
456 if state == IN_DOUBLE_QUOTE:
457 if ch == '"':
458 if i + 1 < argsLen and args[i + 1] == '"':
459 # Undocumented feature in Windows:
460 # Two consecutive double quotes inside a double-quoted
461 # argument are interpreted as a single double quote.
462 buf += '"'
463 i += 1
464 elif len(buf) == 0:
465 result.append("\"\"")
466 state = DEFAULT
467 else:
468 state = ARG
469 else:
470 buf += ch
471
472 else:
473 if ch == '"':
474 state = IN_DOUBLE_QUOTE
475 else:
476 state = ARG
477 buf += ch
478
479 i += 1
480
481 if len(buf) > 0 or state != DEFAULT:
482 result.append(buf)
483
484 return result
485
486
487 def patchArgumentStringWindows(debugClient, argStr):
488 """
489 Function to patch an argument string for Windows.
490
491 @param debugClient reference to the debug client object
492 @type DebugClient
493 @param argStr argument string
494 @type str
495 @return patched argument string
496 @rtype str
497 """
498 args = stringToArgumentsWindows(argStr)
499 if not args or not isPythonProgram(args[0]):
500 return argStr
501
502 argStr = ' '.join(patchArguments(debugClient, args))
503 return argStr

eric ide

mercurial