Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option for enable gui event loop when not using matplotlib #787

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class DebugOptions(object):
'flask_debug',
'stop_on_entry',
'max_exception_stack_frames',
'gui_event_loop',
]

def __init__(self):
Expand All @@ -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 = {}
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
89 changes: 63 additions & 26 deletions src/debugpy/_vendored/pydevd/pydevd.py
Original file line number Diff line number Diff line change
Expand Up @@ -601,9 +601,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 = {}

Expand Down Expand Up @@ -1506,14 +1516,15 @@ 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
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:
Expand All @@ -1529,21 +1540,46 @@ def return_control():
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):
if module in sys.modules:
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 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_mpl_hook(self):
def _call_input_hook(self):
try:
from pydev_ipython.inputhook import get_inputhook
inputhook = get_inputhook()
Expand Down Expand Up @@ -1688,7 +1724,7 @@ def process_internal_commands(self):
# 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
Expand Down Expand Up @@ -1990,22 +2026,23 @@ 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
if info.pydev_state != STATE_SUSPEND or (self.pydb_disposed and not self.terminate_requested):
# 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)
Expand Down Expand Up @@ -2380,7 +2417,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")

Expand Down Expand Up @@ -2425,7 +2462,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
Expand All @@ -2439,9 +2476,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)

Expand Down Expand Up @@ -2833,7 +2870,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")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5162,6 +5162,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)

Expand All @@ -5184,6 +5185,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())
Expand Down