diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_api.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_api.py index 8b738c3a4..b011f29e2 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_api.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_api.py @@ -125,6 +125,9 @@ def set_ide_os(self, ide_os): ''' pydevd_file_utils.set_ide_os(ide_os) + def set_gui_event_loop(self, py_db, gui_event_loop): + py_db._gui_event_loop = gui_event_loop + def send_error_message(self, py_db, msg): sys.stderr.write('pydevd: %s\n' % (msg,)) diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_json_debug_options.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_json_debug_options.py index c6def933c..96ed582b1 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_json_debug_options.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_json_debug_options.py @@ -19,6 +19,7 @@ class DebugOptions(object): 'flask_debug', 'stop_on_entry', 'max_exception_stack_frames', + 'gui_event_loop', ] def __init__(self): @@ -30,6 +31,7 @@ def __init__(self): self.flask_debug = False self.stop_on_entry = False self.max_exception_stack_frames = 0 + self.gui_event_loop = 'matplotlib' def to_json(self): dct = {} @@ -92,6 +94,8 @@ def update_from_args(self, args): self.max_exception_stack_frames = int_parser(args.get('maxExceptionStackFrames', 0)) + if 'guiEventLoop' in args: + self.gui_event_loop = str(args['guiEventLoop']) def int_parser(s, default_value=0): try: diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py index 18c54ec52..bda519b80 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py @@ -460,6 +460,8 @@ def get_variable_presentation(setting, default): if self._options.stop_on_entry and start_reason == 'launch': self.api.stop_on_entry() + self.api.set_gui_event_loop(py_db, self._options.gui_event_loop) + def _send_process_event(self, py_db, start_method): argv = getattr(sys, 'argv', []) if len(argv) > 0: diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_utils.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_utils.py index 00867af0e..47ce11a75 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_utils.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_utils.py @@ -7,6 +7,7 @@ import signal import os import ctypes +from importlib import import_module try: from urllib import quote @@ -506,3 +507,28 @@ def _compute_get_attr_slow(self, diff, cls, attr_name): pass return 'pydevd warning: Getting attribute %s.%s was slow (took %.2fs)\n' % (cls, attr_name, diff) + +def import_attr_from_module(import_with_attr_access): + if '.' not in import_with_attr_access: + # We need at least one '.' (we don't support just the module import, we need the attribute access too). + raise ImportError('Unable to import module with attr access: %s' % (import_with_attr_access,)) + + module_name, attr_name = import_with_attr_access.rsplit('.', 1) + + while True: + try: + mod = import_module(module_name) + except ImportError: + if '.' not in module_name: + raise ImportError('Unable to import module with attr access: %s' % (import_with_attr_access,)) + + module_name, new_attr_part = module_name.rsplit('.', 1) + attr_name = new_attr_part + '.' + attr_name + else: + # Ok, we got the base module, now, get the attribute we need. + try: + for attr in attr_name.split('.'): + mod = getattr(mod, attr) + return mod + except: + raise ImportError('Unable to import module with attr access: %s' % (import_with_attr_access,)) diff --git a/src/debugpy/_vendored/pydevd/pydev_ipython/inputhook.py b/src/debugpy/_vendored/pydevd/pydev_ipython/inputhook.py index 4494da9d0..fe088466f 100644 --- a/src/debugpy/_vendored/pydevd/pydev_ipython/inputhook.py +++ b/src/debugpy/_vendored/pydevd/pydev_ipython/inputhook.py @@ -535,7 +535,7 @@ def enable_gui(gui=None, app=None): if gui is None or gui == '': gui_hook = clear_inputhook else: - e = "Invalid GUI request %r, valid ones are:%s" % (gui, guis.keys()) + e = "Invalid GUI request %r, valid ones are:%s" % (gui, list(guis.keys())) raise ValueError(e) return gui_hook(app) diff --git a/src/debugpy/_vendored/pydevd/pydev_ipython/inputhookqt5.py b/src/debugpy/_vendored/pydevd/pydev_ipython/inputhookqt5.py index 77b938b47..15222df3f 100644 --- a/src/debugpy/_vendored/pydevd/pydev_ipython/inputhookqt5.py +++ b/src/debugpy/_vendored/pydevd/pydev_ipython/inputhookqt5.py @@ -21,29 +21,31 @@ import threading - from pydev_ipython.qt_for_kernel import QtCore, QtGui from pydev_ipython.inputhook import allow_CTRL_C, ignore_CTRL_C, stdin_ready + # To minimise future merging complexity, rather than edit the entire code base below # we fake InteractiveShell here class InteractiveShell: _instance = None + @classmethod def instance(cls): if cls._instance is None: cls._instance = cls() return cls._instance + def set_hook(self, *args, **kwargs): # We don't consider the pre_prompt_hook because we don't have # KeyboardInterrupts to consider since we are running under PyDev pass - #----------------------------------------------------------------------------- # Module Globals #----------------------------------------------------------------------------- + got_kbdint = False sigint_timer = None @@ -51,6 +53,7 @@ def set_hook(self, *args, **kwargs): # Code #----------------------------------------------------------------------------- + def create_inputhook_qt5(mgr, app=None): """Create an input hook for running the Qt5 application event loop. @@ -107,7 +110,7 @@ def inputhook_qt5(): try: allow_CTRL_C() app = QtCore.QCoreApplication.instance() - if not app: # shouldn't happen, but safer if it happens anyway... + if not app: # shouldn't happen, but safer if it happens anyway... return 0 app.processEvents(QtCore.QEventLoop.AllEvents, 300) if not stdin_ready(): @@ -159,13 +162,12 @@ def inputhook_qt5(): pid = os.getpid() if(not sigint_timer): sigint_timer = threading.Timer(.01, os.kill, - args=[pid, signal.SIGINT] ) + args=[pid, signal.SIGINT]) sigint_timer.start() else: print("\nKeyboardInterrupt - Ctrl-C again for new prompt") - - except: # NO exceptions are allowed to escape from a ctypes callback + except: # NO exceptions are allowed to escape from a ctypes callback ignore_CTRL_C() from traceback import print_exc print_exc() diff --git a/src/debugpy/_vendored/pydevd/pydev_ipython/qt_for_kernel.py b/src/debugpy/_vendored/pydevd/pydev_ipython/qt_for_kernel.py index d18a21835..500d25a74 100644 --- a/src/debugpy/_vendored/pydevd/pydev_ipython/qt_for_kernel.py +++ b/src/debugpy/_vendored/pydevd/pydev_ipython/qt_for_kernel.py @@ -35,11 +35,12 @@ import sys from pydev_ipython.version import check_version -from pydev_ipython.qt_loaders import (load_qt, QT_API_PYSIDE, +from pydev_ipython.qt_loaders import (load_qt, QT_API_PYSIDE, QT_API_PYSIDE2, QT_API_PYQT, QT_API_PYQT_DEFAULT, loaded_api, QT_API_PYQT5) -#Constraints placed on an imported matplotlib + +# Constraints placed on an imported matplotlib def matplotlib_options(mpl): if mpl is None: return @@ -70,7 +71,6 @@ def matplotlib_options(mpl): raise ImportError("unhandled value for backend.qt5 from matplotlib: %r" % mpqt) - # Fallback without checking backend (previous code) mpqt = mpl.rcParams.get('backend.qt4', None) if mpqt is None: @@ -92,7 +92,7 @@ def get_options(): """Return a list of acceptable QT APIs, in decreasing order of preference """ - #already imported Qt somewhere. Use that + # already imported Qt somewhere. Use that loaded = loaded_api() if loaded is not None: return [loaded] @@ -100,19 +100,20 @@ def get_options(): mpl = sys.modules.get('matplotlib', None) if mpl is not None and not check_version(mpl.__version__, '1.0.2'): - #1.0.1 only supports PyQt4 v1 + # 1.0.1 only supports PyQt4 v1 return [QT_API_PYQT_DEFAULT] if os.environ.get('QT_API', None) is None: - #no ETS variable. Ask mpl, then use either - return matplotlib_options(mpl) or [QT_API_PYQT_DEFAULT, QT_API_PYSIDE, QT_API_PYQT5] + # no ETS variable. Ask mpl, then use either + return matplotlib_options(mpl) or [QT_API_PYQT_DEFAULT, QT_API_PYSIDE, QT_API_PYSIDE2, QT_API_PYQT5] - #ETS variable present. Will fallback to external.qt + # ETS variable present. Will fallback to external.qt return None + api_opts = get_options() if api_opts is not None: QtCore, QtGui, QtSvg, QT_API = load_qt(api_opts) -else: # use ETS variable +else: # use ETS variable from pydev_ipython.qt import QtCore, QtGui, QtSvg, QT_API diff --git a/src/debugpy/_vendored/pydevd/pydev_ipython/qt_loaders.py b/src/debugpy/_vendored/pydevd/pydev_ipython/qt_loaders.py index 2a77b1a64..0e30d4987 100644 --- a/src/debugpy/_vendored/pydevd/pydev_ipython/qt_loaders.py +++ b/src/debugpy/_vendored/pydevd/pydev_ipython/qt_loaders.py @@ -16,8 +16,9 @@ # Available APIs. QT_API_PYQT = 'pyqt' QT_API_PYQTv1 = 'pyqtv1' -QT_API_PYQT_DEFAULT = 'pyqtdefault' # don't set SIP explicitly +QT_API_PYQT_DEFAULT = 'pyqtdefault' # don't set SIP explicitly QT_API_PYSIDE = 'pyside' +QT_API_PYSIDE2 = 'pyside2' QT_API_PYQT5 = 'pyqt5' @@ -45,6 +46,7 @@ def load_module(self, fullname): already imported an Incompatible QT Binding: %s """ % (fullname, loaded_api())) + ID = ImportDenier() sys.meta_path.append(ID) @@ -58,6 +60,7 @@ def commit_api(api): ID.forbid('PyQt5') else: ID.forbid('PySide') + ID.forbid('PySide2') def loaded_api(): @@ -68,7 +71,7 @@ def loaded_api(): Returns ------- - None, 'pyside', 'pyqt', or 'pyqtv1' + None, 'pyside', 'pyside2', 'pyqt', or 'pyqtv1' """ if 'PyQt4.QtCore' in sys.modules: if qtapi_version() == 2: @@ -77,6 +80,8 @@ def loaded_api(): return QT_API_PYQTv1 elif 'PySide.QtCore' in sys.modules: return QT_API_PYSIDE + elif 'PySide2.QtCore' in sys.modules: + return QT_API_PYSIDE2 elif 'PyQt5.QtCore' in sys.modules: return QT_API_PYQT5 return None @@ -99,6 +104,7 @@ def has_binding(api): # this will cause a crash in sip (#1431) # check for complete presence before importing module_name = {QT_API_PYSIDE: 'PySide', + QT_API_PYSIDE2: 'PySide2', QT_API_PYQT: 'PyQt4', QT_API_PYQTv1: 'PyQt4', QT_API_PYQT_DEFAULT: 'PyQt4', @@ -108,14 +114,14 @@ def has_binding(api): import imp try: - #importing top level PyQt4/PySide module is ok... + # importing top level PyQt4/PySide module is ok... mod = __import__(module_name) - #...importing submodules is not + # ...importing submodules is not imp.find_module('QtCore', mod.__path__) imp.find_module('QtGui', mod.__path__) imp.find_module('QtSvg', mod.__path__) - #we can also safely check PySide version + # we can also safely check PySide version if api == QT_API_PYSIDE: return check_version(mod.__version__, '1.0.3') else: @@ -189,6 +195,7 @@ def import_pyqt4(version=2): api = QT_API_PYQTv1 if version == 1 else QT_API_PYQT return QtCore, QtGui, QtSvg, api + def import_pyqt5(): """ Import PyQt5 @@ -214,6 +221,16 @@ def import_pyside(): return QtCore, QtGui, QtSvg, QT_API_PYSIDE +def import_pyside2(): + """ + Import PySide2 + + ImportErrors raised within this function are non-recoverable + """ + from PySide2 import QtGui, QtCore, QtSvg # @UnresolvedImport + return QtCore, QtGui, QtSvg, QT_API_PYSIDE + + def load_qt(api_options): """ Attempt to import Qt, given a preference list @@ -241,6 +258,7 @@ def load_qt(api_options): an incompatible library has already been installed) """ loaders = {QT_API_PYSIDE: import_pyside, + QT_API_PYSIDE2: import_pyside2, QT_API_PYQT: import_pyqt4, QT_API_PYQTv1: partial(import_pyqt4, version=1), QT_API_PYQT_DEFAULT: partial(import_pyqt4, version=None), @@ -251,14 +269,14 @@ def load_qt(api_options): if api not in loaders: raise RuntimeError( - "Invalid Qt API %r, valid values are: %r, %r, %r, %r, %r" % - (api, QT_API_PYSIDE, QT_API_PYQT, + "Invalid Qt API %r, valid values are: %r, %r, %r, %r, %r, %r" % + (api, QT_API_PYSIDE, QT_API_PYSIDE, QT_API_PYQT, QT_API_PYQTv1, QT_API_PYQT_DEFAULT, QT_API_PYQT5)) if not can_import(api): continue - #cannot safely recover from an ImportError during this + # cannot safely recover from an ImportError during this result = loaders[api]() api = result[-1] # changed if api = QT_API_PYQT_DEFAULT commit_api(api) @@ -273,9 +291,11 @@ def load_qt(api_options): PyQt4 installed: %s PyQt5 installed: %s PySide >= 1.0.3 installed: %s + PySide2 installed: %s Tried to load: %r """ % (loaded_api(), has_binding(QT_API_PYQT), has_binding(QT_API_PYQT5), has_binding(QT_API_PYSIDE), + has_binding(QT_API_PYSIDE2), api_options)) diff --git a/src/debugpy/_vendored/pydevd/pydevd.py b/src/debugpy/_vendored/pydevd/pydevd.py index daecf0967..e729239d1 100644 --- a/src/debugpy/_vendored/pydevd/pydevd.py +++ b/src/debugpy/_vendored/pydevd/pydevd.py @@ -64,7 +64,8 @@ from _pydevd_bundle.pydevd_net_command_factory_xml import NetCommandFactory from _pydevd_bundle.pydevd_trace_dispatch import ( trace_dispatch as _trace_dispatch, global_cache_skips, global_cache_frame_skips, fix_top_level_trace_and_get_trace_func) -from _pydevd_bundle.pydevd_utils import save_main_module, is_current_thread_main_thread +from _pydevd_bundle.pydevd_utils import save_main_module, is_current_thread_main_thread, \ + import_attr_from_module from _pydevd_frame_eval.pydevd_frame_eval_main import ( frame_eval_func, dummy_trace_dispatch) import pydev_ipython # @UnusedImport @@ -614,9 +615,19 @@ def __init__(self, set_as_global=True): self.thread_analyser = None self.asyncio_analyser = None + # The GUI event loop that's going to run. + # Possible values: + # matplotlib - Whatever GUI backend matplotlib is using. + # 'wx'/'qt'/'none'/... - GUI toolkits that have bulitin support. See pydevd_ipython/inputhook.py:24. + # Other - A custom function that'll be imported and run. + self._gui_event_loop = 'matplotlib' + self._installed_gui_support = False + self.gui_in_use = False + + # GUI event loop support in debugger + self.activate_gui_function = None + # matplotlib support in debugger and debug console - self._installed_mpl_support = False - self.mpl_in_use = False self.mpl_hooks_in_debug_console = False self.mpl_modules_for_patching = {} @@ -1522,34 +1533,42 @@ def init_matplotlib_in_debug_console(self): for module in dict_keys(self.mpl_modules_for_patching): import_hook_manager.add_module_name(module, self.mpl_modules_for_patching.pop(module)) - def init_matplotlib_support(self): - if self._installed_mpl_support: + def init_gui_support(self): + if self._installed_gui_support: return - self._installed_mpl_support = True - # prepare debugger for integration with matplotlib GUI event loop - from pydev_ipython.matplotlibtools import activate_matplotlib, activate_pylab, activate_pyplot, do_enable_gui + self._installed_gui_support = True - # enable_gui_function in activate_matplotlib should be called in main thread. Unlike integrated console, + # enable_gui and enable_gui_function in activate_matplotlib should be called in main thread. Unlike integrated console, # in the debug console we have no interpreter instance with exec_queue, but we run this code in the main # thread and can call it directly. - class _MatplotlibHelper: + class _ReturnGuiLoopControlHelper: _return_control_osc = False def return_control(): # Some of the input hooks (e.g. Qt4Agg) check return control without doing # a single operation, so we don't return True on every # call when the debug hook is in place to allow the GUI to run - _MatplotlibHelper._return_control_osc = not _MatplotlibHelper._return_control_osc - return _MatplotlibHelper._return_control_osc + _ReturnGuiLoopControlHelper._return_control_osc = not _ReturnGuiLoopControlHelper._return_control_osc + return _ReturnGuiLoopControlHelper._return_control_osc + + from pydev_ipython.inputhook import set_return_control_callback, enable_gui - from pydev_ipython.inputhook import set_return_control_callback set_return_control_callback(return_control) - self.mpl_modules_for_patching = {"matplotlib": lambda: activate_matplotlib(do_enable_gui), - "matplotlib.pyplot": activate_pyplot, - "pylab": activate_pylab } + if self._gui_event_loop == 'matplotlib': + # prepare debugger for matplotlib integration with GUI event loop + from pydev_ipython.matplotlibtools import activate_matplotlib, activate_pylab, activate_pyplot, do_enable_gui + + self.mpl_modules_for_patching = {"matplotlib": lambda: activate_matplotlib(do_enable_gui), + "matplotlib.pyplot": activate_pyplot, + "pylab": activate_pylab } + else: + self.activate_gui_function = enable_gui + + def _activate_gui_if_needed(self): + if self.gui_in_use: + return - def _activate_mpl_if_needed(self): if len(self.mpl_modules_for_patching) > 0: if is_current_thread_main_thread(): # Note that we call only in the main thread. for module in dict_keys(self.mpl_modules_for_patching): @@ -1557,9 +1576,28 @@ def _activate_mpl_if_needed(self): activate_function = self.mpl_modules_for_patching.pop(module, None) if activate_function is not None: activate_function() - self.mpl_in_use = True + self.gui_in_use = True + + if self.activate_gui_function: + if is_current_thread_main_thread(): # Only call enable_gui in the main thread. + try: + # First try to activate builtin GUI event loops. + self.activate_gui_function(self._gui_event_loop) + self.activate_gui_function = None + self.gui_in_use = True + except ValueError: + # The user requested a custom GUI event loop, try to import it. + from pydev_ipython.inputhook import set_inputhook + try: + inputhook_function = import_attr_from_module(self._gui_event_loop) + set_inputhook(inputhook_function) + self.gui_in_use = True + except Exception as e: + pydev_log.debug("Cannot activate custom GUI event loop {}: {}".format(self._gui_event_loop, e)) + finally: + self.activate_gui_function = None - def _call_mpl_hook(self): + def _call_input_hook(self): try: from pydev_ipython.inputhook import get_inputhook inputhook = get_inputhook() @@ -1700,11 +1738,11 @@ def process_internal_commands(self): while True: int_cmd = queue.get(False) - if not self.mpl_hooks_in_debug_console and isinstance(int_cmd, InternalConsoleExec): + if not self.mpl_hooks_in_debug_console and isinstance(int_cmd, InternalConsoleExec) and not self.gui_in_use: # add import hooks for matplotlib patches if only debug console was started try: self.init_matplotlib_in_debug_console() - self.mpl_in_use = True + self.gui_in_use = True except: pydev_log.debug("Matplotlib support in debug console failed", traceback.format_exc()) self.mpl_hooks_in_debug_console = True @@ -2006,12 +2044,13 @@ def _do_wait_suspend(self, thread, frame, event, arg, suspend_type, from_this_th keep_suspended = False with self._main_lock: # Use lock to check if suspended state changed - activate_matplotlib = info.pydev_state == STATE_SUSPEND and not self.pydb_disposed + activate_gui = info.pydev_state == STATE_SUSPEND and not self.pydb_disposed in_main_thread = is_current_thread_main_thread() - if activate_matplotlib and in_main_thread: + if activate_gui and in_main_thread: # before every stop check if matplotlib modules were imported inside script code - self._activate_mpl_if_needed() + # or some GUI event loop needs to be activated + self._activate_gui_if_needed() while True: with self._main_lock: # Use lock to check if suspended state changed @@ -2019,9 +2058,9 @@ def _do_wait_suspend(self, thread, frame, event, arg, suspend_type, from_this_th # Note: we can't exit here if terminate was requested while a breakpoint was hit. break - if in_main_thread and self.mpl_in_use: - # call input hooks if only matplotlib is in use - self._call_mpl_hook() + if in_main_thread and self.gui_in_use: + # call input hooks if only GUI is in use + self._call_input_hook() self.process_internal_commands() time.sleep(0.01) @@ -2396,7 +2435,7 @@ def run(self, file, globals=None, locals=None, is_module=False, set_trace=True): try: if INTERACTIVE_MODE_AVAILABLE: - self.init_matplotlib_support() + self.init_gui_support() except: pydev_log.exception("Matplotlib support in debugger failed") @@ -2441,7 +2480,7 @@ def _exec(self, is_module, entry_point_fn, module_name, file, globals, locals): return globals def wait_for_commands(self, globals): - self._activate_mpl_if_needed() + self._activate_gui_if_needed() thread = threading.current_thread() from _pydevd_bundle import pydevd_frame_utils @@ -2455,9 +2494,9 @@ def wait_for_commands(self, globals): self.writer.add_command(cmd) while True: - if self.mpl_in_use: - # call input hooks if only matplotlib is in use - self._call_mpl_hook() + if self.gui_in_use: + # call input hooks if only GUI is in use + self._call_input_hook() self.process_internal_commands() time.sleep(0.01) @@ -2849,7 +2888,7 @@ def _locked_settrace( try: if INTERACTIVE_MODE_AVAILABLE: - py_db.init_matplotlib_support() + py_db.init_gui_support() except: pydev_log.exception("Matplotlib support in debugger failed") diff --git a/src/debugpy/_vendored/pydevd/tests_python/debugger_unittest.py b/src/debugpy/_vendored/pydevd/tests_python/debugger_unittest.py index d2998a977..5219a878e 100644 --- a/src/debugpy/_vendored/pydevd/tests_python/debugger_unittest.py +++ b/src/debugpy/_vendored/pydevd/tests_python/debugger_unittest.py @@ -701,6 +701,9 @@ def _ignore_stderr_line(self, line): for expected in ( 'PyDev console: using IPython', 'Attempting to work in a virtualenv. If you encounter problems, please', + 'Unable to create basic Accelerated OpenGL', # Issue loading qt5 + 'Core Image is now using the software OpenGL', # Issue loading qt5 + 'XDG_RUNTIME_DIR not set', # Issue loading qt5 ): if expected in line: return True diff --git a/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_gui_event_loop.py b/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_gui_event_loop.py new file mode 100644 index 000000000..5b0c5d254 --- /dev/null +++ b/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_gui_event_loop.py @@ -0,0 +1,26 @@ +class LoopHolder: + + @staticmethod + def gui_loop(): + print('gui_loop() called') + + +def call_method(): + from _pydevd_bundle.pydevd_constants import get_global_debugger + py_db = get_global_debugger() + + # Check state prior to breaking + assert not py_db.gui_in_use + assert py_db._installed_gui_support + assert py_db._gui_event_loop == '__main__.LoopHolder.gui_loop' + + print('break here') + + assert py_db.gui_in_use + assert py_db._installed_gui_support + assert py_db._gui_event_loop == '__main__.LoopHolder.gui_loop' + + +if __name__ == '__main__': + call_method() + print('TEST SUCEEDED!') diff --git a/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_gui_event_loop_qt5.py b/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_gui_event_loop_qt5.py new file mode 100644 index 000000000..c9d3871af --- /dev/null +++ b/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_gui_event_loop_qt5.py @@ -0,0 +1,51 @@ +def call_method(): + + from _pydevd_bundle.pydevd_constants import get_global_debugger + py_db = get_global_debugger() + + # Check state prior to breaking + assert not py_db.gui_in_use + assert py_db._installed_gui_support + assert py_db._gui_event_loop == 'qt5' + + import os + import PySide2 + from PySide2.QtCore import QTimer + + dirname = os.path.dirname(PySide2.__file__) + plugin_path = os.path.join(dirname, 'plugins', 'platforms') + if os.path.exists(plugin_path): + os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = plugin_path + + from PySide2 import QtWidgets + + app = QtWidgets.QApplication([]) + + def on_timeout(): + print('on_timeout() called') + + print_timer = QTimer() + print_timer.timeout.connect(on_timeout) + print_timer.setInterval(100) + print_timer.start() + + def on_break(): + print('break here') + app.quit() + + break_on_timer = QTimer() + break_on_timer.timeout.connect(on_break) + break_on_timer.setSingleShot(True) + break_on_timer.setInterval(50) + break_on_timer.start() + + app.exec_() # Run forever until app.quit() + + assert py_db.gui_in_use + assert py_db._installed_gui_support + assert py_db._gui_event_loop == 'qt5' + + +if __name__ == '__main__': + call_method() + print('TEST SUCEEDED!') diff --git a/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py b/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py index cffada86c..5499010b5 100644 --- a/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py +++ b/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py @@ -3949,8 +3949,11 @@ def get_environ(writer): 'Hub: run - hub.py' )) - for _tname, tid in thread_name_to_id.items(): - stack = json_facade.get_stack_as_json_hit(tid, no_stack_frame=tid == 4) + for tname, tid in thread_name_to_id.items(): + stack = json_facade.get_stack_as_json_hit( + tid, + no_stack_frame=tname == 'Hub: run - hub.py' + ) assert stack else: @@ -5201,6 +5204,14 @@ def test_stop_on_entry2(case_setup): def test_debug_options(case_setup, val): with case_setup.test_file('_debugger_case_debug_options.py') as writer: json_facade = JsonFacade(writer) + gui_event_loop = 'matplotlib' + if val: + try: + import PySide2.QtCore + except ImportError: + pass + else: + gui_event_loop = 'qt5' args = dict( justMyCode=val, redirectOutput=True, # Always redirect the output regardless of other values. @@ -5210,6 +5221,7 @@ def test_debug_options(case_setup, val): flask=val, stopOnEntry=val, maxExceptionStackFrames=4 if val else 5, + guiEventLoop=gui_event_loop, ) json_facade.write_launch(**args) @@ -5232,6 +5244,7 @@ def test_debug_options(case_setup, val): 'breakOnSystemExitZero': 'break_system_exit_zero', 'stopOnEntry': 'stop_on_entry', 'maxExceptionStackFrames': 'max_exception_stack_frames', + 'guiEventLoop': 'gui_event_loop', } assert json.loads(output.body.output) == dict((translation[key], val) for key, val in args.items()) @@ -5239,6 +5252,50 @@ def test_debug_options(case_setup, val): writer.finished_ok = True +def test_gui_event_loop_custom(case_setup): + with case_setup.test_file('_debugger_case_gui_event_loop.py') as writer: + json_facade = JsonFacade(writer) + json_facade.write_launch(guiEventLoop='__main__.LoopHolder.gui_loop', redirectOutput=True) + break_line = writer.get_line_index_with_content('break here') + json_facade.write_set_breakpoints(break_line) + + json_facade.write_make_initial_run() + json_facade.wait_for_thread_stopped() + + json_facade.wait_for_json_message( + OutputEvent, lambda msg: msg.body.category == 'stdout' and 'gui_loop() called' in msg.body.output) + + json_facade.write_continue() + json_facade.wait_for_terminated() + writer.finished_ok = True + + +def test_gui_event_loop_qt5(case_setup): + try: + from PySide2 import QtCore + except ImportError: + pytest.skip('PySide2 not available') + + with case_setup.test_file('_debugger_case_gui_event_loop_qt5.py') as writer: + json_facade = JsonFacade(writer) + json_facade.write_launch(guiEventLoop='qt5', redirectOutput=True) + break_line = writer.get_line_index_with_content('break here') + json_facade.write_set_breakpoints(break_line) + + json_facade.write_make_initial_run() + json_facade.wait_for_thread_stopped() + + # i.e.: if we don't have the event loop running in this test, this + # output is not shown (as the QTimer timeout wouldn't be executed). + for _i in range(3): + json_facade.wait_for_json_message( + OutputEvent, lambda msg: msg.body.category == 'stdout' and 'on_timeout() called' in msg.body.output) + + json_facade.write_continue() + json_facade.wait_for_terminated() + writer.finished_ok = True + + @pytest.mark.parametrize('debug_stdlib', [True, False]) def test_just_my_code_debug_option_deprecated(case_setup, debug_stdlib, debugger_runner_simple): from _pydev_bundle import pydev_log diff --git a/src/debugpy/_vendored/pydevd/tests_python/test_utilities.py b/src/debugpy/_vendored/pydevd/tests_python/test_utilities.py index 32f5d20ea..d8fa3a673 100644 --- a/src/debugpy/_vendored/pydevd/tests_python/test_utilities.py +++ b/src/debugpy/_vendored/pydevd/tests_python/test_utilities.py @@ -556,3 +556,16 @@ def run(self): assert t1 in threading.enumerate() t0.event.set() t1.event.set() + + +def test_import_token_from_module(): + from _pydevd_bundle.pydevd_utils import import_attr_from_module + + with pytest.raises(ImportError): + import_attr_from_module('sys') + + with pytest.raises(ImportError): + import_attr_from_module('sys.settrace.foo') + + assert import_attr_from_module('sys.settrace') == sys.settrace + assert import_attr_from_module('threading.Thread.start') == threading.Thread.start