hotkeys: use Alt-m for amend on macOS
[git-cola.git] / qtpy / __init__.py
blobd48d746560ce19e6a0e43c8bb729f270b06818e2
2 # Copyright © 2009- The Spyder Development Team
3 # Copyright © 2014-2015 Colin Duquesnoy
5 # Licensed under the terms of the MIT License
6 # (see LICENSE.txt for details)
8 """
9 **QtPy** is a shim over the various Python Qt bindings. It is used to write
10 Qt binding independent libraries or applications.
12 If one of the APIs has already been imported, then it will be used.
14 Otherwise, the shim will automatically select the first available API (PyQt5, PySide2,
15 PyQt6 and PySide6); in that case, you can force the use of one
16 specific bindings (e.g. if your application is using one specific bindings and
17 you need to use library that use QtPy) by setting up the ``QT_API`` environment
18 variable.
20 PyQt5
21 =====
23 For PyQt5, you don't have to set anything as it will be used automatically::
25 >>> from qtpy import QtGui, QtWidgets, QtCore
26 >>> print(QtWidgets.QWidget)
28 PySide2
29 ======
31 Set the QT_API environment variable to 'pyside2' before importing other
32 packages::
34 >>> import os
35 >>> os.environ['QT_API'] = 'pyside2'
36 >>> from qtpy import QtGui, QtWidgets, QtCore
37 >>> print(QtWidgets.QWidget)
39 PyQt6
40 =====
42 >>> import os
43 >>> os.environ['QT_API'] = 'pyqt6'
44 >>> from qtpy import QtGui, QtWidgets, QtCore
45 >>> print(QtWidgets.QWidget)
47 PySide6
48 =======
50 >>> import os
51 >>> os.environ['QT_API'] = 'pyside6'
52 >>> from qtpy import QtGui, QtWidgets, QtCore
53 >>> print(QtWidgets.QWidget)
55 """
57 import contextlib
58 import os
59 import platform
60 import sys
61 import warnings
63 # Version of QtPy
64 __version__ = "2.4.1"
67 class PythonQtError(RuntimeError):
68 """Generic error superclass for QtPy."""
71 class PythonQtWarning(RuntimeWarning):
72 """Warning class for QtPy."""
75 class PythonQtValueError(ValueError):
76 """Error raised if an invalid QT_API is specified."""
79 class QtBindingsNotFoundError(PythonQtError, ImportError):
80 """Error raised if no bindings could be selected."""
82 _msg = "No Qt bindings could be found"
84 def __init__(self):
85 super().__init__(self._msg)
88 class QtModuleNotFoundError(ModuleNotFoundError, PythonQtError):
89 """Raised when a Python Qt binding submodule is not installed/supported."""
91 _msg = "The {name} module was not found."
92 _msg_binding = "{binding}"
93 _msg_extra = ""
95 def __init__(self, *, name, msg=None, **msg_kwargs):
96 global API_NAME
97 binding = self._msg_binding.format(binding=API_NAME)
98 msg = msg or f"{self._msg} {self._msg_extra}".strip()
99 msg = msg.format(name=name, binding=binding, **msg_kwargs)
100 super().__init__(msg, name=name)
103 class QtModuleNotInOSError(QtModuleNotFoundError):
104 """Raised when a module is not supported on the current operating system."""
106 _msg = "{name} does not exist on this operating system."
109 class QtModuleNotInQtVersionError(QtModuleNotFoundError):
110 """Raised when a module is not implemented in the current Qt version."""
112 _msg = "{name} does not exist in {version}."
114 def __init__(self, *, name, msg=None, **msg_kwargs):
115 global QT5, QT6
116 version = "Qt5" if QT5 else "Qt6"
117 super().__init__(name=name, version=version)
120 class QtBindingMissingModuleError(QtModuleNotFoundError):
121 """Raised when a module is not supported by a given binding."""
123 _msg_extra = "It is not currently implemented in {binding}."
126 class QtModuleNotInstalledError(QtModuleNotFoundError):
127 """Raise when a module is supported by the binding, but not installed."""
129 _msg_extra = "It must be installed separately"
131 def __init__(self, *, missing_package=None, **superclass_kwargs):
132 self.missing_package = missing_package
133 if missing_package is not None:
134 self._msg_extra += " as {missing_package}."
135 super().__init__(missing_package=missing_package, **superclass_kwargs)
138 # Qt API environment variable name
139 QT_API = "QT_API"
141 # Names of the expected PyQt5 api
142 PYQT5_API = ["pyqt5"]
144 PYQT6_API = ["pyqt6"]
146 # Names of the expected PySide2 api
147 PYSIDE2_API = ["pyside2"]
149 # Names of the expected PySide6 api
150 PYSIDE6_API = ["pyside6"]
152 # Minimum supported versions of Qt and the bindings
153 QT5_VERSION_MIN = PYQT5_VERSION_MIN = "5.9.0"
154 PYSIDE2_VERSION_MIN = "5.12.0"
155 QT6_VERSION_MIN = PYQT6_VERSION_MIN = PYSIDE6_VERSION_MIN = "6.2.0"
157 QT_VERSION_MIN = QT5_VERSION_MIN
158 PYQT_VERSION_MIN = PYQT5_VERSION_MIN
159 PYSIDE_VERSION_MIN = PYSIDE2_VERSION_MIN
161 # Detecting if a binding was specified by the user
162 binding_specified = QT_API in os.environ
164 API_NAMES = {
165 "pyqt5": "PyQt5",
166 "pyside2": "PySide2",
167 "pyqt6": "PyQt6",
168 "pyside6": "PySide6",
170 API = os.environ.get(QT_API, "pyqt5").lower()
171 initial_api = API
172 if API not in API_NAMES:
173 raise PythonQtValueError(
174 f"Specified QT_API={QT_API.lower()!r} is not in valid options: "
175 f"{API_NAMES}",
178 is_old_pyqt = is_pyqt46 = False
179 QT5 = PYQT5 = True
180 QT4 = QT6 = PYQT4 = PYQT6 = PYSIDE = PYSIDE2 = PYSIDE6 = False
182 PYQT_VERSION = None
183 PYSIDE_VERSION = None
184 QT_VERSION = None
187 def _parse_int(value):
188 """Convert a value into an integer"""
189 try:
190 return int(value)
191 except ValueError:
192 return 0
195 def parse(version):
196 """Parse a version string into a tuple of ints"""
197 return tuple(_parse_int(x) for x in version.split('.'))
200 # Unless `FORCE_QT_API` is set, use previously imported Qt Python bindings
201 if not os.environ.get("FORCE_QT_API"):
202 if "PyQt5" in sys.modules:
203 API = initial_api if initial_api in PYQT5_API else "pyqt5"
204 elif "PySide2" in sys.modules:
205 API = initial_api if initial_api in PYSIDE2_API else "pyside2"
206 elif "PyQt6" in sys.modules:
207 API = initial_api if initial_api in PYQT6_API else "pyqt6"
208 elif "PySide6" in sys.modules:
209 API = initial_api if initial_api in PYSIDE6_API else "pyside6"
211 if API in PYQT5_API:
212 try:
213 from PyQt5.QtCore import (
214 PYQT_VERSION_STR as PYQT_VERSION,
216 from PyQt5.QtCore import (
217 QT_VERSION_STR as QT_VERSION,
220 QT5 = PYQT5 = True
222 if sys.platform == "darwin":
223 macos_version = parse(platform.mac_ver()[0])
224 qt_ver = parse(QT_VERSION)
225 if macos_version < parse("10.10") and qt_ver >= parse("5.9"):
226 raise PythonQtError(
227 "Qt 5.9 or higher only works in "
228 "macOS 10.10 or higher. Your "
229 "program will fail in this "
230 "system.",
232 elif macos_version < parse("10.11") and qt_ver >= parse("5.11"):
233 raise PythonQtError(
234 "Qt 5.11 or higher only works in "
235 "macOS 10.11 or higher. Your "
236 "program will fail in this "
237 "system.",
240 del macos_version
241 del qt_ver
242 except ImportError:
243 API = "pyside2"
244 else:
245 os.environ[QT_API] = API
247 if API in PYSIDE2_API:
248 try:
249 from PySide2 import __version__ as PYSIDE_VERSION # analysis:ignore
250 from PySide2.QtCore import __version__ as QT_VERSION # analysis:ignore
252 PYQT5 = False
253 QT5 = PYSIDE2 = True
255 if sys.platform == "darwin":
256 macos_version = parse(platform.mac_ver()[0])
257 qt_ver = parse(QT_VERSION)
258 if macos_version < parse("10.11") and qt_ver >= parse("5.11"):
259 raise PythonQtError(
260 "Qt 5.11 or higher only works in "
261 "macOS 10.11 or higher. Your "
262 "program will fail in this "
263 "system.",
266 del macos_version
267 del qt_ver
268 except ImportError:
269 API = "pyqt6"
270 else:
271 os.environ[QT_API] = API
273 if API in PYQT6_API:
274 try:
275 from PyQt6.QtCore import (
276 PYQT_VERSION_STR as PYQT_VERSION,
278 from PyQt6.QtCore import (
279 QT_VERSION_STR as QT_VERSION,
282 QT5 = PYQT5 = False
283 QT6 = PYQT6 = True
285 except ImportError:
286 API = "pyside6"
287 else:
288 os.environ[QT_API] = API
290 if API in PYSIDE6_API:
291 try:
292 from PySide6 import __version__ as PYSIDE_VERSION # analysis:ignore
293 from PySide6.QtCore import __version__ as QT_VERSION # analysis:ignore
295 QT5 = PYQT5 = False
296 QT6 = PYSIDE6 = True
298 except ImportError:
299 raise QtBindingsNotFoundError from None
300 else:
301 os.environ[QT_API] = API
304 # If a correct API name is passed to QT_API and it could not be found,
305 # switches to another and informs through the warning
306 if initial_api != API and binding_specified:
307 warnings.warn(
308 f"Selected binding {initial_api!r} could not be found; "
309 f"falling back to {API!r}",
310 PythonQtWarning,
311 stacklevel=2,
315 # Set display name of the Qt API
316 API_NAME = API_NAMES[API]
318 with contextlib.suppress(ImportError, PythonQtError):
319 # QtDataVisualization backward compatibility (QtDataVisualization vs. QtDatavisualization)
320 # Only available for Qt5 bindings > 5.9 on Windows
321 from . import QtDataVisualization as QtDatavisualization # analysis:ignore
324 def _warn_old_minor_version(name, old_version, min_version):
325 """Warn if using a Qt or binding version no longer supported by QtPy."""
326 warning_message = (
327 f"{name} version {old_version} is not supported by QtPy. "
328 "To ensure your application works correctly with QtPy, "
329 f"please upgrade to {name} {min_version} or later."
331 warnings.warn(warning_message, PythonQtWarning, stacklevel=2)
334 # Warn if using an End of Life or unsupported Qt API/binding minor version
335 if QT_VERSION:
336 if QT5 and (parse(QT_VERSION) < parse(QT5_VERSION_MIN)):
337 _warn_old_minor_version("Qt5", QT_VERSION, QT5_VERSION_MIN)
338 elif QT6 and (parse(QT_VERSION) < parse(QT6_VERSION_MIN)):
339 _warn_old_minor_version("Qt6", QT_VERSION, QT6_VERSION_MIN)
341 if PYQT_VERSION:
342 if PYQT5 and (parse(PYQT_VERSION) < parse(PYQT5_VERSION_MIN)):
343 _warn_old_minor_version("PyQt5", PYQT_VERSION, PYQT5_VERSION_MIN)
344 elif PYQT6 and (parse(PYQT_VERSION) < parse(PYQT6_VERSION_MIN)):
345 _warn_old_minor_version("PyQt6", PYQT_VERSION, PYQT6_VERSION_MIN)
346 elif PYSIDE_VERSION:
347 if PYSIDE2 and (parse(PYSIDE_VERSION) < parse(PYSIDE2_VERSION_MIN)):
348 _warn_old_minor_version("PySide2", PYSIDE_VERSION, PYSIDE2_VERSION_MIN)
349 elif PYSIDE6 and (parse(PYSIDE_VERSION) < parse(PYSIDE6_VERSION_MIN)):
350 _warn_old_minor_version("PySide6", PYSIDE_VERSION, PYSIDE6_VERSION_MIN)