Skip to content

Commit

Permalink
Support top-level async. Fixes microsoft#951
Browse files Browse the repository at this point in the history
  • Loading branch information
fabioz committed Aug 11, 2022
1 parent 6e247fb commit c6e843e
Show file tree
Hide file tree
Showing 9 changed files with 512 additions and 144 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -197,19 +210,33 @@ def __call__(self, source, filename="<input>", symbol="single"):
__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:
Expand Down Expand Up @@ -240,7 +267,7 @@ def runsource(self, source, filename="<input>", symbol="single"):
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
Expand Down Expand Up @@ -287,14 +314,24 @@ def runcode(self, code):
"""
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.
Expand All @@ -308,45 +345,49 @@ def showsyntaxerror(self, filename=None):
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.
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.
Expand Down Expand Up @@ -384,45 +425,46 @@ def resetbuffer(self):
"""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.'
if banner is None:
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
Expand All @@ -432,6 +474,10 @@ def interact(self, banner=None):
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.
Expand Down Expand Up @@ -461,14 +507,14 @@ def raw_input(self, prompt=""):
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
Expand All @@ -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)
Expand All @@ -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 argparse

if __name__ == '__main__':
import pdb
pdb.run("interact()\n")
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)
2 changes: 1 addition & 1 deletion src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py
Original file line number Diff line number Diff line change
Expand Up @@ -833,7 +833,7 @@ def __init__(self, seq, roffset, coffset, rows, cols, format, thread_id, frame_i
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)
Expand Down
30 changes: 26 additions & 4 deletions src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -12,6 +11,8 @@
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"
Expand Down Expand Up @@ -152,8 +153,29 @@ def runcode(self, code):
"""
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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
'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,
Expand Down
Loading

0 comments on commit c6e843e

Please sign in to comment.