Skip to content

Commit

Permalink
Fix problems when both PyQt5 & PySide2 are in venv
Browse files Browse the repository at this point in the history
See the explanatory comment in fbs_runtime/application_context/PyQt5.py.
  • Loading branch information
mherrmann committed Jun 10, 2019
1 parent 1327c4d commit 17a2665
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from fbs_runtime.application_context import ApplicationContext
from fbs_runtime.application_context.${python_bindings} import ApplicationContext
from ${python_bindings}.QtWidgets import QMainWindow

import sys
Expand Down
19 changes: 7 additions & 12 deletions fbs_runtime/_signal.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
try:
from PyQt5.QtNetwork import QAbstractSocket
except ImportError:
from PySide2.QtNetwork import QAbstractSocket

from socket import socketpair, SOCK_DGRAM

import signal

class SignalWakeupHandler(QAbstractSocket):
class SignalWakeupHandler:
"""
Python's `signal` module lets us define custom signal handlers. What we want
in particular is a graceful handling of Ctrl+C, meaning that the app shuts
Expand All @@ -34,22 +29,22 @@ class SignalWakeupHandler(QAbstractSocket):
See: https://stackoverflow.com/a/37229299/1839209
"""
def __init__(self, app):
super().__init__(QAbstractSocket.UdpSocket, app)
def __init__(self, app, QAbstractSocket):
self._app = app
self.old_fd = None
# Create a socket pair
self.wsock, self.rsock = socketpair(type=SOCK_DGRAM)
self.socket = QAbstractSocket(QAbstractSocket.UdpSocket, app)
# Let Qt listen on the one end
self.setSocketDescriptor(self.rsock.fileno())
self.socket.setSocketDescriptor(self.rsock.fileno())
# And let Python write on the other end
self.wsock.setblocking(False)
self.old_fd = signal.set_wakeup_fd(self.wsock.fileno())
# First Python code executed gets any exception from
# the signal handler, so add a dummy handler first
self.readyRead.connect(lambda : None)
self.socket.readyRead.connect(lambda : None)
# Second handler does the real handling
self.readyRead.connect(self._readSignal)
self.socket.readyRead.connect(self._readSignal)
def install(self):
signal.signal(signal.SIGINT, lambda *_: self._app.exit(130))
def __del__(self):
Expand All @@ -60,4 +55,4 @@ def _readSignal(self):
# Read the written byte.
# Note: readyRead is blocked from occurring again until readData()
# was called, so call it, even if you don't need the value.
_ = self.readData(1)
_ = self.socket.readData(1)
31 changes: 31 additions & 0 deletions fbs_runtime/application_context/PyQt5.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""
Earlier fbs versions had the following code:
try:
from PyQt5 import ...
except ImportError:
from PySide2 import ...
This lead to problems when both PyQt5 and PySide2 were on PYTHONPATH:
1) PyInstaller packaged both (!) libraries because it saw both imports.
2) The above made fbs always use PyQt5. But if the user's app uses PySide2,
then PySide2 and PyQt5 classes / code would be mixed.
3) It wasn't clear (or deterministic, really) which Python binding took
precedence. For instance, PyQt5 and PySide2 set different QML search paths.
To fix this problems, the above code was split into separate files: One that
contains all PyQt5 imports, and another that contains all PySide2 imports. The
user is supposed to import precisely one of the two. This makes PyInstaller
only package the one necessary library, and prevents the above problems.
"""

from . import _ApplicationContext, _QtBinding, cached_property
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication
from PyQt5.QtNetwork import QAbstractSocket

class ApplicationContext(_ApplicationContext):
@cached_property
def _qt_binding(self):
return _QtBinding(QApplication, QIcon, QAbstractSocket)
31 changes: 31 additions & 0 deletions fbs_runtime/application_context/PySide2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""
Earlier fbs versions had the following code:
try:
from PyQt5 import ...
except ImportError:
from PySide2 import ...
This lead to problems when both PyQt5 and PySide2 were on PYTHONPATH:
1) PyInstaller packaged both (!) libraries because it saw both imports.
2) The above made fbs always use PyQt5. But if the user's app uses PySide2,
then PySide2 and PyQt5 classes / code would be mixed.
3) It wasn't clear (or deterministic, really) which Python binding took
precedence. For instance, PyQt5 and PySide2 set different QML search paths.
To fix this problems, the above code was split into separate files: One that
contains all PyQt5 imports, and another that contains all PySide2 imports. The
user is supposed to import precisely one of the two. This makes PyInstaller
only package the one necessary library, and prevents the above problems.
"""

from . import _ApplicationContext, _QtBinding, cached_property
from PySide2.QtGui import QIcon
from PySide2.QtWidgets import QApplication
from PySide2.QtNetwork import QAbstractSocket

class ApplicationContext(_ApplicationContext):
@cached_property
def _qt_binding(self):
return _QtBinding(QApplication, QIcon, QAbstractSocket)
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
from collections import namedtuple
from fbs_runtime import _state, _frozen, _source
from fbs_runtime._resources import ResourceLocator
from fbs_runtime._signal import SignalWakeupHandler
from fbs_runtime.excepthook import _Excepthook, StderrExceptionHandler
from fbs_runtime.platform import is_windows, is_mac
from functools import lru_cache

try:
from PyQt5.QtGui import QIcon
except ImportError:
from PySide2.QtGui import QIcon
try:
from PyQt5.QtWidgets import QApplication
except ImportError:
from PySide2.QtWidgets import QApplication

import sys

def cached_property(getter):
Expand All @@ -25,7 +17,7 @@ def cached_property(getter):
"""
return property(lru_cache()(getter))

class ApplicationContext:
class _ApplicationContext:
"""
The main point of contact between your application and fbs. For information
on how to use it, please see the Manual:
Expand All @@ -40,7 +32,8 @@ def __init__(self):
# We don't build as a console app on Windows, so no point in installing
# the SIGINT handler:
if not is_windows():
self._signal_wakeup_handler = SignalWakeupHandler(self.app)
self._signal_wakeup_handler = \
SignalWakeupHandler(self.app, self._qt_binding.QAbstractSocket)
self._signal_wakeup_handler.install()
if self.app_icon:
self.app.setWindowIcon(self.app_icon)
Expand All @@ -57,7 +50,7 @@ def app(self):
this property, eg. if you wish to use your own subclass of QApplication.
An example of this is given in the Manual.
"""
result = QApplication([])
result = self._qt_binding.QApplication([])
result.setApplicationName(self.build_settings['app_name'])
result.setApplicationVersion(self.build_settings['version'])
return result
Expand Down Expand Up @@ -108,7 +101,7 @@ def app_icon(self):
OS there.
"""
if not is_mac():
return QIcon(self.get_resource('Icon.ico'))
return self._qt_binding.QIcon(self.get_resource('Icon.ico'))
@cached_property
def excepthook(self):
"""
Expand All @@ -118,6 +111,10 @@ def excepthook(self):
"""
return _Excepthook(self.exception_handlers)
@cached_property
def _qt_binding(self):
# Implemented in subclasses.
raise NotImplementedError()
@cached_property
def _resource_locator(self):
if is_frozen():
resource_dirs = _frozen.get_resource_dirs()
Expand All @@ -129,6 +126,9 @@ def _project_dir(self):
assert not is_frozen(), 'Only available when running from source'
return _source.get_project_dir()

_QtBinding = \
namedtuple('_QtBinding', ('QApplication', 'QIcon', 'QAbstractSocket'))

def is_frozen():
"""
Return True if running from the frozen (i.e. compiled form) of your app, or
Expand Down

0 comments on commit 17a2665

Please sign in to comment.