mirror of
https://github.com/microsoft/debugpy.git
synced 2025-12-23 08:48:12 +00:00
Support top-level async. Fixes #951
This commit is contained in:
parent
6e247fb17b
commit
71d42ed63f
9 changed files with 512 additions and 142 deletions
|
|
@ -1,11 +1,15 @@
|
|||
"""Utilities needed to emulate Python's interactive interpreter.
|
||||
"""
|
||||
A copy of the code module in the standard library with some changes to work with
|
||||
async evaluation.
|
||||
|
||||
Utilities needed to emulate Python's interactive interpreter.
|
||||
"""
|
||||
|
||||
# Inspired by similar code by Jeff Epler and Fredrik Lundh.
|
||||
|
||||
import sys
|
||||
import traceback
|
||||
import inspect
|
||||
|
||||
# START --------------------------- from codeop import CommandCompiler, compile_command
|
||||
# START --------------------------- from codeop import CommandCompiler, compile_command
|
||||
|
|
@ -100,18 +104,21 @@ def _maybe_compile(compiler, source, filename, symbol):
|
|||
|
||||
try:
|
||||
code1 = compiler(source + "\n", filename, symbol)
|
||||
except SyntaxError as err1:
|
||||
pass
|
||||
except SyntaxError as e:
|
||||
err1 = e
|
||||
|
||||
try:
|
||||
code2 = compiler(source + "\n\n", filename, symbol)
|
||||
except SyntaxError as err2:
|
||||
pass
|
||||
except SyntaxError as e:
|
||||
err2 = e
|
||||
|
||||
if code:
|
||||
return code
|
||||
if not code1 and repr(err1) == repr(err2):
|
||||
raise SyntaxError(err1)
|
||||
try:
|
||||
if code:
|
||||
return code
|
||||
if not code1 and repr(err1) == repr(err2):
|
||||
raise err1
|
||||
finally:
|
||||
err1 = err2 = None
|
||||
|
||||
|
||||
def _compile(source, filename, symbol):
|
||||
|
|
@ -148,6 +155,12 @@ class Compile:
|
|||
def __init__(self):
|
||||
self.flags = PyCF_DONT_IMPLY_DEDENT
|
||||
|
||||
try:
|
||||
from ast import PyCF_ALLOW_TOP_LEVEL_AWAIT
|
||||
self.flags |= PyCF_ALLOW_TOP_LEVEL_AWAIT
|
||||
except:
|
||||
pass
|
||||
|
||||
def __call__(self, source, filename, symbol):
|
||||
codeob = compile(source, filename, symbol, self.flags, 1)
|
||||
for feature in _features:
|
||||
|
|
@ -197,19 +210,33 @@ class CommandCompiler:
|
|||
__all__ = ["InteractiveInterpreter", "InteractiveConsole", "interact",
|
||||
"compile_command"]
|
||||
|
||||
from _pydev_bundle._pydev_saved_modules import threading
|
||||
|
||||
def softspace(file, newvalue):
|
||||
oldvalue = 0
|
||||
try:
|
||||
oldvalue = file.softspace
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
file.softspace = newvalue
|
||||
except (AttributeError, TypeError):
|
||||
# "attribute-less object" or "read-only attributes"
|
||||
pass
|
||||
return oldvalue
|
||||
|
||||
class _EvalAwaitInNewEventLoop(threading.Thread):
|
||||
|
||||
def __init__(self, compiled, updated_globals, updated_locals):
|
||||
threading.Thread.__init__(self)
|
||||
self.daemon = True
|
||||
self._compiled = compiled
|
||||
self._updated_globals = updated_globals
|
||||
self._updated_locals = updated_locals
|
||||
|
||||
# Output
|
||||
self.evaluated_value = None
|
||||
self.exc = None
|
||||
|
||||
async def _async_func(self):
|
||||
return await eval(self._compiled, self._updated_locals, self._updated_globals)
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
import asyncio
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
self.evaluated_value = asyncio.run(self._async_func())
|
||||
except:
|
||||
self.exc = sys.exc_info()
|
||||
|
||||
|
||||
class InteractiveInterpreter:
|
||||
|
|
@ -240,7 +267,7 @@ class InteractiveInterpreter:
|
|||
|
||||
Arguments are as for compile_command().
|
||||
|
||||
One several things can happen:
|
||||
One of several things can happen:
|
||||
|
||||
1) The input is incorrect; compile_command() raised an
|
||||
exception (SyntaxError or OverflowError). A syntax traceback
|
||||
|
|
@ -287,14 +314,24 @@ class InteractiveInterpreter:
|
|||
|
||||
"""
|
||||
try:
|
||||
exec(code, self.locals)
|
||||
is_async = False
|
||||
if hasattr(inspect, 'CO_COROUTINE'):
|
||||
is_async = inspect.CO_COROUTINE & code.co_flags == inspect.CO_COROUTINE
|
||||
|
||||
if is_async:
|
||||
t = _EvalAwaitInNewEventLoop(code, self.locals, None)
|
||||
t.start()
|
||||
t.join()
|
||||
|
||||
if t.exc:
|
||||
raise t.exc[1].with_traceback(t.exc[2])
|
||||
|
||||
else:
|
||||
exec(code, self.locals)
|
||||
except SystemExit:
|
||||
raise
|
||||
except:
|
||||
self.showtraceback()
|
||||
else:
|
||||
if softspace(sys.stdout, 0):
|
||||
sys.stdout.write('\n')
|
||||
|
||||
def showsyntaxerror(self, filename=None):
|
||||
"""Display the syntax error that just occurred.
|
||||
|
|
@ -308,24 +345,30 @@ class InteractiveInterpreter:
|
|||
The output is written by self.write(), below.
|
||||
|
||||
"""
|
||||
type, value, sys.last_traceback = sys.exc_info()
|
||||
type, value, tb = sys.exc_info()
|
||||
sys.last_type = type
|
||||
sys.last_value = value
|
||||
sys.last_traceback = tb
|
||||
if filename and type is SyntaxError:
|
||||
# Work hard to stuff the correct filename in the exception
|
||||
try:
|
||||
msg, (dummy_filename, lineno, offset, line) = value
|
||||
except:
|
||||
msg, (dummy_filename, lineno, offset, line) = value.args
|
||||
except ValueError:
|
||||
# Not the format we expect; leave it alone
|
||||
pass
|
||||
else:
|
||||
# Stuff in the right filename
|
||||
value = SyntaxError(msg, (filename, lineno, offset, line))
|
||||
sys.last_value = value
|
||||
list = traceback.format_exception_only(type, value)
|
||||
map(self.write, list)
|
||||
if sys.excepthook is sys.__excepthook__:
|
||||
lines = traceback.format_exception_only(type, value)
|
||||
self.write(''.join(lines))
|
||||
else:
|
||||
# If someone has set sys.excepthook, we let that take precedence
|
||||
# over self.write
|
||||
sys.excepthook(type, value, tb)
|
||||
|
||||
def showtraceback(self, *args, **kwargs):
|
||||
def showtraceback(self):
|
||||
"""Display the exception that just occurred.
|
||||
|
||||
We remove the first stack item because it is our own code.
|
||||
|
|
@ -333,20 +376,18 @@ class InteractiveInterpreter:
|
|||
The output is written by self.write(), below.
|
||||
|
||||
"""
|
||||
sys.last_type, sys.last_value, last_tb = ei = sys.exc_info()
|
||||
sys.last_traceback = last_tb
|
||||
try:
|
||||
type, value, tb = sys.exc_info()
|
||||
sys.last_type = type
|
||||
sys.last_value = value
|
||||
sys.last_traceback = tb
|
||||
tblist = traceback.extract_tb(tb)
|
||||
del tblist[:1]
|
||||
list = traceback.format_list(tblist)
|
||||
if list:
|
||||
list.insert(0, "Traceback (most recent call last):\n")
|
||||
list[len(list):] = traceback.format_exception_only(type, value)
|
||||
lines = traceback.format_exception(ei[0], ei[1], last_tb.tb_next)
|
||||
if sys.excepthook is sys.__excepthook__:
|
||||
self.write(''.join(lines))
|
||||
else:
|
||||
# If someone has set sys.excepthook, we let that take precedence
|
||||
# over self.write
|
||||
sys.excepthook(ei[0], ei[1], last_tb)
|
||||
finally:
|
||||
tblist = tb = None
|
||||
map(self.write, list)
|
||||
last_tb = ei = None
|
||||
|
||||
def write(self, data):
|
||||
"""Write a string.
|
||||
|
|
@ -384,23 +425,28 @@ class InteractiveConsole(InteractiveInterpreter):
|
|||
"""Reset the input buffer."""
|
||||
self.buffer = []
|
||||
|
||||
def interact(self, banner=None):
|
||||
def interact(self, banner=None, exitmsg=None):
|
||||
"""Closely emulate the interactive Python console.
|
||||
|
||||
The optional banner argument specify the banner to print
|
||||
The optional banner argument specifies the banner to print
|
||||
before the first interaction; by default it prints a banner
|
||||
similar to the one printed by the real Python interpreter,
|
||||
followed by the current class name in parentheses (so as not
|
||||
to confuse this with the real interpreter -- since it's so
|
||||
close!).
|
||||
|
||||
The optional exitmsg argument specifies the exit message
|
||||
printed when exiting. Pass the empty string to suppress
|
||||
printing an exit message. If exitmsg is not given or None,
|
||||
a default message is printed.
|
||||
|
||||
"""
|
||||
try:
|
||||
sys.ps1 # @UndefinedVariable
|
||||
sys.ps1
|
||||
except AttributeError:
|
||||
sys.ps1 = ">>> "
|
||||
try:
|
||||
sys.ps2 # @UndefinedVariable
|
||||
sys.ps2
|
||||
except AttributeError:
|
||||
sys.ps2 = "... "
|
||||
cprt = 'Type "help", "copyright", "credits" or "license" for more information.'
|
||||
|
|
@ -408,21 +454,17 @@ class InteractiveConsole(InteractiveInterpreter):
|
|||
self.write("Python %s on %s\n%s\n(%s)\n" %
|
||||
(sys.version, sys.platform, cprt,
|
||||
self.__class__.__name__))
|
||||
else:
|
||||
elif banner:
|
||||
self.write("%s\n" % str(banner))
|
||||
more = 0
|
||||
while 1:
|
||||
try:
|
||||
if more:
|
||||
prompt = sys.ps2 # @UndefinedVariable
|
||||
prompt = sys.ps2
|
||||
else:
|
||||
prompt = sys.ps1 # @UndefinedVariable
|
||||
prompt = sys.ps1
|
||||
try:
|
||||
line = self.raw_input(prompt)
|
||||
# Can be None if sys.stdin was redefined
|
||||
encoding = getattr(sys.stdin, "encoding", None)
|
||||
if encoding and not isinstance(line, str):
|
||||
line = line.decode(encoding)
|
||||
except EOFError:
|
||||
self.write("\n")
|
||||
break
|
||||
|
|
@ -432,6 +474,10 @@ class InteractiveConsole(InteractiveInterpreter):
|
|||
self.write("\nKeyboardInterrupt\n")
|
||||
self.resetbuffer()
|
||||
more = 0
|
||||
if exitmsg is None:
|
||||
self.write('now exiting %s...\n' % self.__class__.__name__)
|
||||
elif exitmsg != '':
|
||||
self.write('%s\n' % exitmsg)
|
||||
|
||||
def push(self, line):
|
||||
"""Push a line to the interpreter.
|
||||
|
|
@ -461,14 +507,14 @@ class InteractiveConsole(InteractiveInterpreter):
|
|||
When the user enters the EOF key sequence, EOFError is raised.
|
||||
|
||||
The base implementation uses the built-in function
|
||||
raw_input(); a subclass may replace this with a different
|
||||
input(); a subclass may replace this with a different
|
||||
implementation.
|
||||
|
||||
"""
|
||||
return input(prompt)
|
||||
|
||||
|
||||
def interact(banner=None, readfunc=None, local=None):
|
||||
def interact(banner=None, readfunc=None, local=None, exitmsg=None):
|
||||
"""Closely emulate the interactive Python interpreter.
|
||||
|
||||
This is a backwards compatible interface to the InteractiveConsole
|
||||
|
|
@ -480,6 +526,7 @@ def interact(banner=None, readfunc=None, local=None):
|
|||
banner -- passed to InteractiveConsole.interact()
|
||||
readfunc -- if not None, replaces InteractiveConsole.raw_input()
|
||||
local -- passed to InteractiveInterpreter.__init__()
|
||||
exitmsg -- passed to InteractiveConsole.interact()
|
||||
|
||||
"""
|
||||
console = InteractiveConsole(local)
|
||||
|
|
@ -490,9 +537,18 @@ def interact(banner=None, readfunc=None, local=None):
|
|||
import readline
|
||||
except ImportError:
|
||||
pass
|
||||
console.interact(banner)
|
||||
console.interact(banner, exitmsg)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import pdb
|
||||
pdb.run("interact()\n")
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-q', action='store_true',
|
||||
help="don't print version and copyright messages")
|
||||
args = parser.parse_args()
|
||||
if args.q or sys.flags.quiet:
|
||||
banner = ''
|
||||
else:
|
||||
banner = None
|
||||
interact(banner)
|
||||
|
|
@ -833,7 +833,7 @@ class InternalGetArray(InternalThreadCommand):
|
|||
def do_it(self, dbg):
|
||||
try:
|
||||
frame = dbg.find_frame(self.thread_id, self.frame_id)
|
||||
var = pydevd_vars.eval_in_context(self.name, frame.f_globals, frame.f_locals)
|
||||
var = pydevd_vars.eval_in_context(self.name, frame.f_globals, frame.f_locals, py_db=dbg)
|
||||
xml = pydevd_vars.table_like_struct_to_xml(var, self.name, self.roffset, self.coffset, self.rows, self.cols, self.format)
|
||||
cmd = dbg.cmd_factory.make_get_array_message(self.sequence, xml)
|
||||
dbg.writer.add_command(cmd)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@
|
|||
'''
|
||||
import sys
|
||||
import traceback
|
||||
from code import InteractiveConsole
|
||||
|
||||
from _pydevd_bundle.pydevconsole_code import InteractiveConsole, _EvalAwaitInNewEventLoop
|
||||
from _pydev_bundle import _pydev_completer
|
||||
from _pydev_bundle.pydev_console_utils import BaseInterpreterInterface, BaseStdIn
|
||||
from _pydev_bundle.pydev_imports import Exec
|
||||
|
|
@ -12,6 +11,8 @@ from _pydevd_bundle import pydevd_save_locals
|
|||
from _pydevd_bundle.pydevd_io import IOBuf
|
||||
from pydevd_tracing import get_exception_traceback_str
|
||||
from _pydevd_bundle.pydevd_xml import make_valid_xml_value
|
||||
import inspect
|
||||
from _pydevd_bundle.pydevd_save_locals import update_globals_and_locals
|
||||
|
||||
CONSOLE_OUTPUT = "output"
|
||||
CONSOLE_ERROR = "error"
|
||||
|
|
@ -152,8 +153,29 @@ class DebugConsole(InteractiveConsole, BaseInterpreterInterface):
|
|||
|
||||
"""
|
||||
try:
|
||||
Exec(code, self.frame.f_globals, self.frame.f_locals)
|
||||
pydevd_save_locals.save_locals(self.frame)
|
||||
updated_globals = self.get_namespace()
|
||||
initial_globals = updated_globals.copy()
|
||||
|
||||
updated_locals = None
|
||||
|
||||
is_async = False
|
||||
if hasattr(inspect, 'CO_COROUTINE'):
|
||||
is_async = inspect.CO_COROUTINE & code.co_flags == inspect.CO_COROUTINE
|
||||
|
||||
if is_async:
|
||||
t = _EvalAwaitInNewEventLoop(code, updated_globals, updated_locals)
|
||||
t.start()
|
||||
t.join()
|
||||
|
||||
update_globals_and_locals(updated_globals, initial_globals, self.frame)
|
||||
if t.exc:
|
||||
raise t.exc[1].with_traceback(t.exc[2])
|
||||
|
||||
else:
|
||||
try:
|
||||
exec(code, updated_globals, updated_locals)
|
||||
finally:
|
||||
update_globals_and_locals(updated_globals, initial_globals, self.frame)
|
||||
except SystemExit:
|
||||
raise
|
||||
except:
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ DONT_TRACE = {
|
|||
'pydev_umd.py': PYDEV_FILE,
|
||||
'pydev_versioncheck.py': PYDEV_FILE,
|
||||
'pydevconsole.py': PYDEV_FILE,
|
||||
'pydevconsole_code_for_ironpython.py': PYDEV_FILE,
|
||||
'pydevconsole_code.py': PYDEV_FILE,
|
||||
'pydevd.py': PYDEV_FILE,
|
||||
'pydevd_additional_thread_info.py': PYDEV_FILE,
|
||||
'pydevd_additional_thread_info_regular.py': PYDEV_FILE,
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ def make_save_locals_impl():
|
|||
pass
|
||||
else:
|
||||
if '__pypy__' in sys.builtin_module_names:
|
||||
|
||||
def save_locals_pypy_impl(frame):
|
||||
save_locals(frame)
|
||||
|
||||
|
|
@ -58,6 +59,7 @@ def make_save_locals_impl():
|
|||
except:
|
||||
pass
|
||||
else:
|
||||
|
||||
def save_locals_ctypes_impl(frame):
|
||||
locals_to_fast(ctypes.py_object(frame), ctypes.c_int(0))
|
||||
|
||||
|
|
@ -67,3 +69,28 @@ def make_save_locals_impl():
|
|||
|
||||
|
||||
save_locals_impl = make_save_locals_impl()
|
||||
|
||||
|
||||
def update_globals_and_locals(updated_globals, initial_globals, frame):
|
||||
# We don't have the locals and passed all in globals, so, we have to
|
||||
# manually choose how to update the variables.
|
||||
#
|
||||
# Note that the current implementation is a bit tricky: it does work in general
|
||||
# but if we do something as 'some_var = 10' and 'some_var' is already defined to have
|
||||
# the value '10' in the globals, we won't actually put that value in the locals
|
||||
# (which means that the frame locals won't be updated).
|
||||
# Still, the approach to have a single namespace was chosen because it was the only
|
||||
# one that enabled creating and using variables during the same evaluation.
|
||||
assert updated_globals is not None
|
||||
f_locals = None
|
||||
for key, val in updated_globals.items():
|
||||
if initial_globals.get(key) is not val:
|
||||
if f_locals is None:
|
||||
# Note: we call f_locals only once because each time
|
||||
# we call it the values may be reset.
|
||||
f_locals = frame.f_locals
|
||||
|
||||
f_locals[key] = val
|
||||
|
||||
if f_locals is not None:
|
||||
save_locals(frame)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"""
|
||||
import pickle
|
||||
from _pydevd_bundle.pydevd_constants import get_frame, get_current_thread_id, \
|
||||
iter_chars, silence_warnings_decorator
|
||||
iter_chars, silence_warnings_decorator, get_global_debugger
|
||||
|
||||
from _pydevd_bundle.pydevd_xml import ExceptionOnEvaluate, get_type, var_to_xml
|
||||
from _pydev_bundle import pydev_log
|
||||
|
|
@ -17,6 +17,10 @@ from _pydev_bundle._pydev_saved_modules import threading
|
|||
from _pydevd_bundle import pydevd_save_locals, pydevd_timeout, pydevd_constants
|
||||
from _pydev_bundle.pydev_imports import Exec, execfile
|
||||
from _pydevd_bundle.pydevd_utils import to_string
|
||||
import inspect
|
||||
from _pydevd_bundle.pydevd_daemon_thread import PyDBDaemonThread
|
||||
from _pydevd_bundle.pydevd_save_locals import update_globals_and_locals
|
||||
from functools import lru_cache
|
||||
|
||||
SENTINEL_VALUE = []
|
||||
|
||||
|
|
@ -203,6 +207,7 @@ def custom_operation(dbg, thread_id, frame_id, scope, attrs, style, code_or_file
|
|||
pydev_log.exception()
|
||||
|
||||
|
||||
@lru_cache(3)
|
||||
def _expression_to_evaluate(expression):
|
||||
keepends = True
|
||||
lines = expression.splitlines(keepends)
|
||||
|
|
@ -240,13 +245,27 @@ def _expression_to_evaluate(expression):
|
|||
return expression
|
||||
|
||||
|
||||
def eval_in_context(expression, globals, locals=None):
|
||||
def eval_in_context(expression, global_vars, local_vars, py_db=None):
|
||||
result = None
|
||||
try:
|
||||
if locals is None:
|
||||
result = eval(_expression_to_evaluate(expression), globals)
|
||||
compiled = compile_as_eval(expression)
|
||||
is_async = inspect.CO_COROUTINE & compiled.co_flags == inspect.CO_COROUTINE
|
||||
|
||||
if is_async:
|
||||
if py_db is None:
|
||||
py_db = get_global_debugger()
|
||||
if py_db is None:
|
||||
raise RuntimeError('Cannot evaluate async without py_db.')
|
||||
t = _EvalAwaitInNewEventLoop(py_db, compiled, global_vars, local_vars)
|
||||
t.start()
|
||||
t.join()
|
||||
|
||||
if t.exc:
|
||||
raise t.exc[1].with_traceback(t.exc[2])
|
||||
else:
|
||||
result = t.evaluated_value
|
||||
else:
|
||||
result = eval(_expression_to_evaluate(expression), globals, locals)
|
||||
result = eval(compiled, global_vars, local_vars)
|
||||
except (Exception, KeyboardInterrupt):
|
||||
etype, result, tb = sys.exc_info()
|
||||
result = ExceptionOnEvaluate(result, etype, tb)
|
||||
|
|
@ -258,9 +277,9 @@ def eval_in_context(expression, globals, locals=None):
|
|||
split = expression.split('.')
|
||||
entry = split[0]
|
||||
|
||||
if locals is None:
|
||||
locals = globals
|
||||
curr = locals[entry] # Note: we want the KeyError if it's not there.
|
||||
if local_vars is None:
|
||||
local_vars = global_vars
|
||||
curr = local_vars[entry] # Note: we want the KeyError if it's not there.
|
||||
for entry in split[1:]:
|
||||
if entry.startswith('__') and not hasattr(curr, entry):
|
||||
entry = '_%s%s' % (curr.__class__.__name__, entry)
|
||||
|
|
@ -353,60 +372,117 @@ def _evaluate_with_timeouts(original_func):
|
|||
return new_func
|
||||
|
||||
|
||||
_ASYNC_COMPILE_FLAGS = None
|
||||
try:
|
||||
from ast import PyCF_ALLOW_TOP_LEVEL_AWAIT
|
||||
_ASYNC_COMPILE_FLAGS = PyCF_ALLOW_TOP_LEVEL_AWAIT
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def compile_as_eval(expression):
|
||||
'''
|
||||
|
||||
:param expression:
|
||||
The expression to be compiled.
|
||||
The expression to be _compiled.
|
||||
|
||||
:return: code object
|
||||
|
||||
:raises Exception if the expression cannot be evaluated.
|
||||
'''
|
||||
return compile(_expression_to_evaluate(expression), '<string>', 'eval')
|
||||
expression_to_evaluate = _expression_to_evaluate(expression)
|
||||
if _ASYNC_COMPILE_FLAGS is not None:
|
||||
return compile(expression_to_evaluate, '<string>', 'eval', _ASYNC_COMPILE_FLAGS)
|
||||
else:
|
||||
return compile(expression_to_evaluate, '<string>', 'eval')
|
||||
|
||||
|
||||
def _update_globals_and_locals(updated_globals, initial_globals, frame):
|
||||
# We don't have the locals and passed all in globals, so, we have to
|
||||
# manually choose how to update the variables.
|
||||
#
|
||||
# Note that the current implementation is a bit tricky: it does work in general
|
||||
# but if we do something as 'some_var = 10' and 'some_var' is already defined to have
|
||||
# the value '10' in the globals, we won't actually put that value in the locals
|
||||
# (which means that the frame locals won't be updated).
|
||||
# Still, the approach to have a single namespace was chosen because it was the only
|
||||
# one that enabled creating and using variables during the same evaluation.
|
||||
assert updated_globals is not None
|
||||
f_locals = None
|
||||
for key, val in updated_globals.items():
|
||||
if initial_globals.get(key) is not val:
|
||||
if f_locals is None:
|
||||
# Note: we call f_locals only once because each time
|
||||
# we call it the values may be reset.
|
||||
f_locals = frame.f_locals
|
||||
def _compile_as_exec(expression):
|
||||
'''
|
||||
|
||||
f_locals[key] = val
|
||||
:param expression:
|
||||
The expression to be _compiled.
|
||||
|
||||
if f_locals is not None:
|
||||
pydevd_save_locals.save_locals(frame)
|
||||
:return: code object
|
||||
|
||||
:raises Exception if the expression cannot be evaluated.
|
||||
'''
|
||||
expression_to_evaluate = _expression_to_evaluate(expression)
|
||||
if _ASYNC_COMPILE_FLAGS is not None:
|
||||
return compile(expression_to_evaluate, '<string>', 'exec', _ASYNC_COMPILE_FLAGS)
|
||||
else:
|
||||
return compile(expression_to_evaluate, '<string>', 'exec')
|
||||
|
||||
|
||||
class _EvalAwaitInNewEventLoop(PyDBDaemonThread):
|
||||
|
||||
def __init__(self, py_db, compiled, updated_globals, updated_locals):
|
||||
PyDBDaemonThread.__init__(self, py_db)
|
||||
self._compiled = compiled
|
||||
self._updated_globals = updated_globals
|
||||
self._updated_locals = updated_locals
|
||||
|
||||
# Output
|
||||
self.evaluated_value = None
|
||||
self.exc = None
|
||||
|
||||
async def _async_func(self):
|
||||
return await eval(self._compiled, self._updated_locals, self._updated_globals)
|
||||
|
||||
def _on_run(self):
|
||||
try:
|
||||
import asyncio
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
self.evaluated_value = asyncio.run(self._async_func())
|
||||
except:
|
||||
self.exc = sys.exc_info()
|
||||
|
||||
|
||||
@_evaluate_with_timeouts
|
||||
def evaluate_expression(py_db, frame, expression, is_exec):
|
||||
'''
|
||||
There are some changes in this function depending on whether it's an exec or an eval.
|
||||
:param str expression:
|
||||
The expression to be evaluated.
|
||||
|
||||
When it's an exec (i.e.: is_exec==True):
|
||||
This function returns None.
|
||||
Any exception that happens during the evaluation is reraised.
|
||||
If the expression could actually be evaluated, the variable is printed to the console if not None.
|
||||
Note that if the expression is indented it's automatically dedented (based on the indentation
|
||||
found on the first non-empty line).
|
||||
|
||||
When it's an eval (i.e.: is_exec==False):
|
||||
This function returns the result from the evaluation.
|
||||
If some exception happens in this case, the exception is caught and a ExceptionOnEvaluate is returned.
|
||||
Also, in this case we try to resolve name-mangling (i.e.: to be able to add a self.__my_var watch).
|
||||
i.e.: something as:
|
||||
|
||||
`
|
||||
def method():
|
||||
a = 1
|
||||
`
|
||||
|
||||
becomes:
|
||||
|
||||
`
|
||||
def method():
|
||||
a = 1
|
||||
`
|
||||
|
||||
Also, it's possible to evaluate calls with a top-level await (currently this is done by
|
||||
creating a new event loop in a new thread and making the evaluate at that thread -- note
|
||||
that this is still done synchronously so the evaluation has to finish before this
|
||||
function returns).
|
||||
|
||||
:param is_exec: determines if we should do an exec or an eval.
|
||||
There are some changes in this function depending on whether it's an exec or an eval.
|
||||
|
||||
When it's an exec (i.e.: is_exec==True):
|
||||
This function returns None.
|
||||
Any exception that happens during the evaluation is reraised.
|
||||
If the expression could actually be evaluated, the variable is printed to the console if not None.
|
||||
|
||||
When it's an eval (i.e.: is_exec==False):
|
||||
This function returns the result from the evaluation.
|
||||
If some exception happens in this case, the exception is caught and a ExceptionOnEvaluate is returned.
|
||||
Also, in this case we try to resolve name-mangling (i.e.: to be able to add a self.__my_var watch).
|
||||
|
||||
:param py_db:
|
||||
The debugger. Only needed if some top-level await is detected (for creating a
|
||||
PyDBDaemonThread).
|
||||
'''
|
||||
if frame is None:
|
||||
return
|
||||
|
|
@ -457,7 +533,7 @@ def evaluate_expression(py_db, frame, expression, is_exec):
|
|||
|
||||
if is_exec:
|
||||
try:
|
||||
# try to make it an eval (if it is an eval we can print it, otherwise we'll exec it and
|
||||
# Try to make it an eval (if it is an eval we can print it, otherwise we'll exec it and
|
||||
# it will have whatever the user actually did)
|
||||
compiled = compile_as_eval(expression)
|
||||
except Exception:
|
||||
|
|
@ -465,18 +541,39 @@ def evaluate_expression(py_db, frame, expression, is_exec):
|
|||
|
||||
if compiled is None:
|
||||
try:
|
||||
Exec(_expression_to_evaluate(expression), updated_globals, updated_locals)
|
||||
compiled = _compile_as_exec(expression)
|
||||
is_async = inspect.CO_COROUTINE & compiled.co_flags == inspect.CO_COROUTINE
|
||||
if is_async:
|
||||
t = _EvalAwaitInNewEventLoop(py_db, compiled, updated_globals, updated_locals)
|
||||
t.start()
|
||||
t.join()
|
||||
|
||||
if t.exc:
|
||||
raise t.exc[1].with_traceback(t.exc[2])
|
||||
else:
|
||||
Exec(compiled, updated_globals, updated_locals)
|
||||
finally:
|
||||
# Update the globals even if it errored as it may have partially worked.
|
||||
_update_globals_and_locals(updated_globals, initial_globals, frame)
|
||||
update_globals_and_locals(updated_globals, initial_globals, frame)
|
||||
else:
|
||||
result = eval(compiled, updated_globals, updated_locals)
|
||||
is_async = inspect.CO_COROUTINE & compiled.co_flags == inspect.CO_COROUTINE
|
||||
if is_async:
|
||||
t = _EvalAwaitInNewEventLoop(py_db, compiled, updated_globals, updated_locals)
|
||||
t.start()
|
||||
t.join()
|
||||
|
||||
if t.exc:
|
||||
raise t.exc[1].with_traceback(t.exc[2])
|
||||
else:
|
||||
result = t.evaluated_value
|
||||
else:
|
||||
result = eval(compiled, updated_globals, updated_locals)
|
||||
if result is not None: # Only print if it's not None (as python does)
|
||||
sys.stdout.write('%s\n' % (result,))
|
||||
return
|
||||
|
||||
else:
|
||||
ret = eval_in_context(expression, updated_globals, updated_locals)
|
||||
ret = eval_in_context(expression, updated_globals, updated_locals, py_db)
|
||||
try:
|
||||
is_exception_returned = ret.__class__ == ExceptionOnEvaluate
|
||||
except:
|
||||
|
|
@ -485,7 +582,7 @@ def evaluate_expression(py_db, frame, expression, is_exec):
|
|||
if not is_exception_returned:
|
||||
# i.e.: by using a walrus assignment (:=), expressions can change the locals,
|
||||
# so, make sure that we save the locals back to the frame.
|
||||
_update_globals_and_locals(updated_globals, initial_globals, frame)
|
||||
update_globals_and_locals(updated_globals, initial_globals, frame)
|
||||
return ret
|
||||
finally:
|
||||
# Should not be kept alive if an exception happens and this frame is kept in the stack.
|
||||
|
|
|
|||
|
|
@ -5,10 +5,7 @@ from _pydev_bundle._pydev_saved_modules import thread, _code
|
|||
from _pydevd_bundle.pydevd_constants import IS_JYTHON
|
||||
start_new_thread = thread.start_new_thread
|
||||
|
||||
try:
|
||||
from code import InteractiveConsole
|
||||
except ImportError:
|
||||
from _pydevd_bundle.pydevconsole_code_for_ironpython import InteractiveConsole
|
||||
from _pydevd_bundle.pydevconsole_code import InteractiveConsole
|
||||
|
||||
compile_command = _code.compile_command
|
||||
InteractiveInterpreter = _code.InteractiveInterpreter
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ import sys
|
|||
import pydevconsole
|
||||
from _pydev_bundle.pydev_imports import xmlrpclib, SimpleXMLRPCServer
|
||||
from _pydevd_bundle import pydevd_io
|
||||
from contextlib import contextmanager
|
||||
import pytest
|
||||
|
||||
try:
|
||||
raw_input
|
||||
raw_input_name = 'raw_input'
|
||||
except NameError:
|
||||
raw_input_name = 'input'
|
||||
from ast import PyCF_ALLOW_TOP_LEVEL_AWAIT # @UnusedImport
|
||||
CAN_EVALUATE_TOP_LEVEL_ASYNC = True
|
||||
except:
|
||||
CAN_EVALUATE_TOP_LEVEL_ASYNC = False
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
|
|
@ -17,11 +19,15 @@ except NameError:
|
|||
#=======================================================================================================================
|
||||
class Test(unittest.TestCase):
|
||||
|
||||
def test_console_hello(self):
|
||||
@contextmanager
|
||||
def interpreter(self):
|
||||
self.original_stdout = sys.stdout
|
||||
self.original_stderr = sys.stderr
|
||||
sys.stdout = pydevd_io.IOBuf()
|
||||
sys.stderr = pydevd_io.IOBuf()
|
||||
try:
|
||||
sys.stdout.encoding = sys.stdin.encoding
|
||||
sys.stderr.encoding = sys.stdin.encoding
|
||||
except AttributeError:
|
||||
# In Python 3 encoding is not writable (whereas in Python 2 it doesn't exist).
|
||||
pass
|
||||
|
|
@ -34,31 +40,50 @@ class Test(unittest.TestCase):
|
|||
|
||||
from _pydev_bundle import pydev_localhost
|
||||
interpreter = pydevconsole.InterpreterInterface(pydev_localhost.get_localhost(), client_port, threading.current_thread())
|
||||
|
||||
(result,) = interpreter.hello("Hello pydevconsole")
|
||||
self.assertEqual(result, "Hello eclipse")
|
||||
yield interpreter
|
||||
except:
|
||||
# if there's some error, print the output to the actual output.
|
||||
self.original_stdout.write(sys.stdout.getvalue())
|
||||
self.original_stderr.write(sys.stderr.getvalue())
|
||||
raise
|
||||
finally:
|
||||
sys.stderr = self.original_stderr
|
||||
sys.stdout = self.original_stdout
|
||||
|
||||
def test_console_requests(self):
|
||||
self.original_stdout = sys.stdout
|
||||
sys.stdout = pydevd_io.IOBuf()
|
||||
def test_console_hello(self):
|
||||
with self.interpreter() as interpreter:
|
||||
(result,) = interpreter.hello("Hello pydevconsole")
|
||||
self.assertEqual(result, "Hello eclipse")
|
||||
|
||||
try:
|
||||
client_port, _server_port = self.get_free_addresses()
|
||||
client_thread = self.start_client_thread(client_port) # @UnusedVariable
|
||||
import time
|
||||
time.sleep(.3) # let's give it some time to start the threads
|
||||
|
||||
from _pydev_bundle import pydev_localhost
|
||||
@pytest.mark.skipif(not CAN_EVALUATE_TOP_LEVEL_ASYNC, reason='Requires top-level async.')
|
||||
def test_console_async(self):
|
||||
with self.interpreter() as interpreter:
|
||||
from _pydev_bundle.pydev_console_utils import CodeFragment
|
||||
more = interpreter.add_exec(CodeFragment('''
|
||||
async def async_func(a):
|
||||
return a
|
||||
'''))
|
||||
assert not more
|
||||
assert not sys.stderr.getvalue()
|
||||
assert not sys.stdout.getvalue()
|
||||
|
||||
interpreter = pydevconsole.InterpreterInterface(pydev_localhost.get_localhost(), client_port, threading.current_thread())
|
||||
sys.stdout = pydevd_io.IOBuf()
|
||||
more = interpreter.add_exec(CodeFragment('''x = await async_func(1111)'''))
|
||||
assert not more
|
||||
assert not sys.stderr.getvalue()
|
||||
assert not sys.stdout.getvalue()
|
||||
|
||||
more = interpreter.add_exec(CodeFragment('''print(x)'''))
|
||||
assert not more
|
||||
assert not sys.stderr.getvalue()
|
||||
assert '1111' in sys.stdout.getvalue()
|
||||
|
||||
def test_console_requests(self):
|
||||
with self.interpreter() as interpreter:
|
||||
from _pydev_bundle.pydev_console_utils import CodeFragment
|
||||
interpreter.add_exec(CodeFragment('class Foo:\n CONSTANT=1\n'))
|
||||
interpreter.add_exec(CodeFragment('foo=Foo()'))
|
||||
interpreter.add_exec(CodeFragment('foo.__doc__=None'))
|
||||
interpreter.add_exec(CodeFragment('val = %s()' % (raw_input_name,)))
|
||||
interpreter.add_exec(CodeFragment('val = input()'))
|
||||
interpreter.add_exec(CodeFragment('50'))
|
||||
interpreter.add_exec(CodeFragment('print (val)'))
|
||||
found = sys.stdout.getvalue().split()
|
||||
|
|
@ -129,8 +154,6 @@ class Test(unittest.TestCase):
|
|||
desc.find('Concatenate any number of strings.') >= 0 or
|
||||
desc.find('bound method str.join') >= 0, # PyPy
|
||||
"Could not recognize: %s" % (desc,))
|
||||
finally:
|
||||
sys.stdout = self.original_stdout
|
||||
|
||||
def start_client_thread(self, client_port):
|
||||
|
||||
|
|
@ -234,7 +257,7 @@ class Test(unittest.TestCase):
|
|||
server.execLine(' pass')
|
||||
server.execLine('')
|
||||
server.execLine('foo = Foo()')
|
||||
server.execLine('a = %s()' % (raw_input_name,))
|
||||
server.execLine('a = input()')
|
||||
server.execLine('print (a)')
|
||||
initial = time.time()
|
||||
while not client_thread.requested_input:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
from _pydevd_bundle.pydevd_constants import IS_PY38_OR_GREATER, NULL
|
||||
from _pydevd_bundle.pydevd_xml import ExceptionOnEvaluate
|
||||
|
||||
import sys
|
||||
from _pydevd_bundle.pydevd_constants import IS_PY38_OR_GREATER
|
||||
import pytest
|
||||
|
||||
SOME_LST = ["foo", "bar"]
|
||||
|
|
@ -130,3 +132,149 @@ def test_evaluate_expression_5(disable_critical_log):
|
|||
assert frame.f_locals['B'] == 6
|
||||
|
||||
check(next(iter(obtain_frame())))
|
||||
|
||||
|
||||
class _DummyPyDB(object):
|
||||
|
||||
def __init__(self):
|
||||
self.created_pydb_daemon_threads = {}
|
||||
self.timeout_tracker = NULL
|
||||
self.multi_threads_single_notification = False
|
||||
|
||||
|
||||
try:
|
||||
from ast import PyCF_ALLOW_TOP_LEVEL_AWAIT # @UnusedImport
|
||||
CAN_EVALUATE_TOP_LEVEL_ASYNC = True
|
||||
except:
|
||||
CAN_EVALUATE_TOP_LEVEL_ASYNC = False
|
||||
|
||||
|
||||
@pytest.mark.skipif(not CAN_EVALUATE_TOP_LEVEL_ASYNC, reason='Requires top-level async evaluation.')
|
||||
def test_evaluate_expression_async_exec(disable_critical_log):
|
||||
py_db = _DummyPyDB()
|
||||
|
||||
async def async_call(a):
|
||||
return a
|
||||
|
||||
async def main():
|
||||
from _pydevd_bundle.pydevd_vars import evaluate_expression
|
||||
a = 10
|
||||
assert async_call is not None # Make sure it's in the locals.
|
||||
frame = sys._getframe()
|
||||
eval_txt = 'y = await async_call(a)'
|
||||
evaluate_expression(py_db, frame, eval_txt, is_exec=True)
|
||||
assert frame.f_locals['y'] == a
|
||||
|
||||
import asyncio
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
@pytest.mark.skipif(not CAN_EVALUATE_TOP_LEVEL_ASYNC, reason='Requires top-level async evaluation.')
|
||||
def test_evaluate_expression_async_exec_as_eval(disable_critical_log):
|
||||
py_db = _DummyPyDB()
|
||||
|
||||
async def async_call(a):
|
||||
return a
|
||||
|
||||
async def main():
|
||||
from _pydevd_bundle.pydevd_vars import evaluate_expression
|
||||
assert async_call is not None # Make sure it's in the locals.
|
||||
frame = sys._getframe()
|
||||
eval_txt = 'await async_call(10)'
|
||||
from io import StringIO
|
||||
_original_stdout = sys.stdout
|
||||
try:
|
||||
stringio = sys.stdout = StringIO()
|
||||
evaluate_expression(py_db, frame, eval_txt, is_exec=True)
|
||||
finally:
|
||||
sys.stdout = _original_stdout
|
||||
|
||||
# I.e.: Check that we printed the value obtained in the exec.
|
||||
assert '10\n' in stringio.getvalue()
|
||||
|
||||
import asyncio
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
@pytest.mark.skipif(not CAN_EVALUATE_TOP_LEVEL_ASYNC, reason='Requires top-level async evaluation.')
|
||||
def test_evaluate_expression_async_exec_error(disable_critical_log):
|
||||
py_db = _DummyPyDB()
|
||||
|
||||
async def async_call(a):
|
||||
raise RuntimeError('foobar')
|
||||
|
||||
async def main():
|
||||
from _pydevd_bundle.pydevd_vars import evaluate_expression
|
||||
assert async_call is not None # Make sure it's in the locals.
|
||||
frame = sys._getframe()
|
||||
eval_txt = 'y = await async_call(10)'
|
||||
with pytest.raises(RuntimeError) as e:
|
||||
evaluate_expression(py_db, frame, eval_txt, is_exec=True)
|
||||
assert 'foobar' in str(e)
|
||||
assert 'y' not in frame.f_locals
|
||||
|
||||
import asyncio
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
@pytest.mark.skipif(not CAN_EVALUATE_TOP_LEVEL_ASYNC, reason='Requires top-level async evaluation.')
|
||||
def test_evaluate_expression_async_eval(disable_critical_log):
|
||||
py_db = _DummyPyDB()
|
||||
|
||||
async def async_call(a):
|
||||
return a
|
||||
|
||||
async def main():
|
||||
from _pydevd_bundle.pydevd_vars import evaluate_expression
|
||||
a = 10
|
||||
assert async_call is not None # Make sure it's in the locals.
|
||||
frame = sys._getframe()
|
||||
eval_txt = 'await async_call(a)'
|
||||
v = evaluate_expression(py_db, frame, eval_txt, is_exec=False)
|
||||
if isinstance(v, ExceptionOnEvaluate):
|
||||
raise v.result.with_traceback(v.tb)
|
||||
assert v == a
|
||||
|
||||
import asyncio
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
@pytest.mark.skipif(not CAN_EVALUATE_TOP_LEVEL_ASYNC, reason='Requires top-level async evaluation.')
|
||||
def test_evaluate_expression_async_eval_error(disable_critical_log):
|
||||
py_db = _DummyPyDB()
|
||||
|
||||
async def async_call(a):
|
||||
raise RuntimeError('foobar')
|
||||
|
||||
async def main():
|
||||
from _pydevd_bundle.pydevd_vars import evaluate_expression
|
||||
a = 10
|
||||
assert async_call is not None # Make sure it's in the locals.
|
||||
frame = sys._getframe()
|
||||
eval_txt = 'await async_call(a)'
|
||||
v = evaluate_expression(py_db, frame, eval_txt, is_exec=False)
|
||||
assert isinstance(v, ExceptionOnEvaluate)
|
||||
assert 'foobar' in str(v.result)
|
||||
|
||||
import asyncio
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
def test_evaluate_expression_name_mangling(disable_critical_log):
|
||||
from _pydevd_bundle.pydevd_vars import evaluate_expression
|
||||
|
||||
class SomeObj(object):
|
||||
|
||||
def __init__(self):
|
||||
self.__value = 10
|
||||
self.frame = sys._getframe()
|
||||
|
||||
obj = SomeObj()
|
||||
frame = obj.frame
|
||||
|
||||
eval_txt = '''self.__value'''
|
||||
v = evaluate_expression(None, frame, eval_txt, is_exec=False)
|
||||
if isinstance(v, ExceptionOnEvaluate):
|
||||
raise v.result.with_traceback(v.tb)
|
||||
|
||||
assert v == 10
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue