Skip to content
This repository has been archived by the owner on Aug 2, 2023. It is now read-only.

Commit

Permalink
Fixes #987 (nonetype), #1000 (completions), #1077 (<frozen>), #1072 (…
Browse files Browse the repository at this point in the history
…builtins has no interpreter) (#1081)

* Change completions command to Debug Adapter Protocol. Fixes #1000

* Fixed 'NoneType' object is not callable. Fixes #987
  • Loading branch information
fabioz authored and karthiknadig committed Jan 2, 2019
1 parent 2484f7b commit 5af4b61
Show file tree
Hide file tree
Showing 39 changed files with 10,368 additions and 13,370 deletions.
4 changes: 3 additions & 1 deletion pytests/func/test_attach.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ def test_attach(run_as, wait_for_attach, is_attached, break_into):
session.send_request('pause').wait_for_response(freeze=False)
hit = session.wait_for_thread_stopped(reason='pause')
frames = hit.stacktrace.body['stackFrames']
assert frames[0]['line'] in [27, 28, 29]
# Note: no longer asserting line as it can even stop on different files
# (such as as backchannel.py).
# assert frames[0]['line'] in [27, 28, 29]

session.send_request('continue').wait_for_response(freeze=False)
session.wait_for_exit()
Expand Down
6 changes: 5 additions & 1 deletion pytests/func/test_break_into_dbg.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

@pytest.mark.parametrize('run_as', ['file', 'module', 'code'])
def test_with_wait_for_attach(pyfile, run_as, start_method):

@pyfile
def code_to_debug():
# NOTE: These tests verify break_into_debugger for launch
Expand Down Expand Up @@ -40,6 +41,7 @@ def code_to_debug():
@pytest.mark.parametrize('run_as', ['file', 'module', 'code'])
@pytest.mark.skipif(sys.version_info < (3, 7), reason='Supported from 3.7+')
def test_breakpoint_function(pyfile, run_as, start_method):

@pyfile
def code_to_debug():
# NOTE: These tests verify break_into_debugger for launch
Expand All @@ -58,7 +60,9 @@ def code_to_debug():
session.start_debugging()
hit = session.wait_for_thread_stopped()
frames = hit.stacktrace.body['stackFrames']
assert frames[0]['line'] == 5
path = frames[0]['source']['path']
assert path.endswith('code_to_debug.py') or path.endswith('<string>')
assert frames[0]['line'] == 6

session.send_request('continue').wait_for_response(freeze=False)
session.wait_for_exit()
37 changes: 24 additions & 13 deletions pytests/func/test_completions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@

expected_at_line = {
8: [
{'label': 'SomeClass', 'type': 'class'},
{'label': 'someFunction', 'type': 'function'},
{'label': 'someVariable', 'type': 'field'},
{'label': 'SomeClass', 'type': 'class', 'start': 0, 'length': 4},
{'label': 'someFunction', 'type': 'function', 'start': 0, 'length': 4},
{'label': 'someVariable', 'type': 'field', 'start': 0, 'length': 4},
],
11: [
{'label': 'SomeClass', 'type': 'class'},
{'label': 'someFunction', 'type': 'function'},
{'label': 'someVar', 'type': 'field'},
{'label': 'someVariable', 'type': 'field'},
{'label': 'SomeClass', 'type': 'class', 'start': 0, 'length': 4},
{'label': 'someFunction', 'type': 'function', 'start': 0, 'length': 4},
{'label': 'someVar', 'type': 'field', 'start': 0, 'length': 4},
{'label': 'someVariable', 'type': 'field', 'start': 0, 'length': 4},
],
13: [
{'label': 'SomeClass', 'type': 'class'},
{'label': 'someFunction', 'type': 'function'},
{'label': 'SomeClass', 'type': 'class', 'start': 0, 'length': 4},
{'label': 'someFunction', 'type': 'function', 'start': 0, 'length': 4},
],
}

Expand Down Expand Up @@ -72,7 +72,7 @@ def someFunction(someVar):
resp_completions = session.send_request('completions', arguments={
'text': 'some',
'frameId': fid,
'column': 1
'column': 5,
}).wait_for_response()
targets = resp_completions.body['targets']

Expand All @@ -85,7 +85,7 @@ def someFunction(someVar):
session.wait_for_exit()


def test_completions(pyfile, run_as, start_method):
def test_completions_cases(pyfile, run_as, start_method):
@pyfile
def code_to_debug():
from dbgimporter import import_and_enable_debugger
Expand All @@ -110,15 +110,26 @@ def code_to_debug():

response = session.send_request('completions', arguments={
'frameId': hit.frame_id,
'text': 'b.'
'text': 'b.',
'column': 3,
}).wait_for_response()

labels = set(target['label'] for target in response.body['targets'])
assert labels.issuperset(['get', 'items', 'keys', 'setdefault', 'update', 'values'])

response = session.send_request('completions', arguments={
'frameId': hit.frame_id,
'text': 'not_there'
'text': 'x = b.setdefault',
'column': 13,
}).wait_for_response()

assert response.body['targets'] == [
{'label': 'setdefault', 'length': 6, 'start': 6, 'type': 'function'}]

response = session.send_request('completions', arguments={
'frameId': hit.frame_id,
'text': 'not_there',
'column': 10,
}).wait_for_response()

assert not response.body['targets']
Expand Down
17 changes: 10 additions & 7 deletions pytests/func/test_start_stop.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

@pytest.mark.parametrize('start_method', ['launch'])
def test_break_on_entry(pyfile, run_as, start_method):

@pyfile
def code_to_debug():
import backchannel
Expand Down Expand Up @@ -48,6 +49,7 @@ def code_to_debug():
@pytest.mark.skipif(sys.version_info < (3, 0) and platform.system() == 'Windows',
reason="On Win32 Python2.7, unable to send key strokes to test.")
def test_wait_on_normal_exit_enabled(pyfile, run_as, start_method):

@pyfile
def code_to_debug():
import backchannel
Expand Down Expand Up @@ -76,20 +78,19 @@ def code_to_debug():
session.process.stdin.write(b' \r\n')
session.wait_for_exit()

def _decode(text):
if isinstance(text, bytes):
return text.decode('utf-8')
return text
assert any(
l for l in session.output_data['OUT']
if _decode(l).startswith('Press')
decoded = u'\n'.join(
(x.decode('utf-8') if isinstance(x, bytes) else x)
for x in session.output_data['OUT']
)

assert u'Press' in decoded


@pytest.mark.parametrize('start_method', ['launch', 'attach_socket_cmdline'])
@pytest.mark.skipif(sys.version_info < (3, 0) and platform.system() == 'Windows',
reason="On windows py2.7 unable to send key strokes to test.")
def test_wait_on_abnormal_exit_enabled(pyfile, run_as, start_method):

@pyfile
def code_to_debug():
import backchannel
Expand Down Expand Up @@ -124,6 +125,7 @@ def _decode(text):
if isinstance(text, bytes):
return text.decode('utf-8')
return text

assert any(
l for l in session.output_data['OUT']
if _decode(l).startswith('Press')
Expand All @@ -132,6 +134,7 @@ def _decode(text):

@pytest.mark.parametrize('start_method', ['launch', 'attach_socket_cmdline'])
def test_exit_normally_with_wait_on_abnormal_exit_enabled(pyfile, run_as, start_method):

@pyfile
def code_to_debug():
import backchannel
Expand Down
144 changes: 121 additions & 23 deletions src/ptvsd/_vendored/pydevd/_pydev_bundle/_pydev_completer.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
from collections import namedtuple
from string import ascii_letters, digits

from _pydevd_bundle import pydevd_xml
from _pydevd_bundle.pydevd_constants import IS_PY2
import pydevconsole
import sys

if sys.version_info[0] >= 3:
import builtins as __builtin__ # Py3
else:
if IS_PY2:
import __builtin__
else:
import builtins as __builtin__ # Py3

try:
import java.lang #@UnusedImport
import java.lang # @UnusedImport
from _pydev_bundle import _pydev_jy_imports_tipper
_pydev_imports_tipper = _pydev_jy_imports_tipper
except ImportError:
IS_JYTHON = False
from _pydev_bundle import _pydev_imports_tipper

from _pydevd_bundle import pydevd_xml
dir2 = _pydev_imports_tipper.generate_imports_tip_for_module


Expand All @@ -26,13 +29,13 @@ class _StartsWithFilter:
Used because we can't create a lambda that'll use an outer scope in jython 2.1
'''


def __init__(self, start_with):
self.start_with = start_with.lower()

def __call__(self, name):
return name.lower().startswith(self.start_with)


#=======================================================================================================================
# Completer
#
Expand Down Expand Up @@ -82,9 +85,9 @@ def complete(self, text):
"""
if self.use_main_ns:
#In pydev this option should never be used
# In pydev this option should never be used
raise RuntimeError('Namespace must be provided!')
self.namespace = __main__.__dict__ #@UndefinedVariable
self.namespace = __main__.__dict__ # @UndefinedVariable

if "." in text:
return self.attr_matches(text)
Expand All @@ -99,13 +102,12 @@ def global_matches(self, text):
"""


def get_item(obj, attr):
return obj[attr]

a = {}

for dict_with_comps in [__builtin__.__dict__, self.namespace, self.global_namespace]: #@UndefinedVariable
for dict_with_comps in [__builtin__.__dict__, self.namespace, self.global_namespace]: # @UndefinedVariable
a.update(dict_with_comps)

filter = _StartsWithFilter(text)
Expand All @@ -128,7 +130,7 @@ def attr_matches(self, text):
import re

# Another option, seems to work great. Catches things like ''.<tab>
m = re.match(r"(\S+(\.\w+)*)\.(\w*)$", text) #@UndefinedVariable
m = re.match(r"(\S+(\.\w+)*)\.(\w*)$", text) # @UndefinedVariable

if not m:
return []
Expand All @@ -149,30 +151,44 @@ def attr_matches(self, text):
return words


#=======================================================================================================================
# generate_completions_as_xml
#=======================================================================================================================
def generate_completions_as_xml(frame, act_tok):
def generate_completions(frame, act_tok):
'''
:return list(tuple(method_name, docstring, parameters, completion_type))
method_name: str
docstring: str
parameters: str -- i.e.: "(a, b)"
completion_type is an int
See: _pydev_bundle._pydev_imports_tipper for TYPE_ constants
'''
if frame is None:
return '<xml></xml>'
return []

#Not using frame.f_globals because of https://sourceforge.net/tracker2/?func=detail&aid=2541355&group_id=85796&atid=577329
#(Names not resolved in generator expression in method)
#See message: http://mail.python.org/pipermail/python-list/2009-January/526522.html
# Not using frame.f_globals because of https://sourceforge.net/tracker2/?func=detail&aid=2541355&group_id=85796&atid=577329
# (Names not resolved in generator expression in method)
# See message: http://mail.python.org/pipermail/python-list/2009-January/526522.html
updated_globals = {}
updated_globals.update(frame.f_globals)
updated_globals.update(frame.f_locals) #locals later because it has precedence over the actual globals
updated_globals.update(frame.f_locals) # locals later because it has precedence over the actual globals

if pydevconsole.IPYTHON:
completions = pydevconsole.get_completions(act_tok, act_tok, updated_globals, frame.f_locals)
else:
completer = Completer(updated_globals, None)
#list(tuple(name, descr, parameters, type))
# list(tuple(name, descr, parameters, type))
completions = completer.complete(act_tok)

return completions


def generate_completions_as_xml(frame, act_tok):
completions = generate_completions(frame, act_tok)
return completions_to_xml(completions)


def completions_to_xml(completions):
valid_xml = pydevd_xml.make_valid_xml_value
quote = pydevd_xml.quote

msg = ["<xml>"]

for comp in completions:
Expand All @@ -189,3 +205,85 @@ def generate_completions_as_xml(frame, act_tok):

return ''.join(msg)


identifier_start = ascii_letters + '_'
identifier_part = ascii_letters + '_' + digits

if IS_PY2:
identifier_start = identifier_start.decode('utf-8')
identifier_part = identifier_part.decode('utf-8')

identifier_start = set(identifier_start)
identifier_part = set(identifier_part)

if IS_PY2:

# There's no string.isidentifier() on py2.
def isidentifier(s):
if not s:
return False
if s[0] not in identifier_start:
return False

for c in s[1:]:
if c not in identifier_part:
return False
return True

else:

def isidentifier(s):
return s.isidentifier()

TokenAndQualifier = namedtuple('TokenAndQualifier', 'token, qualifier')


def extract_token_and_qualifier(text, line=0, column=0):
'''
Extracts the token a qualifier from the text given the line/colum
(see test_extract_token_and_qualifier for examples).
:param unicode text:
:param int line: 0-based
:param int column: 0-based
'''
# Note: not using the tokenize module because text should be unicode and
# line/column refer to the unicode text (otherwise we'd have to know
# those ranges after converted to bytes).
if line < 0:
line = 0
if column < 0:
column = 0

if isinstance(text, bytes):
text = text.decode('utf-8')

lines = text.splitlines()
try:
text = lines[line]
except IndexError:
return TokenAndQualifier(u'', u'')

if column >= len(text):
column = len(text)

text = text[:column]
token = u''
qualifier = u''

temp_token = []
for i in range(column - 1, -1, -1):
c = text[i]
if c in identifier_part or isidentifier(c) or c == u'.':
temp_token.append(c)
else:
break
temp_token = u''.join(reversed(temp_token))
if u'.' in temp_token:
temp_token = temp_token.split(u'.')
token = u'.'.join(temp_token[:-1])
qualifier = temp_token[-1]
else:
qualifier = temp_token

return TokenAndQualifier(token, qualifier)
Loading

0 comments on commit 5af4b61

Please sign in to comment.