Skip to content

Commit b645ed7

Browse files
committed
Rewrite to new behavior when trying to import api, closes #154
1 parent f0ab3ae commit b645ed7

File tree

1 file changed

+191
-94
lines changed

1 file changed

+191
-94
lines changed

qtpy/__init__.py

Lines changed: 191 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,46 @@
88

99
"""
1010
**QtPy** is a shim over the various Python Qt bindings. It is used to write
11-
Qt binding indenpendent libraries or applications.
11+
Qt binding independent libraries or applications.
1212
1313
If one of the APIs has already been imported, then it will be used.
1414
15-
Otherwise, the shim will automatically select the first available API (PyQt5,
16-
PySide2, PyQt4 and finally PySide); in that case, you can force the use of one
15+
Otherwise, the shim will automatically select the first available API
16+
following the list; in that case, you can force the use of one
1717
specific bindings (e.g. if your application is using one specific bindings and
1818
you need to use library that use QtPy) by setting up the ``QT_API`` environment
1919
variable.
2020
21+
For each binding selected, there are more three attempts if it not found,
22+
following the most recent (Qt5) and most stable (PyQt) API. See bellow:
23+
24+
* PyQt5: PySide2, PyQt4, PySide
25+
* PySide2: PyQt5, PyQt4, PySide
26+
* PyQt4: PySide, PyQt5, PySide2
27+
* PySide: PyQt4, PyQt5, PySide2
28+
29+
The default value for QT_API is PyQt5 (not case sensitive).
30+
31+
The clearest way to set which API is to be used by QtPy is setting``QT_API``
32+
environment variable.
33+
If any of binding is imported directly anywhere, but before you import QtPy,
34+
this binding will be used, overwriting your definition.
35+
36+
Priority when setting the Qt binding API:
37+
38+
1 Have been already imported any Qt binding (not recommended):
39+
1.1 QT_API is not set, pass, no output;
40+
1.2 QT_API is set to the same binding, pass, no output;
41+
1.3 QT_API is set to differ binding, ignore QT_API, pass but warns;
42+
43+
2 Have NOT been already imported any Qt binding:
44+
2.1 QT_API is set correctly, pass;
45+
2.1.1 If binding is found, pass, no output;
46+
2.1.2 If binding is not found, try another one (more three):
47+
2.1.2.a If any is found (different from set), pass but warns;
48+
2.1.2.b If no one is found, stop, error;
49+
2.2 QT_API is not set correctly, stop, error;
50+
2151
PyQt5
2252
=====
2353
@@ -28,40 +58,41 @@
2858
2959
3060
PySide2
31-
======
61+
=======
3262
33-
Set the QT_API environment variable to 'pyside2' before importing other
63+
Set the QT_API environment variable to 'PySide2' before importing other
3464
packages::
3565
3666
>>> import os
37-
>>> os.environ['QT_API'] = 'pyside2'
67+
>>> os.environ['QT_API'] = 'PySide2'
3868
>>> from qtpy import QtGui, QtWidgets, QtCore
3969
>>> print(QtWidgets.QWidget)
4070
4171
PyQt4
4272
=====
4373
44-
Set the ``QT_API`` environment variable to 'pyqt' before importing any python
74+
Set the ``QT_API`` environment variable to 'PyQt4' before importing any python
4575
package::
4676
4777
>>> import os
48-
>>> os.environ['QT_API'] = 'pyqt'
78+
>>> os.environ['QT_API'] = 'PyQt4'
4979
>>> from qtpy import QtGui, QtWidgets, QtCore
5080
>>> print(QtWidgets.QWidget)
5181
5282
PySide
5383
======
5484
55-
Set the QT_API environment variable to 'pyside' before importing other
85+
Set the QT_API environment variable to 'PySide' before importing other
5686
packages::
5787
5888
>>> import os
59-
>>> os.environ['QT_API'] = 'pyside'
89+
>>> os.environ['QT_API'] = 'PySide'
6090
>>> from qtpy import QtGui, QtWidgets, QtCore
6191
>>> print(QtWidgets.QWidget)
6292
6393
"""
6494

