mirror of
https://github.com/microsoft/debugpy.git
synced 2025-12-23 08:48:12 +00:00
Deal with __future__ imports on python -c subprocess. Fixes #642
This commit is contained in:
parent
8d90ef2a45
commit
02477b5ac3
2 changed files with 171 additions and 3 deletions
|
|
@ -9,6 +9,7 @@ from _pydev_bundle import pydev_log
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from _pydevd_bundle import pydevd_constants
|
from _pydevd_bundle import pydevd_constants
|
||||||
from _pydevd_bundle.pydevd_defaults import PydevdCustomization
|
from _pydevd_bundle.pydevd_defaults import PydevdCustomization
|
||||||
|
import ast
|
||||||
|
|
||||||
try:
|
try:
|
||||||
xrange
|
xrange
|
||||||
|
|
@ -75,16 +76,116 @@ def _get_setup_updated_with_protocol_and_ppid(setup, is_exec=False):
|
||||||
return setup
|
return setup
|
||||||
|
|
||||||
|
|
||||||
|
class _LastFutureImportFinder(ast.NodeVisitor):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.last_future_import_found = None
|
||||||
|
|
||||||
|
def visit_ImportFrom(self, node):
|
||||||
|
if node.module == '__future__':
|
||||||
|
self.last_future_import_found = node
|
||||||
|
|
||||||
|
|
||||||
|
def _get_offset_from_line_col(code, line, col):
|
||||||
|
offset = 0
|
||||||
|
for i, line_contents in enumerate(code.splitlines(True)):
|
||||||
|
if i == line:
|
||||||
|
offset += col
|
||||||
|
return offset
|
||||||
|
else:
|
||||||
|
offset += len(line_contents)
|
||||||
|
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
|
def _separate_future_imports(code):
|
||||||
|
'''
|
||||||
|
:param code:
|
||||||
|
The code from where we want to get the __future__ imports (note that it's possible that
|
||||||
|
there's no such entry).
|
||||||
|
|
||||||
|
:return tuple(str, str):
|
||||||
|
The return is a tuple(future_import, code).
|
||||||
|
|
||||||
|
If the future import is not available a return such as ('', code) is given, otherwise, the
|
||||||
|
future import will end with a ';' (so that it can be put right before the pydevd attach
|
||||||
|
code).
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
node = ast.parse(code, '<string>', 'exec')
|
||||||
|
visitor = _LastFutureImportFinder()
|
||||||
|
visitor.visit(node)
|
||||||
|
|
||||||
|
if visitor.last_future_import_found is None:
|
||||||
|
return '', code
|
||||||
|
|
||||||
|
node = visitor.last_future_import_found
|
||||||
|
offset = -1
|
||||||
|
if hasattr(node, 'end_lineno') and hasattr(node, 'end_col_offset'):
|
||||||
|
# Python 3.8 onwards has these (so, use when possible).
|
||||||
|
line, col = node.end_lineno, node.end_col_offset
|
||||||
|
offset = _get_offset_from_line_col(code, line - 1, col) # ast lines are 1-based, make it 0-based.
|
||||||
|
|
||||||
|
else:
|
||||||
|
# end line/col not available, let's just find the offset and then search
|
||||||
|
# for the alias from there.
|
||||||
|
line, col = node.lineno, node.col_offset
|
||||||
|
offset = _get_offset_from_line_col(code, line - 1, col) # ast lines are 1-based, make it 0-based.
|
||||||
|
if offset >= 0 and node.names:
|
||||||
|
from_future_import_name = node.names[-1].name
|
||||||
|
i = code.find(from_future_import_name, offset)
|
||||||
|
if i < 0:
|
||||||
|
offset = -1
|
||||||
|
else:
|
||||||
|
offset = i + len(from_future_import_name)
|
||||||
|
|
||||||
|
if offset >= 0:
|
||||||
|
for i in range(offset, len(code)):
|
||||||
|
if code[i] in (' ', '\t', ';', ')', '\n'):
|
||||||
|
offset += 1
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
future_import = code[:offset]
|
||||||
|
code_remainder = code[offset:]
|
||||||
|
|
||||||
|
# Now, put '\n' lines back into the code remainder (we had to search for
|
||||||
|
# `\n)`, but in case we just got the `\n`, it should be at the remainder,
|
||||||
|
# not at the future import.
|
||||||
|
while future_import.endswith('\n'):
|
||||||
|
future_import = future_import[:-1]
|
||||||
|
code_remainder = '\n' + code_remainder
|
||||||
|
|
||||||
|
if not future_import.endswith(';'):
|
||||||
|
future_import += ';'
|
||||||
|
return future_import, code_remainder
|
||||||
|
|
||||||
|
# This shouldn't happen...
|
||||||
|
pydev_log.info('Unable to find line %s in code:\n%r', line, code)
|
||||||
|
return '', code
|
||||||
|
|
||||||
|
except:
|
||||||
|
pydev_log.exception('Error getting from __future__ imports from: %r', code)
|
||||||
|
return '', code
|
||||||
|
|
||||||
|
|
||||||
def _get_python_c_args(host, port, code, args, setup):
|
def _get_python_c_args(host, port, code, args, setup):
|
||||||
setup = _get_setup_updated_with_protocol_and_ppid(setup)
|
setup = _get_setup_updated_with_protocol_and_ppid(setup)
|
||||||
|
|
||||||
# i.e.: We want to make the repr sorted so that it works in tests.
|
# i.e.: We want to make the repr sorted so that it works in tests.
|
||||||
setup_repr = setup if setup is None else (sorted_dict_repr(setup))
|
setup_repr = setup if setup is None else (sorted_dict_repr(setup))
|
||||||
|
|
||||||
return ("import sys; sys.path.insert(0, r'%s'); import pydevd; pydevd.PydevdCustomization.DEFAULT_PROTOCOL=%r; "
|
future_imports = ''
|
||||||
|
if '__future__' in code:
|
||||||
|
# If the code has a __future__ import, we need to be able to strip the __future__
|
||||||
|
# imports from the code and add them to the start of our code snippet.
|
||||||
|
future_imports, code = _separate_future_imports(code)
|
||||||
|
|
||||||
|
return ("%simport sys; sys.path.insert(0, r'%s'); import pydevd; pydevd.PydevdCustomization.DEFAULT_PROTOCOL=%r; "
|
||||||
"pydevd.settrace(host=%r, port=%s, suspend=False, trace_only_current_thread=False, patch_multiprocessing=True, access_token=%r, client_access_token=%r, __setup_holder__=%s); "
|
"pydevd.settrace(host=%r, port=%s, suspend=False, trace_only_current_thread=False, patch_multiprocessing=True, access_token=%r, client_access_token=%r, __setup_holder__=%s); "
|
||||||
"%s"
|
"%s"
|
||||||
) % (
|
) % (
|
||||||
|
future_imports,
|
||||||
pydev_src_dir,
|
pydev_src_dir,
|
||||||
pydevd_constants.get_protocol(),
|
pydevd_constants.get_protocol(),
|
||||||
host,
|
host,
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,73 @@ def test_monkey_patch_args_indc():
|
||||||
SetupHolder.setup = original
|
SetupHolder.setup = original
|
||||||
|
|
||||||
|
|
||||||
|
def test_separate_future_imports():
|
||||||
|
found = pydev_monkey._separate_future_imports('''from __future__ import print_function\nprint(1)''')
|
||||||
|
assert found == ('from __future__ import print_function;', '\nprint(1)')
|
||||||
|
|
||||||
|
found = pydev_monkey._separate_future_imports('''from __future__ import print_function;print(1)''')
|
||||||
|
assert found == ('from __future__ import print_function;', 'print(1)')
|
||||||
|
|
||||||
|
found = pydev_monkey._separate_future_imports('''from __future__ import (\nprint_function);print(1)''')
|
||||||
|
assert found == ('from __future__ import (\nprint_function);', 'print(1)')
|
||||||
|
|
||||||
|
found = pydev_monkey._separate_future_imports('''"line";from __future__ import (\n\nprint_function, absolute_imports\n);print(1)''')
|
||||||
|
assert found == ('"line";from __future__ import (\n\nprint_function, absolute_imports\n);', 'print(1)')
|
||||||
|
|
||||||
|
found = pydev_monkey._separate_future_imports('''from __future__ import bar\nfrom __future__ import (\n\nprint_function, absolute_imports\n);print(1)''')
|
||||||
|
assert found == ('from __future__ import bar\nfrom __future__ import (\n\nprint_function, absolute_imports\n);', 'print(1)')
|
||||||
|
|
||||||
|
|
||||||
|
def test_monkey_patch_args_indc_future_import():
|
||||||
|
original = SetupHolder.setup
|
||||||
|
|
||||||
|
try:
|
||||||
|
SetupHolder.setup = {'client': '127.0.0.1', 'port': '0', 'ppid': os.getpid(), 'protocol-quoted-line': True, 'skip-notify-stdin': True}
|
||||||
|
check = ['C:\\bin\\python.exe', '-u', '-c', 'from __future__ import print_function;connect("127.0.0.1")']
|
||||||
|
debug_command = (
|
||||||
|
"from __future__ import print_function;import sys; sys.path.insert(0, r\'%s\'); import pydevd; pydevd.PydevdCustomization.DEFAULT_PROTOCOL='quoted-line'; "
|
||||||
|
'pydevd.settrace(host=\'127.0.0.1\', port=0, suspend=False, trace_only_current_thread=False, patch_multiprocessing=True, access_token=None, client_access_token=None, __setup_holder__=%s); '
|
||||||
|
''
|
||||||
|
'connect("127.0.0.1")') % (pydev_src_dir, sorted_dict_repr(SetupHolder.setup))
|
||||||
|
if sys.platform == "win32":
|
||||||
|
debug_command = debug_command.replace('"', '\\"')
|
||||||
|
debug_command = '"%s"' % debug_command
|
||||||
|
res = pydev_monkey.patch_args(check)
|
||||||
|
assert res == [
|
||||||
|
'C:\\bin\\python.exe',
|
||||||
|
'-u',
|
||||||
|
'-c',
|
||||||
|
debug_command
|
||||||
|
]
|
||||||
|
finally:
|
||||||
|
SetupHolder.setup = original
|
||||||
|
|
||||||
|
|
||||||
|
def test_monkey_patch_args_indc_future_import2():
|
||||||
|
original = SetupHolder.setup
|
||||||
|
|
||||||
|
try:
|
||||||
|
SetupHolder.setup = {'client': '127.0.0.1', 'port': '0', 'ppid': os.getpid(), 'protocol-quoted-line': True, 'skip-notify-stdin': True}
|
||||||
|
check = ['C:\\bin\\python.exe', '-u', '-c', 'from __future__ import print_function\nconnect("127.0.0.1")']
|
||||||
|
debug_command = (
|
||||||
|
"from __future__ import print_function;import sys; sys.path.insert(0, r\'%s\'); import pydevd; pydevd.PydevdCustomization.DEFAULT_PROTOCOL='quoted-line'; "
|
||||||
|
'pydevd.settrace(host=\'127.0.0.1\', port=0, suspend=False, trace_only_current_thread=False, patch_multiprocessing=True, access_token=None, client_access_token=None, __setup_holder__=%s); '
|
||||||
|
''
|
||||||
|
'\nconnect("127.0.0.1")') % (pydev_src_dir, sorted_dict_repr(SetupHolder.setup))
|
||||||
|
if sys.platform == "win32":
|
||||||
|
debug_command = debug_command.replace('"', '\\"')
|
||||||
|
debug_command = '"%s"' % debug_command
|
||||||
|
res = pydev_monkey.patch_args(check)
|
||||||
|
assert res == [
|
||||||
|
'C:\\bin\\python.exe',
|
||||||
|
'-u',
|
||||||
|
'-c',
|
||||||
|
debug_command
|
||||||
|
]
|
||||||
|
finally:
|
||||||
|
SetupHolder.setup = original
|
||||||
|
|
||||||
|
|
||||||
def test_monkey_patch_args_indc2():
|
def test_monkey_patch_args_indc2():
|
||||||
original = SetupHolder.setup
|
original = SetupHolder.setup
|
||||||
|
|
||||||
|
|
@ -495,7 +562,7 @@ def test_monkey_patch_c_program_arg(use_bytes):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
SetupHolder.setup = {'client': '127.0.0.1', 'port': '0'}
|
SetupHolder.setup = {'client': '127.0.0.1', 'port': '0'}
|
||||||
check = ['C:\\bin\\python.exe', '-u', 'target.py', '-c', '-áéíóú']
|
check = ['C:\\bin\\python.exe', '-u', 'target.py', '-c', '-áéíóú']
|
||||||
|
|
||||||
encode = lambda s:s
|
encode = lambda s:s
|
||||||
if use_bytes:
|
if use_bytes:
|
||||||
|
|
@ -521,7 +588,7 @@ def test_monkey_patch_c_program_arg(use_bytes):
|
||||||
'--file',
|
'--file',
|
||||||
encode('target.py'),
|
encode('target.py'),
|
||||||
encode('-c'),
|
encode('-c'),
|
||||||
encode('-áéíóú')
|
encode('-áéíóú')
|
||||||
]
|
]
|
||||||
finally:
|
finally:
|
||||||
SetupHolder.setup = original
|
SetupHolder.setup = original
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue