diff --git a/src/ptvsd/_vendored/pydevd/.travis_install_python_deps.sh b/src/ptvsd/_vendored/pydevd/.travis_install_python_deps.sh index 8538f1152..eb10ba335 100644 --- a/src/ptvsd/_vendored/pydevd/.travis_install_python_deps.sh +++ b/src/ptvsd/_vendored/pydevd/.travis_install_python_deps.sh @@ -35,7 +35,7 @@ fi if [ "$PYDEVD_PYTHON_VERSION" = "3.7" ]; then conda install --yes pyqt=5 matplotlib # Note: track the latest django - pip install "django" + pip install "django>=2.1,<2.2" fi pip install untangle diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_api.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_api.py index 909440efc..0436254db 100644 --- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_api.py +++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_api.py @@ -473,6 +473,15 @@ def remove_plugins_exception_breakpoint(self, py_db, exception_type, exception): py_db.on_breakpoints_changed(removed=True) + def remove_all_exception_breakpoints(self, py_db): + py_db.break_on_uncaught_exceptions = {} + py_db.break_on_caught_exceptions = {} + + plugin = py_db.plugin + if plugin is not None: + plugin.remove_all_exception_breakpoints(py_db) + py_db.on_breakpoints_changed(removed=True) + def set_project_roots(self, py_db, project_roots): ''' :param unicode project_roots: diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py index c249a5f84..6119c6852 100644 --- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py +++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py @@ -1135,9 +1135,11 @@ def internal_get_exception_details_json(dbg, request, thread_id, set_additional_ # This is an extra bit of data used by Visual Studio source_path = frames[0][0] if frames else '' - # TODO: breakMode is set to always. This should be retrieved from exception - # breakpoint settings for that exception, or its parent chain. Currently json - # support for setExceptionBreakpoint is not implemented. + if thread.stop_reason == CMD_STEP_CAUGHT_EXCEPTION: + break_mode = pydevd_schema.ExceptionBreakMode.ALWAYS + else: + break_mode = pydevd_schema.ExceptionBreakMode.UNHANDLED + response = pydevd_schema.ExceptionInfoResponse( request_seq=request.seq, success=True, @@ -1145,7 +1147,7 @@ def internal_get_exception_details_json(dbg, request, thread_id, set_additional_ body=pydevd_schema.ExceptionInfoResponseBody( exceptionId=name, description=description, - breakMode=pydevd_schema.ExceptionBreakMode.ALWAYS, + breakMode=break_mode, details=pydevd_schema.ExceptionDetails( message=description, typeName=name, diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py index 4c5973291..d8f9941f7 100644 --- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py +++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py @@ -7,7 +7,7 @@ from _pydevd_bundle._debug_adapter import pydevd_base_schema from _pydevd_bundle._debug_adapter.pydevd_schema import (SourceBreakpoint, ScopesResponseBody, Scope, VariablesResponseBody, SetVariableResponseBody, ModulesResponseBody, SourceResponseBody, - GotoTargetsResponseBody) + GotoTargetsResponseBody, ExceptionOptions) from _pydevd_bundle.pydevd_api import PyDevdAPI from _pydevd_bundle.pydevd_comm_constants import ( CMD_RETURN, CMD_STEP_OVER_MY_CODE, CMD_STEP_OVER, CMD_STEP_INTO_MY_CODE, @@ -326,6 +326,7 @@ def on_disconnect_request(self, py_db, request): :param DisconnectRequest request: ''' self.api.remove_all_breakpoints(py_db, filename='*') + self.api.remove_all_exception_breakpoints(py_db) self.api.request_resume_thread(thread_id='*') response = pydevd_base_schema.build_response(request) @@ -380,6 +381,97 @@ def on_setbreakpoints_request(self, py_db, request): set_breakpoints_response = pydevd_base_schema.build_response(request, kwargs={'body':body}) return NetCommand(CMD_RETURN, 0, set_breakpoints_response, is_json=True) + def on_setexceptionbreakpoints_request(self, py_db, request): + ''' + :param SetExceptionBreakpointsRequest request: + ''' + # : :type arguments: SetExceptionBreakpointsArguments + arguments = request.arguments + filters = arguments.filters + exception_options = arguments.exceptionOptions + self.api.remove_all_exception_breakpoints(py_db) + + # Can't set these in the DAP. + condition = None + expression = None + notify_on_first_raise_only = False + + ignore_libraries = 1 if py_db.get_use_libraries_filter() else 0 + + if exception_options: + break_raised = True + break_uncaught = True + + for option in exception_options: + option = ExceptionOptions(**option) + if not option.path: + continue + + notify_on_handled_exceptions = 1 if option.breakMode == 'always' else 0 + notify_on_unhandled_exceptions = 1 if option.breakMode in ('unhandled', 'userUnhandled') else 0 + exception_paths = option.path + + exception_names = [] + if len(exception_paths) == 0: + continue + + elif len(exception_paths) == 1: + if 'Python Exceptions' in exception_paths[0]['names']: + exception_names = ['BaseException'] + + else: + path_iterator = iter(exception_paths) + if 'Python Exceptions' in next(path_iterator)['names']: + for path in path_iterator: + for ex_name in path['names']: + exception_names.append(ex_name) + + for exception_name in exception_names: + self.api.add_python_exception_breakpoint( + py_db, + exception_name, + condition, + expression, + notify_on_handled_exceptions, + notify_on_unhandled_exceptions, + notify_on_first_raise_only, + ignore_libraries + ) + + else: + break_raised = 'raised' in filters + break_uncaught = 'uncaught' in filters + if break_raised or break_uncaught: + notify_on_handled_exceptions = 1 if break_raised else 0 + notify_on_unhandled_exceptions = 1 if break_uncaught else 0 + exception = 'BaseException' + + self.api.add_python_exception_breakpoint( + py_db, + exception, + condition, + expression, + notify_on_handled_exceptions, + notify_on_unhandled_exceptions, + notify_on_first_raise_only, + ignore_libraries + ) + + if break_raised or break_uncaught: + btype = None + if self._debug_options.get('DJANGO_DEBUG', False): + btype = 'django' + elif self._debug_options.get('FLASK_DEBUG', False): + btype = 'jinja2' + + if btype: + self.api.add_plugins_exception_breakpoint( + py_db, btype, 'BaseException') # Note: Exception name could be anything here. + + # Note: no body required on success. + set_breakpoints_response = pydevd_base_schema.build_response(request) + return NetCommand(CMD_RETURN, 0, set_breakpoints_response, is_json=True) + def on_stacktrace_request(self, py_db, request): ''' :param StackTraceRequest request: diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_trace_api.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_trace_api.py index 16ae7bcd0..999bc1c1d 100644 --- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_trace_api.py +++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_trace_api.py @@ -10,6 +10,10 @@ def remove_exception_breakpoint(plugin, pydb, type, exception): return False +def remove_all_exception_breakpoints(plugin, pydb): + return False + + def get_breakpoints(plugin, pydb): return None diff --git a/src/ptvsd/_vendored/pydevd/pydevd_plugins/django_debug.py b/src/ptvsd/_vendored/pydevd/pydevd_plugins/django_debug.py index b48c9df89..3e3ec0d4b 100644 --- a/src/ptvsd/_vendored/pydevd/pydevd_plugins/django_debug.py +++ b/src/ptvsd/_vendored/pydevd/pydevd_plugins/django_debug.py @@ -67,6 +67,13 @@ def remove_exception_breakpoint(plugin, pydb, type, exception): return False +def remove_all_exception_breakpoints(plugin, pydb): + if hasattr(pydb, 'django_exception_break'): + pydb.django_exception_break = {} + return True + return False + + def get_breakpoints(plugin, pydb, type): if type == 'django-line': return pydb.django_breakpoints diff --git a/src/ptvsd/_vendored/pydevd/pydevd_plugins/jinja2_debug.py b/src/ptvsd/_vendored/pydevd/pydevd_plugins/jinja2_debug.py index c8b7c1c77..40dbfa5f5 100644 --- a/src/ptvsd/_vendored/pydevd/pydevd_plugins/jinja2_debug.py +++ b/src/ptvsd/_vendored/pydevd/pydevd_plugins/jinja2_debug.py @@ -45,6 +45,13 @@ def _init_plugin_breaks(pydb): pydb.jinja2_breakpoints = {} +def remove_all_exception_breakpoints(plugin, pydb): + if hasattr(pydb, 'jinja2_exception_break'): + pydb.jinja2_exception_break = {} + return True + return False + + def remove_exception_breakpoint(plugin, pydb, type, exception): if type == 'jinja2': try: diff --git a/src/ptvsd/_vendored/pydevd/tests_python/debugger_unittest.py b/src/ptvsd/_vendored/pydevd/tests_python/debugger_unittest.py index afc35c6f7..4e9193446 100644 --- a/src/ptvsd/_vendored/pydevd/tests_python/debugger_unittest.py +++ b/src/ptvsd/_vendored/pydevd/tests_python/debugger_unittest.py @@ -205,8 +205,7 @@ def get_next_message(self, context_message, timeout=None): frame = sys._getframe().f_back.f_back frame_info = '' while frame: - if frame.f_code.co_name in ( - 'wait_for_message', 'accept_json_message', 'wait_for_json_message', 'wait_for_response'): + if not frame.f_code.co_name.startswith('test_'): frame = frame.f_back continue diff --git a/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_exceptions.py b/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_exceptions.py index 1beda8aaf..f4a7be2d8 100644 --- a/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_exceptions.py +++ b/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_exceptions.py @@ -1,17 +1,22 @@ import sys + + def method3(): - raise IndexError('foo') + raise IndexError('foo') # raise indexerror line + def method2(): - return method3() - + return method3() # reraise on method2 + + def method1(): try: - method2() + method2() # handle on method1 except: pass # Ok, handled assert '__exception__' not in sys._getframe().f_locals - + + if __name__ == '__main__': method1() - print('TEST SUCEEDED!') \ No newline at end of file + print('TEST SUCEEDED!') diff --git a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger.py b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger.py index 6551ea6ba..1968b12ac 100644 --- a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger.py +++ b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger.py @@ -1607,7 +1607,11 @@ def test_case_handled_exceptions0(case_setup): ) writer.write_make_initial_run() - hit = writer.wait_for_breakpoint_hit(REASON_CAUGHT_EXCEPTION, line=3) + hit = writer.wait_for_breakpoint_hit( + REASON_CAUGHT_EXCEPTION, + line=writer.get_line_index_with_content('raise indexerror line') + ) + writer.write_run_thread(hit.thread_id) writer.finished_ok = True @@ -1646,13 +1650,16 @@ def check(hit): assert unquote(unquote(msg.thread.frame[0]['file'])).endswith('_debugger_case_exceptions.py') writer.write_run_thread(hit.thread_id) - hit = writer.wait_for_breakpoint_hit(REASON_CAUGHT_EXCEPTION, line=3) + hit = writer.wait_for_breakpoint_hit( + REASON_CAUGHT_EXCEPTION, line=writer.get_line_index_with_content('raise indexerror line')) check(hit) - hit = writer.wait_for_breakpoint_hit(REASON_CAUGHT_EXCEPTION, line=6) + hit = writer.wait_for_breakpoint_hit( + REASON_CAUGHT_EXCEPTION, line=writer.get_line_index_with_content('reraise on method2')) check(hit) - hit = writer.wait_for_breakpoint_hit(REASON_CAUGHT_EXCEPTION, line=10) + hit = writer.wait_for_breakpoint_hit( + REASON_CAUGHT_EXCEPTION, line=writer.get_line_index_with_content('handle on method1')) check(hit) writer.finished_ok = True @@ -1702,7 +1709,8 @@ def get_environ(self): ) writer.write_make_initial_run() - hit = writer.wait_for_breakpoint_hit(REASON_CAUGHT_EXCEPTION, line=3) + hit = writer.wait_for_breakpoint_hit( + REASON_CAUGHT_EXCEPTION, line=writer.get_line_index_with_content('raise indexerror line')) writer.write_run_thread(hit.thread_id) writer.finished_ok = True @@ -1729,7 +1737,8 @@ def get_environ(self): ) writer.write_make_initial_run() - hit = writer.wait_for_breakpoint_hit(REASON_CAUGHT_EXCEPTION, line=6) + hit = writer.wait_for_breakpoint_hit( + REASON_CAUGHT_EXCEPTION, line=writer.get_line_index_with_content('reraise on method2')) writer.write_run_thread(hit.thread_id) writer.finished_ok = True diff --git a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py index 669f43977..a784fd34b 100644 --- a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py +++ b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py @@ -4,8 +4,10 @@ from _pydevd_bundle._debug_adapter import pydevd_schema, pydevd_base_schema from _pydevd_bundle._debug_adapter.pydevd_base_schema import from_json from tests_python.debugger_unittest import IS_JYTHON, REASON_STEP_INTO, REASON_STEP_OVER, \ - REASON_CAUGHT_EXCEPTION, REASON_THREAD_SUSPEND, REASON_STEP_RETURN, IS_APPVEYOR, overrides -from _pydevd_bundle._debug_adapter.pydevd_schema import ThreadEvent, ModuleEvent, OutputEvent + REASON_CAUGHT_EXCEPTION, REASON_THREAD_SUSPEND, REASON_STEP_RETURN, IS_APPVEYOR, overrides, \ + REASON_UNCAUGHT_EXCEPTION +from _pydevd_bundle._debug_adapter.pydevd_schema import ThreadEvent, ModuleEvent, OutputEvent, \ + ExceptionOptions from tests_python import debugger_unittest import json from collections import namedtuple @@ -114,6 +116,26 @@ def write_set_breakpoints(self, lines, filename=None, line_to_info=None): lines_in_response = [b['line'] for b in body.breakpoints] assert set(lines_in_response) == set(lines) + def write_set_exception_breakpoints(self, filters=None, exception_options=None): + ''' + :param list(str) filters: + A list with 'raised' or 'uncaught' entries. + + :param list(ExceptionOptions) exception_options: + + ''' + filters = filters or [] + assert set(filters).issubset(set(('raised', 'uncaught'))) + + exception_options = exception_options or [] + exception_options = [exception_option.to_dict() for exception_option in exception_options] + + arguments = pydevd_schema.SetExceptionBreakpointsArguments(filters, exception_options) + request = pydevd_schema.SetExceptionBreakpointsRequest(arguments) + # : :type response: SetExceptionBreakpointsResponse + response = self.wait_for_response(self.write_request(request)) + assert response.success + def write_launch(self, **arguments): arguments['noDebug'] = False request = {'type': 'request', 'command': 'launch', 'arguments': arguments, 'seq':-1} @@ -229,6 +251,59 @@ def test_case_json_change_breaks(case_setup): writer.finished_ok = True +def test_case_handled_exception_breaks(case_setup): + with case_setup.test_file('_debugger_case_exceptions.py') as writer: + json_facade = JsonFacade(writer) + + writer.write_set_protocol('http_json') + json_facade.write_launch() + json_facade.write_set_exception_breakpoints(['raised']) + json_facade.write_make_initial_run() + + hit = writer.wait_for_breakpoint_hit( + reason=REASON_CAUGHT_EXCEPTION, line=writer.get_line_index_with_content('raise indexerror line')) + writer.write_run_thread(hit.thread_id) + + hit = writer.wait_for_breakpoint_hit( + reason=REASON_CAUGHT_EXCEPTION, line=writer.get_line_index_with_content('reraise on method2')) + + # Clear so that the last one is not hit. + json_facade.write_set_exception_breakpoints([]) + writer.write_run_thread(hit.thread_id) + + writer.finished_ok = True + + +def test_case_handled_exception_breaks_by_type(case_setup): + with case_setup.test_file('_debugger_case_exceptions.py') as writer: + json_facade = JsonFacade(writer) + + writer.write_set_protocol('http_json') + json_facade.write_launch() + json_facade.write_set_exception_breakpoints(exception_options=[ + ExceptionOptions(breakMode='always', path=[ + {'names': ['Python Exceptions']}, + {'names': ['IndexError']}, + ]) + ]) + json_facade.write_make_initial_run() + + hit = writer.wait_for_breakpoint_hit( + reason=REASON_CAUGHT_EXCEPTION, line=writer.get_line_index_with_content('raise indexerror line')) + + # Deal only with RuntimeErorr now. + json_facade.write_set_exception_breakpoints(exception_options=[ + ExceptionOptions(breakMode='always', path=[ + {'names': ['Python Exceptions']}, + {'names': ['RuntimeError']}, + ]) + ]) + + writer.write_run_thread(hit.thread_id) + + writer.finished_ok = True + + @pytest.mark.skipif(IS_JYTHON, reason='Must check why it is failing in Jython.') def test_case_json_protocol(case_setup): with case_setup.test_file('_debugger_case_print.py') as writer: @@ -1070,12 +1145,7 @@ def test_exception_details(case_setup): json_facade = JsonFacade(writer) writer.write_set_protocol('http_json') - writer.write_add_exception_breakpoint_with_policy( - 'IndexError', - notify_on_handled_exceptions=2, # Notify only once - notify_on_unhandled_exceptions=0, - ignore_libraries=1 - ) + json_facade.write_set_exception_breakpoints(['raised']) json_facade.write_make_initial_run() hit = writer.wait_for_breakpoint_hit(REASON_CAUGHT_EXCEPTION) @@ -1088,6 +1158,7 @@ def test_exception_details(case_setup): assert body.description == 'foo' assert body.details.kwargs['source'] == writer.TEST_FILE + json_facade.write_set_exception_breakpoints([]) # Don't stop on reraises. writer.write_run_thread(hit.thread_id) writer.finished_ok = True @@ -1302,11 +1373,19 @@ def test_case_django_no_attribute_exception_breakpoint(case_setup_django, jmc): if jmc: writer.write_set_project_roots([debugger_unittest._get_debugger_test_file('my_code')]) - json_facade.write_launch(debugOptions=[]) + json_facade.write_launch(debugOptions=['Django']) + json_facade.write_set_exception_breakpoints(['raised']) else: - json_facade.write_launch(debugOptions=['DebugStdLib']) + json_facade.write_launch(debugOptions=['DebugStdLib', 'Django']) + # Don't set to all 'raised' because we'd stop on standard library exceptions here + # (which is not something we want). + json_facade.write_set_exception_breakpoints(exception_options=[ + ExceptionOptions(breakMode='always', path=[ + {'names': ['Python Exceptions']}, + {'names': ['AssertionError']}, + ]) + ]) - writer.write_add_exception_breakpoint_django() writer.write_make_initial_run() t = writer.create_request_thread('my_app/template_error') @@ -1348,11 +1427,18 @@ def test_case_flask_exceptions(case_setup_flask, jmc): if jmc: writer.write_set_project_roots([debugger_unittest._get_debugger_test_file('my_code')]) - json_facade.write_launch(debugOptions=[]) + json_facade.write_launch(debugOptions=['Jinja']) + json_facade.write_set_exception_breakpoints(['raised']) else: - json_facade.write_launch(debugOptions=['DebugStdLib']) - - writer.write_add_exception_breakpoint_jinja2() + json_facade.write_launch(debugOptions=['DebugStdLib', 'Jinja']) + # Don't set to all 'raised' because we'd stop on standard library exceptions here + # (which is not something we want). + json_facade.write_set_exception_breakpoints(exception_options=[ + ExceptionOptions(breakMode='always', path=[ + {'names': ['Python Exceptions']}, + {'names': ['IndexError']}, + ]) + ]) writer.write_make_initial_run() t = writer.create_request_thread('/bad_template') diff --git a/src/ptvsd/wrapper.py b/src/ptvsd/wrapper.py index 054956316..5f65300a5 100644 --- a/src/ptvsd/wrapper.py +++ b/src/ptvsd/wrapper.py @@ -498,129 +498,6 @@ def pydevd_request(self, loop, cmd_id, args, is_json=False): return fut -class ExceptionsManager(object): - - def __init__(self, proc): - self.proc = proc - self.exceptions = {} - self.lock = threading.Lock() - - def remove_exception_break(self, ex_type='python', exception='BaseException'): - cmdargs = (ex_type, exception) - msg = '{}-{}'.format(*cmdargs) - self.proc.pydevd_notify(pydevd_comm.CMD_REMOVE_EXCEPTION_BREAK, msg) - - def remove_all_exception_breaks(self): - with self.lock: - for exception in self.exceptions.keys(): - self.remove_exception_break(exception=exception) - self.exceptions = {} - - def _find_exception(self, name): - if name in self.exceptions: - return name - - for ex_name in self.exceptions.keys(): - # exception name can be in repr form - # here we attempt to find the exception as it - # is saved in the dictionary - if ex_name in name: - return ex_name - - return 'BaseException' - - def get_break_mode(self, name): - with self.lock: - try: - return self.exceptions[self._find_exception(name)] - except KeyError: - pass - return 'unhandled' - - def add_exception_break(self, exception, break_raised, break_uncaught, - skip_stdlib=False, ex_type='python'): - - notify_on_handled_exceptions = 1 if break_raised else 0 - notify_on_unhandled_exceptions = 1 if break_uncaught else 0 - ignore_libraries = 1 if skip_stdlib else 0 - - cmdargs = ( - ex_type, - exception, - notify_on_handled_exceptions, - notify_on_unhandled_exceptions, - ignore_libraries, - ) - - break_mode = 'never' - if break_raised: - break_mode = 'always' - elif break_uncaught: - break_mode = 'unhandled' - - msg = '{}-{}\t{}\t{}\t{}'.format(*cmdargs) - with self.lock: - self.proc.pydevd_notify( - pydevd_comm.CMD_ADD_EXCEPTION_BREAK, msg) - self.exceptions[exception] = break_mode - - def apply_exception_options(self, exception_options, skip_stdlib=False): - """ - Applies exception options after removing any existing exception - breaks. - """ - self.remove_all_exception_breaks() - pyex_options = (opt - for opt in exception_options - if self._is_python_exception_category(opt)) - for option in pyex_options: - exception_paths = option['path'] - if not exception_paths: - continue - - mode = option['breakMode'] - break_raised = (mode == 'always') - break_uncaught = (mode in ['unhandled', 'userUnhandled']) - - # Special case for the entire python exceptions category - is_category = False - if len(exception_paths) == 1: - # TODO: isn't the first one always the category? - if exception_paths[0]['names'][0] == 'Python Exceptions': - is_category = True - if is_category: - self.add_exception_break( - 'BaseException', break_raised, break_uncaught, skip_stdlib) - else: - path_iterator = iter(exception_paths) - # Skip the first one. It will always be the category - # "Python Exceptions" - next(path_iterator) - exception_names = [] - for path in path_iterator: - for ex_name in path['names']: - exception_names.append(ex_name) - for exception_name in exception_names: - self.add_exception_break( - exception_name, break_raised, - break_uncaught, skip_stdlib) - - def _is_python_exception_category(self, option): - """ - Check if the option has entires and that the first entry - is 'Python Exceptions'. - """ - exception_paths = option['path'] - if not exception_paths: - return False - - category = exception_paths[0]['names'] - if category is None or len(category) != 1: - return False - - return category[0] == 'Python Exceptions' - - class VariablesSorter(object): def __init__(self): @@ -1232,7 +1109,6 @@ def __init__( self.is_process_created_lock = threading.Lock() self.thread_map = IDMap() self._path_mappings = [] - self.exceptions_mgr = ExceptionsManager(self) self.internals_filter = InternalsFilter() self.new_thread_lock = threading.Lock() @@ -1482,7 +1358,6 @@ def _handle_detach(self): self._detached = True self._clear_output_redirection() - self.exceptions_mgr.remove_all_exception_breaks() # No related pydevd command id (removes all breaks and resumes threads). self.pydevd_request( @@ -1507,6 +1382,24 @@ def send_process_event(self, start_method): } self.send_event('process', **evt) + @async_handler + def _forward_request_to_pydevd(self, request, args): + translate_thread_id = args.get('threadId') is not None + if translate_thread_id: + pyd_tid = self.thread_map.to_pydevd(int(args['threadId'])) + + pydevd_request = copy.deepcopy(request) + del pydevd_request['seq'] # A new seq should be created for pydevd. + if translate_thread_id: + pydevd_request['arguments']['threadId'] = pyd_tid + cmd_id = -1 # It's actually unused on json requests. + _, _, resp_args = yield self.pydevd_request(cmd_id, pydevd_request, is_json=True) + + body = resp_args.get('body') + if body is None: + body = {} + self.send_response(request, **body) + @async_handler def on_threads(self, request, args): # TODO: docstring @@ -1591,30 +1484,11 @@ def on_stackTrace(self, request, args): totalFrames = resp_args['body']['totalFrames'] self.send_response(request, stackFrames=stackFrames, totalFrames=totalFrames) - @async_handler def on_scopes(self, request, args): - pydevd_request = copy.deepcopy(request) - del pydevd_request['seq'] # A new seq should be created for pydevd. - _, _, resp_args = yield self.pydevd_request( - -1, - pydevd_request, - is_json=True) - - scopes = resp_args['body']['scopes'] - self.send_response(request, scopes=scopes) + self._forward_request_to_pydevd(request, args) - @async_handler def on_variables(self, request, args): - """Handles DAP VariablesRequest.""" - pydevd_request = copy.deepcopy(request) - del pydevd_request['seq'] # A new seq should be created for pydevd. - _, _, resp_args = yield self.pydevd_request( - pydevd_comm.CMD_GET_VARIABLE, - pydevd_request, - is_json=True) - - variables = resp_args['body']['variables'] - self.send_response(request, variables=variables) + self._forward_request_to_pydevd(request, args) @async_handler def on_setVariable(self, request, args): @@ -1656,29 +1530,11 @@ def on_evaluate(self, request, args): body['result'] = resp_args['message'] self.send_response(request, **body) - @async_handler def on_setExpression(self, request, args): - pydevd_request = copy.deepcopy(request) - del pydevd_request['seq'] # A new seq should be created for pydevd. - _, _, resp_args = yield self.pydevd_request( - -1, - pydevd_request, - is_json=True) + self._forward_request_to_pydevd(request, args) - body = resp_args['body'] - self.send_response(request, **body) - - @async_handler def on_modules(self, request, args): - pydevd_request = copy.deepcopy(request) - del pydevd_request['seq'] # A new seq should be created for pydevd. - _, _, resp_args = yield self.pydevd_request( - -1, - pydevd_request, - is_json=True) - - body = resp_args.get('body', {}) - self.send_response(request, **body) + self._forward_request_to_pydevd(request, args) @async_handler def on_pause(self, request, args): @@ -1719,53 +1575,17 @@ def on_continue(self, request, args): pydevd_request, is_json=True) self.send_response(request, allThreadsContinued=True) - @async_handler def on_next(self, request, args): + self._forward_request_to_pydevd(request, args) - pyd_tid = self.thread_map.to_pydevd(int(args['threadId'])) - - pydevd_request = copy.deepcopy(request) - del pydevd_request['seq'] # A new seq should be created for pydevd. - pydevd_request['arguments']['threadId'] = pyd_tid - cmd_id = pydevd_comm.CMD_STEP_OVER - - self.pydevd_request(cmd_id, pydevd_request, is_json=True) - self.send_response(request) - - @async_handler def on_stepIn(self, request, args): + self._forward_request_to_pydevd(request, args) - pyd_tid = self.thread_map.to_pydevd(int(args['threadId'])) - - pydevd_request = copy.deepcopy(request) - del pydevd_request['seq'] # A new seq should be created for pydevd. - pydevd_request['arguments']['threadId'] = pyd_tid - cmd_id = pydevd_comm.CMD_STEP_INTO - - self.pydevd_request(cmd_id, pydevd_request, is_json=True) - self.send_response(request) - - @async_handler def on_stepOut(self, request, args): + self._forward_request_to_pydevd(request, args) - pyd_tid = self.thread_map.to_pydevd(int(args['threadId'])) - - pydevd_request = copy.deepcopy(request) - del pydevd_request['seq'] # A new seq should be created for pydevd. - pydevd_request['arguments']['threadId'] = pyd_tid - cmd_id = pydevd_comm.CMD_STEP_RETURN - - self.pydevd_request(cmd_id, pydevd_request, is_json=True) - self.send_response(request) - - @async_handler def on_gotoTargets(self, request, args): - pydevd_request = copy.deepcopy(request) - del pydevd_request['seq'] # A new seq should be created for pydevd. - cmd_id = pydevd_comm.CMD_GET_NEXT_STATEMENT_TARGETS - _, _, resp_args = yield self.pydevd_request(cmd_id, pydevd_request, is_json=True) - - self.send_response(request, targets=resp_args['body']['targets']) + self._forward_request_to_pydevd(request, args) @async_handler def on_goto(self, request, args): @@ -1827,73 +1647,14 @@ def on_setBreakpoints(self, request, args): breakpoints = resp_args['body']['breakpoints'] self.send_response(request, breakpoints=breakpoints) - def _get_pydevex_type(self): - if self.debug_options.get('DJANGO_DEBUG', False): - return 'django' - elif self.debug_options.get('FLASK_DEBUG', False): - return 'jinja2' - return 'python' - - @async_handler def on_setExceptionBreakpoints(self, request, args): - # TODO: docstring - filters = args['filters'] - exception_options = args.get('exceptionOptions', []) - jmc = self._is_just_my_code_stepping_enabled() - - pydevex_type = self._get_pydevex_type() - if exception_options: - self.exceptions_mgr.apply_exception_options( - exception_options, jmc) - if pydevex_type != 'python': - self.exceptions_mgr.remove_exception_break(ex_type=pydevex_type) - self.exceptions_mgr.add_exception_break( - 'BaseException', True, True, - skip_stdlib=jmc, ex_type=pydevex_type) - else: - self.exceptions_mgr.remove_all_exception_breaks() - break_raised = 'raised' in filters - break_uncaught = 'uncaught' in filters - if break_raised or break_uncaught: - self.exceptions_mgr.add_exception_break( - 'BaseException', break_raised, break_uncaught, - skip_stdlib=jmc) - if pydevex_type != 'python': - self.exceptions_mgr.remove_exception_break(ex_type=pydevex_type) - self.exceptions_mgr.add_exception_break( - 'BaseException', break_raised, break_uncaught, - skip_stdlib=jmc, ex_type=pydevex_type) - - if request is not None: - self.send_response(request) + self._forward_request_to_pydevd(request, args) - @async_handler def on_exceptionInfo(self, request, args): - pyd_tid = self.thread_map.to_pydevd(args['threadId']) - - pydevd_request = copy.deepcopy(request) - del pydevd_request['seq'] # A new seq should be created for pydevd. - pydevd_request['arguments']['threadId'] = pyd_tid - _, _, resp_args = yield self.pydevd_request( - pydevd_comm.CMD_GET_EXCEPTION_DETAILS, - pydevd_request, - is_json=True) - - body = resp_args['body'] - body['breakMode'] = self.exceptions_mgr.get_break_mode(body['exceptionId']) - self.send_response(request, **body) + self._forward_request_to_pydevd(request, args) - @async_handler def on_completions(self, request, args): - pydevd_request = copy.deepcopy(request) - del pydevd_request['seq'] # A new seq should be created for pydevd. - _, _, resp_args = yield self.pydevd_request( - pydevd_comm.CMD_GET_COMPLETIONS, - pydevd_request, - is_json=True) - - targets = resp_args['body']['targets'] - self.send_response(request, targets=targets) + self._forward_request_to_pydevd(request, args) # Custom ptvsd message def on_ptvsd_systemInfo(self, request, args): diff --git a/tests/func/test_exception.py b/tests/func/test_exception.py index 59f312d91..d26467452 100644 --- a/tests/func/test_exception.py +++ b/tests/func/test_exception.py @@ -15,18 +15,22 @@ @pytest.mark.parametrize('raised', ['raisedOn', 'raisedOff']) @pytest.mark.parametrize('uncaught', ['uncaughtOn', 'uncaughtOff']) def test_vsc_exception_options_raise_with_except(pyfile, run_as, start_method, raised, uncaught): + @pyfile def code_to_debug(): from dbgimporter import import_and_enable_debugger import_and_enable_debugger() + def raise_with_except(): try: - raise ArithmeticError('bad code') + raise ArithmeticError('bad code') # @exception_line except Exception: pass + raise_with_except() - ex_line = 5 + line_numbers = get_marked_line_numbers(code_to_debug) + ex_line = line_numbers['exception_line'] filters = [] filters += ['raised'] if raised == 'raisedOn' else [] filters += ['uncaught'] if uncaught == 'uncaughtOn' else [] @@ -72,15 +76,19 @@ def raise_with_except(): @pytest.mark.parametrize('raised', ['raisedOn', 'raisedOff']) @pytest.mark.parametrize('uncaught', ['uncaughtOn', 'uncaughtOff']) def test_vsc_exception_options_raise_without_except(pyfile, run_as, start_method, raised, uncaught): + @pyfile def code_to_debug(): from dbgimporter import import_and_enable_debugger import_and_enable_debugger() + def raise_without_except(): - raise ArithmeticError('bad code') + raise ArithmeticError('bad code') # @exception_line + raise_without_except() - ex_line = 4 + line_numbers = get_marked_line_numbers(code_to_debug) + ex_line = line_numbers['exception_line'] filters = [] filters += ['raised'] if raised == 'raisedOn' else [] filters += ['uncaught'] if uncaught == 'uncaughtOn' else [] @@ -135,6 +143,17 @@ def raise_without_except(): 'threadId': hit.thread_id }).wait_for_response() + expected = ANY.dict_with({ + 'exceptionId': ANY.such_that(lambda s: s.endswith('ArithmeticError')), + 'description': 'bad code', + 'breakMode': 'unhandled', # Only difference from previous expected is breakMode. + 'details': ANY.dict_with({ + 'typeName': ANY.such_that(lambda s: s.endswith('ArithmeticError')), + 'message': 'bad code', + 'source': Path(code_to_debug), + }), + }) + assert resp_exc_info.body == expected session.send_request('continue').wait_for_response(freeze=False) @@ -146,6 +165,7 @@ def raise_without_except(): @pytest.mark.parametrize('zero', ['zero', '']) @pytest.mark.parametrize('exit_code', [0, 1, 'nan']) def test_systemexit(pyfile, run_as, start_method, raised, uncaught, zero, exit_code): + @pyfile def code_to_debug(): from dbgimporter import import_and_enable_debugger @@ -154,10 +174,10 @@ def code_to_debug(): exit_code = eval(sys.argv[1]) print('sys.exit(%r)' % (exit_code,)) try: - sys.exit(exit_code) #@handled + sys.exit(exit_code) # @handled except SystemExit: pass - sys.exit(exit_code) #@unhandled + sys.exit(exit_code) # @unhandled line_numbers = get_marked_line_numbers(code_to_debug) @@ -208,3 +228,86 @@ def code_to_debug(): session.send_request('continue').wait_for_response(freeze=False) session.wait_for_exit() + + +@pytest.mark.parametrize('break_mode', ['always', 'never', 'unhandled', 'userUnhandled']) +@pytest.mark.parametrize('exceptions', [ + ['RuntimeError'], + ['AssertionError'], + ['RuntimeError', 'AssertionError'], + [], # Add the whole Python Exceptions category. + ]) +def test_raise_exception_options(pyfile, run_as, start_method, exceptions, break_mode): + + if break_mode in ('never', 'unhandled', 'userUnhandled'): + + @pyfile + def code_to_debug(): + from dbgimporter import import_and_enable_debugger + import_and_enable_debugger() + raise AssertionError() # @AssertionError + + if break_mode == 'never': + expect_exceptions = [] + + elif 'AssertionError' in exceptions or not exceptions: + # Only AssertionError is raised in this use-case. + expect_exceptions = ['AssertionError'] + + else: + expect_exceptions = [] + + else: + expect_exceptions = exceptions[:] + if not expect_exceptions: + # Deal with the Python Exceptions category + expect_exceptions = ['RuntimeError', 'AssertionError', 'IndexError'] + + @pyfile + def code_to_debug(): + from dbgimporter import import_and_enable_debugger + import_and_enable_debugger() + try: + raise RuntimeError() # @RuntimeError + except RuntimeError: + pass + try: + raise AssertionError() # @AssertionError + except AssertionError: + pass + try: + raise IndexError() # @IndexError + except IndexError: + pass + + line_numbers = get_marked_line_numbers(code_to_debug) + + with DebugSession() as session: + session.initialize( + target=(run_as, code_to_debug), + start_method=start_method, + ignore_unobserved=[Event('continued'), Event('stopped')], + expected_returncode=ANY.int, + ) + path = [ + {'names': ['Python Exceptions']}, + ] + if exceptions: + path.append({'names': exceptions}) + session.send_request('setExceptionBreakpoints', { + 'filters': [], # Unused when exceptionOptions is passed. + 'exceptionOptions': [{ + 'path': path, + 'breakMode': break_mode, # Can be "never", "always", "unhandled", "userUnhandled" + }], + }).wait_for_response() + session.start_debugging() + + for expected_exception in expect_exceptions: + hit = session.wait_for_thread_stopped(reason='exception') + frames = hit.stacktrace.body['stackFrames'] + assert frames[0]['source']['path'].endswith('code_to_debug.py') + assert frames[0]['line'] == line_numbers[expected_exception] + session.send_request('continue').wait_for_response(freeze=False) + + session.wait_for_exit()