95+
import importlib.util
6596
import os
6697
import sys
6798
import warnings
@@ -71,7 +102,7 @@
71102

72103

73104
class PythonQtError(Exception):
74-
"""Error raise if no bindings could be selected"""
105+
"""Error raise if no bindings could be selected."""
75106
pass
76107

77108

@@ -80,110 +111,176 @@ class PythonQtWarning(Warning):
80111
pass
81112

82113

83-
# Qt API environment variable name
84-
QT_API = 'QT_API'
114+
def get_imported_api(apis_to_search):
115+
"""Return an ordered list of Qt bindings that have been already imported."""
116+
imported_api = []
85117

86-
# Names of the expected PyQt5 api
87-
PYQT5_API = ['pyqt5']
118+
for api_name in apis_to_search:
119+
if api_name in sys.modules:
120+
imported_api.append(api_name)
88121

89-
# Names of the expected PyQt4 api
90-
PYQT4_API = [
91-
'pyqt', # name used in IPython.qt
92-
'pyqt4' # pyqode.qt original name
93-
]
122+
return imported_api
94123

95-
# Names of the expected PySide api
96-
PYSIDE_API = ['pyside']
97124

98-
# Names of the expected PySide2 api
99-
PYSIDE2_API = ['pyside2']
125+
def get_available_api(apis_to_search):
126+
"""Return an ordered list of Qt bindings that are available (installed)."""
127+
available_api = []
100128

101-
# Setting a default value for QT_API
102-
os.environ.setdefault(QT_API, 'pyqt5')
129+
for api_name in apis_to_search:
130+
module_spec = importlib.util.find_spec(api_name)
131+
if module_spec:
132+
available_api.append(api_name)
103133

104-
API = os.environ[QT_API].lower()
105-
initial_api = API
106-
assert API in (PYQT5_API + PYQT4_API + PYSIDE_API + PYSIDE2_API)
134+
return available_api
107135

108-
is_old_pyqt = is_pyqt46 = False
109-
PYQT5 = True
110-
PYQT4 = PYSIDE = PYSIDE2 = False
111136

137+
def get_api_information(api_name):
138+
"""Get API information of version and Qt version, also"""
112139

113-
if 'PyQt5' in sys.modules:
114-
API = 'pyqt5'
115-
elif 'PySide2' in sys.modules:
116-
API = 'pyside2'
117-
elif 'PyQt4' in sys.modules:
118-
API = 'pyqt4'
119-
elif 'PySide' in sys.modules:
120-
API = 'pyside'
140+
if api_name == 'PyQt4':
141+
try:
142+
import sip
143+
try:
144+
sip.setapi('QString', 2)
145+
sip.setapi('QVariant', 2)
146+
sip.setapi('QDate', 2)
147+
sip.setapi('QDateTime', 2)
148+
sip.setapi('QTextStream', 2)
149+
sip.setapi('QTime', 2)
150+
sip.setapi('QUrl', 2)
151+
except (AttributeError, ValueError):
152+
# PyQt < v4.6
153+
pass
154+
from PyQt4.Qt import PYQT_VERSION_STR as api_version # analysis:ignore
155+
from PyQt4.Qt import QT_VERSION_STR as qt_version # analysis:ignore
156+
except ImportError:
157+
raise PythonQtError('PyQt4 cannot be imported in QtPy.')
158+
159+
elif api_name == 'PyQt5':
160+
try:
161+
from PyQt5.QtCore import PYQT_VERSION_STR as api_version # analysis:ignore
162+
from PyQt5.QtCore import QT_VERSION_STR as qt_version # analysis:ignore
163+
except ImportError:
164+
raise PythonQtError('PyQt5 cannot be imported in QtPy.')
121165

