##// END OF EJS Templates
Fix memory leak in Qt event loop integration (#14240)...
Hanno Perrey -
Show More
@@ -1,86 +1,88 b''
1 import sys
1 import sys
2 import os
2 import os
3 from IPython.external.qt_for_kernel import QtCore, QtGui, enum_helper
3 from IPython.external.qt_for_kernel import QtCore, QtGui, enum_helper
4 from IPython import get_ipython
4 from IPython import get_ipython
5
5
6 # If we create a QApplication, keep a reference to it so that it doesn't get
6 # If we create a QApplication, keep a reference to it so that it doesn't get
7 # garbage collected.
7 # garbage collected.
8 _appref = None
8 _appref = None
9 _already_warned = False
9 _already_warned = False
10
10
11
11
12 def _exec(obj):
12 def _exec(obj):
13 # exec on PyQt6, exec_ elsewhere.
13 # exec on PyQt6, exec_ elsewhere.
14 obj.exec() if hasattr(obj, "exec") else obj.exec_()
14 obj.exec() if hasattr(obj, "exec") else obj.exec_()
15
15
16
16
17 def _reclaim_excepthook():
17 def _reclaim_excepthook():
18 shell = get_ipython()
18 shell = get_ipython()
19 if shell is not None:
19 if shell is not None:
20 sys.excepthook = shell.excepthook
20 sys.excepthook = shell.excepthook
21
21
22
22
23 def inputhook(context):
23 def inputhook(context):
24 global _appref
24 global _appref
25 app = QtCore.QCoreApplication.instance()
25 app = QtCore.QCoreApplication.instance()
26 if not app:
26 if not app:
27 if sys.platform == 'linux':
27 if sys.platform == 'linux':
28 if not os.environ.get('DISPLAY') \
28 if not os.environ.get('DISPLAY') \
29 and not os.environ.get('WAYLAND_DISPLAY'):
29 and not os.environ.get('WAYLAND_DISPLAY'):
30 import warnings
30 import warnings
31 global _already_warned
31 global _already_warned
32 if not _already_warned:
32 if not _already_warned:
33 _already_warned = True
33 _already_warned = True
34 warnings.warn(
34 warnings.warn(
35 'The DISPLAY or WAYLAND_DISPLAY environment variable is '
35 'The DISPLAY or WAYLAND_DISPLAY environment variable is '
36 'not set or empty and Qt5 requires this environment '
36 'not set or empty and Qt5 requires this environment '
37 'variable. Deactivate Qt5 code.'
37 'variable. Deactivate Qt5 code.'
38 )
38 )
39 return
39 return
40 try:
40 try:
41 QtCore.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
41 QtCore.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
42 except AttributeError: # Only for Qt>=5.6, <6.
42 except AttributeError: # Only for Qt>=5.6, <6.
43 pass
43 pass
44 try:
44 try:
45 QtCore.QApplication.setHighDpiScaleFactorRoundingPolicy(
45 QtCore.QApplication.setHighDpiScaleFactorRoundingPolicy(
46 QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
46 QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
47 )
47 )
48 except AttributeError: # Only for Qt>=5.14.
48 except AttributeError: # Only for Qt>=5.14.
49 pass
49 pass
50 _appref = app = QtGui.QApplication([" "])
50 _appref = app = QtGui.QApplication([" "])
51
51
52 # "reclaim" IPython sys.excepthook after event loop starts
52 # "reclaim" IPython sys.excepthook after event loop starts
53 # without this, it defaults back to BaseIPythonApplication.excepthook
53 # without this, it defaults back to BaseIPythonApplication.excepthook
54 # and exceptions in the Qt event loop are rendered without traceback
54 # and exceptions in the Qt event loop are rendered without traceback
55 # formatting and look like "bug in IPython".
55 # formatting and look like "bug in IPython".
56 QtCore.QTimer.singleShot(0, _reclaim_excepthook)
56 QtCore.QTimer.singleShot(0, _reclaim_excepthook)
57
57
58 event_loop = QtCore.QEventLoop(app)
58 event_loop = QtCore.QEventLoop(app)
59
59
60 if sys.platform == 'win32':
60 if sys.platform == 'win32':
61 # The QSocketNotifier method doesn't appear to work on Windows.
61 # The QSocketNotifier method doesn't appear to work on Windows.
62 # Use polling instead.
62 # Use polling instead.
63 timer = QtCore.QTimer()
63 timer = QtCore.QTimer()
64 timer.timeout.connect(event_loop.quit)
64 timer.timeout.connect(event_loop.quit)
65 while not context.input_is_ready():
65 while not context.input_is_ready():
66 # NOTE: run the event loop, and after 50 ms, call `quit` to exit it.
66 # NOTE: run the event loop, and after 50 ms, call `quit` to exit it.
67 timer.start(50) # 50 ms
67 timer.start(50) # 50 ms
68 _exec(event_loop)
68 _exec(event_loop)
69 timer.stop()
69 timer.stop()
70 else:
70 else:
71 # On POSIX platforms, we can use a file descriptor to quit the event
71 # On POSIX platforms, we can use a file descriptor to quit the event
72 # loop when there is input ready to read.
72 # loop when there is input ready to read.
73 notifier = QtCore.QSocketNotifier(
73 notifier = QtCore.QSocketNotifier(
74 context.fileno(), enum_helper("QtCore.QSocketNotifier.Type").Read
74 context.fileno(), enum_helper("QtCore.QSocketNotifier.Type").Read
75 )
75 )
76 try:
76 try:
77 # connect the callback we care about before we turn it on
77 # connect the callback we care about before we turn it on
78 # lambda is necessary as PyQT inspect the function signature to know
78 # lambda is necessary as PyQT inspect the function signature to know
79 # what arguments to pass to. See https://github.com/ipython/ipython/pull/12355
79 # what arguments to pass to. See https://github.com/ipython/ipython/pull/12355
80 notifier.activated.connect(lambda: event_loop.exit())
80 notifier.activated.connect(lambda: event_loop.exit())
81 notifier.setEnabled(True)
81 notifier.setEnabled(True)
82 # only start the event loop we are not already flipped
82 # only start the event loop we are not already flipped
83 if not context.input_is_ready():
83 if not context.input_is_ready():
84 _exec(event_loop)
84 _exec(event_loop)
85 finally:
85 finally:
86 notifier.setEnabled(False)
86 notifier.setEnabled(False)
87 # make sure that the QObject is being deleted
88 event_loop.setParent(None)
General Comments 0
You need to be logged in to leave comments. Login now