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 (guiEventLoop) #800

Merged
merged 2 commits into from
Dec 4, 2021
Merged
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
26 changes: 26 additions & 0 deletions src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import signal
import os
import ctypes
from importlib import import_module

try:
from urllib import quote
Expand Down Expand Up @@ -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,))
2 changes: 1 addition & 1 deletion src/debugpy/_vendored/pydevd/pydev_ipython/inputhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
14 changes: 8 additions & 6 deletions src/debugpy/_vendored/pydevd/pydev_ipython/inputhookqt5.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,36 +21,39 @@

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

#-----------------------------------------------------------------------------
# Code
#-----------------------------------------------------------------------------


def create_inputhook_qt5(mgr, app=None):
"""Create an input hook for running the Qt5 application event loop.

Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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()
Expand Down
19 changes: 10 additions & 9 deletions src/debugpy/_vendored/pydevd/pydev_ipython/qt_for_kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -92,27 +92,28 @@ 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]

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
36 changes: 28 additions & 8 deletions src/debugpy/_vendored/pydevd/pydev_ipython/qt_loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'


Expand Down Expand Up @@ -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)

Expand All @@ -58,6 +60,7 @@ def commit_api(api):
ID.forbid('PyQt5')
else:
ID.forbid('PySide')
ID.forbid('PySide2')


def loaded_api():
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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',
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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),
Expand All @@ -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)
Expand All @@ -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))
Loading