166+
elif api_name == 'PySide':
167+
try:
168+
from PySide import __version__ as api_version # analysis:ignore
169+
from PySide.QtCore import __version__ as qt_version # analysis:ignore
170+
except ImportError:
171+
raise PythonQtError('PySide cannot be imported in QtPy.')
122172

123-
if API in PYQT5_API:
124-
try:
125-
from PyQt5.QtCore import PYQT_VERSION_STR as PYQT_VERSION # analysis:ignore
126-
from PyQt5.QtCore import QT_VERSION_STR as QT_VERSION # analysis:ignore
127-
PYSIDE_VERSION = None
128-
except ImportError:
129-
API = os.environ['QT_API'] = 'pyside2'
173+
elif api_name == 'PySide2':
174+
try:
175+
from PySide2 import __version__ as api_version # analysis:ignore
176+
from PySide2.QtCore import __version__ as qt_version # analysis:ignore
177+
except ImportError:
178+
raise PythonQtError('PySide2 cannot be imported in QtPy.')
179+
else:
180+
return (None, None)
130181

131-
if API in PYSIDE2_API:
132-
try:
133-
from PySide2 import __version__ as PYSIDE_VERSION # analysis:ignore
134-
from PySide2.QtCore import __version__ as QT_VERSION # analysis:ignore
182+
return (api_version, qt_version)
135183

136-
PYQT_VERSION = None
137-
PYQT5 = False
138-
PYSIDE2 = True
139-
except ImportError:
140-
API = os.environ['QT_API'] = 'pyqt'
141184

142-
if API in PYQT4_API:
143-
try:
144-
import sip
145-
try:
146-
sip.setapi('QString', 2)
147-
sip.setapi('QVariant', 2)
148-
sip.setapi('QDate', 2)
149-
sip.setapi('QDateTime', 2)
150-
sip.setapi('QTextStream', 2)
151-
sip.setapi('QTime', 2)
152-
sip.setapi('QUrl', 2)
153-
except (AttributeError, ValueError):
154-
# PyQt < v4.6
155-
pass
156-
from PyQt4.Qt import PYQT_VERSION_STR as PYQT_VERSION # analysis:ignore
157-
from PyQt4.Qt import QT_VERSION_STR as QT_VERSION # analysis:ignore
158-
PYSIDE_VERSION = None
159-
PYQT5 = False
160-
PYQT4 = True
161-
except ImportError:
162-
API = os.environ['QT_API'] = 'pyside'
163-
else:
164-
is_old_pyqt = PYQT_VERSION.startswith(('4.4', '4.5', '4.6', '4.7'))
165-
is_pyqt46 = PYQT_VERSION.startswith('4.6')
185+
# Qt API environment variable name
186+
QT_API = 'QT_API'
187+
188+
# Keys: names of the expected Qt API (internal names)
189+
# Values: ordered list of importing names based on its key
190+
# The sequence preserves the most recent (Qt5) and stable (PyQt) api
191+
api_names = {'pyqt4': ['PyQt4', 'PySide', 'PyQt5', 'PySide2'],
192+
'pyqt5': ['PyQt5', 'PySide2', 'PyQt4', 'PySide'],
193+
'pyside': ['PySide', 'PyQt4', 'PyQt5', 'PySide2'],
194+
'pyside2': ['PySide2', 'PyQt5', 'PyQt4', 'PySide']}
166195

