Add option for injecting custom GUI event loop

This commit is contained in:
chubei 2021-11-23 14:23:17 +08:00 committed by Fabio Zadrozny
parent fba80681ad
commit 043b11fd12
5 changed files with 74 additions and 26 deletions

View file

@ -125,6 +125,9 @@ class PyDevdAPI(object):
'''
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,))

View file

@ -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 @@ class DebugOptions(object):
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 @@ class DebugOptions(object):
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:

View file

@ -460,6 +460,8 @@ class PyDevJsonCommandProcessor(object):
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:

View file

@ -614,9 +614,19 @@ class PyDB(object):
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,14 +1532,15 @@ class PyDB(object):
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
self._installed_gui_support = True
# prepare debugger for integration with GUI event loop
from pydev_ipython.matplotlibtools import activate_matplotlib, activate_pylab, activate_pyplot, do_enable_gui
from pydev_ipython.inputhook import enable_gui
# enable_gui_function in activate_matplotlib should be called in main thread. Unlike integrated console,
# enalbe_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:
@ -1545,11 +1556,14 @@ class PyDB(object):
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':
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_mpl_if_needed(self):
def _activate_gui_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 +1571,31 @@ class PyDB(object):
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
def _call_mpl_hook(self):
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 importlib import import_module
from pydev_ipython.inputhook import set_inputhook
try:
module_name, inputhook_name = self._gui_event_loop.rsplit('.', 1)
module = import_module(module_name)
inputhook_function = getattr(module, inputhook_name)
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_input_hook(self):
try:
from pydev_ipython.inputhook import get_inputhook
inputhook = get_inputhook()
@ -1704,7 +1740,7 @@ class PyDB(object):
# 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 +2042,13 @@ class PyDB(object):
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 +2056,9 @@ class PyDB(object):
# 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 +2433,7 @@ class PyDB(object):
try:
if INTERACTIVE_MODE_AVAILABLE:
self.init_matplotlib_support()
self.init_gui_support()
except:
pydev_log.exception("Matplotlib support in debugger failed")
@ -2441,7 +2478,7 @@ class PyDB(object):
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 +2492,9 @@ class PyDB(object):
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 +2886,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")

View file

@ -5210,6 +5210,7 @@ def test_debug_options(case_setup, val):
flask=val,
stopOnEntry=val,
maxExceptionStackFrames=4 if val else 5,
guiEventLoop='qt5' if val else 'matplotlib',
)
json_facade.write_launch(**args)
@ -5232,6 +5233,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())