src/eric7/DebugClients/Python/DebugUtilities.py

branch
eric7-maintenance
changeset 9264
18a7312cfdb3
parent 9221
bf71ee032bb4
child 9473
3f23dbf37dbe
equal deleted inserted replaced
9241:d23e9854aea4 9264:18a7312cfdb3
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(
91 args,
92 varargs,
93 varkw,
94 localsDict,
95 formatarg=str,
96 formatvarargs=lambda name: "*" + name,
97 formatvarkw=lambda name: "**" + name,
98 formatvalue=lambda value: "=" + repr(value),
99 ):
100 """
101 Function to format an argument spec from the 4 values returned
102 by getargvalues.
103
104 @param args list of argument names
105 @type list of str
106 @param varargs name of the variable arguments
107 @type str
108 @param varkw name of the keyword arguments
109 @type str
110 @param localsDict reference to the local variables dictionary
111 @type dict
112 @param formatarg argument formatting function
113 @type func
114 @param formatvarargs variable arguments formatting function
115 @type func
116 @param formatvarkw keyword arguments formatting function
117 @type func
118 @param formatvalue value formating functtion
119 @type func
120 @return formatted call signature
121 @rtype str
122 """
123 specs = []
124 for i in range(len(args)):
125 name = args[i]
126 specs.append(formatarg(name) + formatvalue(localsDict[name]))
127 if varargs:
128 specs.append(formatvarargs(varargs) + formatvalue(localsDict[varargs]))
129 if varkw:
130 specs.append(formatvarkw(varkw) + formatvalue(localsDict[varkw]))
131 argvalues = "(" + ", ".join(specs) + ")"
132 if "__return__" in localsDict:
133 argvalues += " -> " + formatvalue(localsDict["__return__"])
134 return argvalues
135
136
137 def prepareJsonCommand(method, params):
138 """
139 Function to prepare a single command or response for transmission to
140 the IDE.
141
142 @param method command or response name to be sent
143 @type str
144 @param params dictionary of named parameters for the command or response
145 @type dict
146 @return prepared JSON command or response string
147 @rtype str
148 """
149 commandDict = {
150 "jsonrpc": "2.0",
151 "method": method,
152 "params": params,
153 }
154 return json.dumps(commandDict) + "\n"
155
156
157 ###########################################################################
158 ## Things related to monkey patching below
159 ###########################################################################
160
161
162 PYTHON_NAMES = ["python", "pypy"]
163
164
165 def isWindowsPlatform():
166 """
167 Function to check, if this is a Windows platform.
168
169 @return flag indicating Windows platform
170 @rtype bool
171 """
172 return sys.platform.startswith(("win", "cygwin"))
173
174
175 def isExecutable(program):
176 """
177 Function to check, if the given program is executable.
178
179 @param program program path to be checked
180 @type str
181 @return flag indicating an executable program
182 @rtype bool
183 """
184 return os.access(os.path.abspath(program), os.X_OK)
185
186
187 def startsWithShebang(program):
188 """
189 Function to check, if the given program start with a Shebang line.
190
191 @param program program path to be checked
192 @type str
193 @return flag indicating an existing and valid shebang line
194 @rtype bool
195 """
196 try:
197 if os.path.exists(program):
198 with open(program) as f:
199 for line in f:
200 line = line.strip()
201 if line:
202 for name in PYTHON_NAMES: # __IGNORE_WARNING_Y110__
203 if line.startswith("#!/usr/bin/env {0}".format(name)) or (
204 line.startswith("#!") and name in line
205 ):
206 return True
207 return False
208 else:
209 return False
210 except UnicodeDecodeError:
211 return False
212 except Exception:
213 traceback.print_exc()
214 return False
215
216
217 def isPythonProgram(program):
218 """
219 Function to check, if the given program is a Python interpreter or
220 program.
221
222 @param program program to be checked
223 @type str
224 @return flag indicating a Python interpreter or program
225 @rtype bool
226 """
227 if not program:
228 return False
229
230 prog = os.path.basename(program).lower()
231 if any(pyname in prog for pyname in PYTHON_NAMES):
232 return True
233
234 return (
235 not isWindowsPlatform() and isExecutable(program) and startsWithShebang(program)
236 )
237
238
239 def removeQuotesFromArgs(args):
240 """
241 Function to remove quotes from the arguments list.
242
243 @param args list of arguments
244 @type list of str
245 @return list of unquoted strings
246 @rtype list of str
247 """
248 if isWindowsPlatform():
249 newArgs = []
250 for x in args:
251 if len(x) > 1 and x.startswith('"') and x.endswith('"'):
252 x = x[1:-1]
253 newArgs.append(x)
254 return newArgs
255 else:
256 return args
257
258
259 def quoteArgs(args):
260 """
261 Function to quote the given list of arguments.
262
263 @param args list of arguments to be quoted
264 @type list of str
265 @return list of quoted arguments
266 @rtype list of str
267 """
268 if isWindowsPlatform():
269 quotedArgs = []
270 for x in args:
271 if x.startswith('"') and x.endswith('"'):
272 quotedArgs.append(x)
273 else:
274 if " " in x:
275 x = x.replace('"', '\\"')
276 quotedArgs.append('"{0}"'.format(x))
277 else:
278 quotedArgs.append(x)
279 return quotedArgs
280 else:
281 return args
282
283
284 def patchArguments(debugClient, arguments, noRedirect=False):
285 """
286 Function to patch the arguments given to start a program in order to
287 execute it in our debugger.
288
289 @param debugClient reference to the debug client object
290 @type DebugClient
291 @param arguments list of program arguments
292 @type list of str
293 @param noRedirect flag indicating to not redirect stdin and stdout
294 @type bool
295 @return modified argument list
296 @rtype list of str
297 """
298 debugClientScript = os.path.join(os.path.dirname(__file__), "DebugClient.py")
299 if debugClientScript in arguments:
300 # it is already patched
301 return arguments
302
303 args = list(arguments[:]) # create a copy of the arguments list
304 args = removeQuotesFromArgs(args)
305
306 # support for shebang line
307 program = os.path.basename(args[0]).lower()
308 for pyname in PYTHON_NAMES:
309 if pyname in program:
310 break
311 else:
312 if (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 (
350 wd,
351 host,
352 port,
353 exceptions,
354 tracePython,
355 redirect,
356 noencoding,
357 ) = debugClient.startOptions[:7]
358
359 modifiedArguments = [interpreter]
360 modifiedArguments.extend(interpreterArgs)
361 modifiedArguments.extend(
362 [
363 debugClientScript,
364 "-h",
365 host,
366 "-p",
367 str(port),
368 "--no-passive",
369 ]
370 )
371
372 if wd:
373 modifiedArguments.extend(["-w", wd])
374 if not exceptions:
375 modifiedArguments.append("-e")
376 if tracePython:
377 modifiedArguments.append("-t")
378 if noRedirect or not redirect:
379 modifiedArguments.append("-n")
380 if noencoding:
381 modifiedArguments.append("--no-encoding")
382 if debugClient.multiprocessSupport:
383 modifiedArguments.append("--multiprocess")
384 if hasCode:
385 modifiedArguments.append("--code")
386 modifiedArguments.append(args.pop(0))
387 if hasScriptModule:
388 modifiedArguments.append("--module")
389 modifiedArguments.append(args.pop(0))
390 modifiedArguments.append("--")
391 # end the arguments for DebugClient
392
393 # append the arguments for the program to be debugged
394 modifiedArguments.extend(args)
395 modifiedArguments = quoteArgs(modifiedArguments)
396
397 return modifiedArguments
398
399
400 def stringToArgumentsWindows(args):
401 """
402 Function to prepare a string of arguments for Windows platform.
403
404 @param args list of command arguments
405 @type str
406 @return list of command arguments
407 @rtype list of str
408 @exception RuntimeError raised to indicate an illegal arguments parsing
409 condition
410 """
411 # see http://msdn.microsoft.com/en-us/library/a1y7w461.aspx
412 result = []
413
414 DEFAULT = 0
415 ARG = 1
416 IN_DOUBLE_QUOTE = 2
417
418 state = DEFAULT
419 backslashes = 0
420 buf = ""
421
422 argsLen = len(args)
423 i = 0
424 while i < argsLen:
425 ch = args[i]
426 if ch == "\\":
427 backslashes += 1
428 i += 1
429 continue
430 elif backslashes != 0:
431 if ch == '"':
432 while backslashes >= 2:
433 backslashes -= 2
434 buf += "\\"
435 if backslashes == 1:
436 if state == DEFAULT:
437 state = ARG
438
439 buf += '"'
440 backslashes = 0
441 i += 1
442 continue
443 else:
444 # false alarm, treat passed backslashes literally...
445 if state == DEFAULT:
446 state = ARG
447
448 while backslashes > 0:
449 backslashes -= 1
450 buf += "\\"
451
452 if ch in (" ", "\t"):
453 if state == DEFAULT:
454 # skip
455 i += 1
456 continue
457 elif state == ARG:
458 state = DEFAULT
459 result.append(buf)
460 buf = ""
461 i += 1
462 continue
463
464 if state not in (DEFAULT, ARG, IN_DOUBLE_QUOTE):
465 raise RuntimeError("Illegal condition")
466
467 if state == IN_DOUBLE_QUOTE:
468 if ch == '"':
469 if i + 1 < argsLen and args[i + 1] == '"':
470 # Undocumented feature in Windows:
471 # Two consecutive double quotes inside a double-quoted
472 # argument are interpreted as a single double quote.
473 buf += '"'
474 i += 1
475 elif len(buf) == 0:
476 result.append('""')
477 state = DEFAULT
478 else:
479 state = ARG
480 else:
481 buf += ch
482
483 else:
484 if ch == '"':
485 state = IN_DOUBLE_QUOTE
486 else:
487 state = ARG
488 buf += ch
489
490 i += 1
491
492 if len(buf) > 0 or state != DEFAULT:
493 result.append(buf)
494
495 return result
496
497
498 def patchArgumentStringWindows(debugClient, argStr):
499 """
500 Function to patch an argument string for Windows.
501
502 @param debugClient reference to the debug client object
503 @type DebugClient
504 @param argStr argument string
505 @type str
506 @return patched argument string
507 @rtype str
508 """
509 args = stringToArgumentsWindows(argStr)
510 if not args or not isPythonProgram(args[0]):
511 return argStr
512
513 argStr = " ".join(patchArguments(debugClient, args))
514 return argStr

eric ide

mercurial