167-
if API in PYSIDE_API:
196+
# Other keys for the same Qt API that can be used, for compatibility
197+
# pyqt4 -> pyqode.qt original name
198+
# pyqt -> name used in IPython.qt
199+
api_names['pyqt'] = api_names['pyqt4']
200+
201+
# Default/Preferrable API, must be one of api_names keys
202+
default_api = 'pyqt5'
203+
204+
# Check if API's are available, maybe overtested
205+
if not get_available_api(api_names[default_api]):
206+
raise PythonQtError('No Qt API can be imported. Please, install at least '
207+
'one of these: {}.'.format(api_names[default_api]))
208+
209+
# All False/None because they were not imported yet
210+
PYQT5 = PYQT4 = PYSIDE = PYSIDE2 = False
211+
API_VERSION = QT_VERSION = ''
212+
213+
is_old_pyqt = is_pyqt46 = False
214+
api_trial = []
215+
216+
# Setting a default value for QT_API and get the value from environment
217+
os.environ.setdefault(QT_API, default_api)
218+
env_api = os.environ[QT_API].lower()
219+
220+
# Check if it was correctly set with environment variable
221+
if env_api not in api_names.keys():
222+
raise PythonQtError('Qt binding "{}" is unknown, please use a name '
223+
'(not case sensitive) from {}'.format(env_api,
224+
list(api_names.keys())))
225+
environment_api_list = api_names[env_api]
226+
227+
# Check if Qt binding was already imported in 'sys.modules'
228+
# The preference sequence is given by env_api if set or by default_api
229+
imported_api_list = get_imported_api(api_names[env_api])
230+
231+
# If more than one Qt binding is imported, just warns for now
232+
if len(imported_api_list) >= 2:
233+
warnings.warn('There is more than one imported Qt binding {}.'
234+
'This may cause some issues, check your code '
235+
'consistence'.format(imported_api_list), RuntimeWarning)
236+
237+
# Priority for imported binding(s), even QT_API is set
238+
if imported_api_list:
239+
api_trial = imported_api_list
240+
else:
241+
api_trial = environment_api_list
242+
243+
# Initial value for API is get always from environment
244+
initial_api = environment_api_list[0]
245+
246+
for api_name in api_trial:
168247
try:
169-
from PySide import __version__ as PYSIDE_VERSION # analysis:ignore
170-
from PySide.QtCore import __version__ as QT_VERSION # analysis:ignore
171-
PYQT_VERSION = None
172-
PYQT5 = PYSIDE2 = False
173-
PYSIDE = True
174-
except ImportError:
175-
raise PythonQtError('No Qt bindings could be found')
176-
177-
# If a correct API name is passed to QT_API and it could not be found,
248+
api_version, qt_version = get_api_information(api_name)
249+
except PythonQtError:
250+
pass
251+
else:
252+
if api_version and qt_version:
253+
API = api_name
254+
API_VERSION = api_version
255+
QT_VERSION = qt_version
256+
if API == 'PyQt4':
257+
PYQT4 = True
258+
elif API == 'PyQt5':
259+
PYQT5 = True
260+
elif API == 'PySide':
261+
PYSIDE = True
262+
elif API == 'PySide2':
263+
PYSIDE2 = True
264+
break
265+
266+
# If a correct API name is passed to QT_API and it cannot be found,
178267
# switches to another and informs through the warning
179268
if API != initial_api:
180-
warnings.warn('Selected binding "{}" could not be found, '
181-
'using "{}"'.format(initial_api, API), RuntimeWarning)
269+
if imported_api_list:
270+
# If the code is using QtPy is not supposed do directly import Qt api's,
271+
# so a warning is sent to check consistence
272+
warnings.warn('Selected binding "{}" could not be set because there is '
273+
'already imported "{}", please check your code for '
274+
'consistence'.format(initial_api, API), RuntimeWarning)
275+
else:
276+
warnings.warn('Selected binding "{}" could not be found, '
277+
'using "{}"'.format(initial_api, API), RuntimeWarning)
182278

183-
API_NAME = {'pyqt5': 'PyQt5', 'pyqt': 'PyQt4', 'pyqt4': 'PyQt4',
184-
'pyside': 'PySide', 'pyside2':'PySide2'}[API]
279+
API_NAME = API
185280

186281
if PYQT4:
282+
is_old_pyqt = API_VERSION.startswith(('4.4', '4.5', '4.6', '4.7'))
283+
is_pyqt46 = API_VERSION.startswith('4.6')
187284
import sip
188285
try:
189286
API_NAME += (" (API v{0})".format(sip.getapi('QString')))

0 commit comments

Comments
 (0)