`_.
diff --git a/doc/users/explain/backends.rst b/doc/users/explain/backends.rst
index e42a489c707b..ca670b82f9ba 100644
--- a/doc/users/explain/backends.rst
+++ b/doc/users/explain/backends.rst
@@ -244,7 +244,8 @@ The :envvar:`QT_API` environment variable can be set to override the search
when nothing has already been loaded. It may be set to (case-insensitively)
PyQt6, PySide6, PyQt5, or PySide2 to pick the version and binding to use. If
the chosen implementation is unavailable, the Qt backend will fail to load
-without attempting any other Qt implementations.
+without attempting any other Qt implementations. See :ref:`QT_bindings` for
+more details.
Using non-builtin backends
--------------------------
diff --git a/lib/matplotlib/backends/__init__.py b/lib/matplotlib/backends/__init__.py
index 6f4015d6ea8e..3e687f85b0be 100644
--- a/lib/matplotlib/backends/__init__.py
+++ b/lib/matplotlib/backends/__init__.py
@@ -1,2 +1,3 @@
# NOTE: plt.switch_backend() (called at import time) will add a "backend"
# attribute here for backcompat.
+_QT_FORCE_QT5_BINDING = False
diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py
index 5234b424d974..76b4d74ef640 100644
--- a/lib/matplotlib/backends/backend_qt.py
+++ b/lib/matplotlib/backends/backend_qt.py
@@ -114,6 +114,28 @@ def _create_qApp():
QtCore.Qt.AA_EnableHighDpiScaling)
except AttributeError: # Only for Qt>=5.6, <6.
pass
+
+ # Check to make sure a QApplication from a different major version
+ # of Qt is not instantiated in the process
+ if QT_API in {'PyQt6', 'PySide6'}:
+ other_bindings = ('PyQt5', 'PySide2')
+ elif QT_API in {'PyQt5', 'PySide2'}:
+ other_bindings = ('PyQt6', 'PySide6')
+ else:
+ raise RuntimeError("Should never be here")
+
+ for binding in other_bindings:
+ mod = sys.modules.get(f'{binding}.QtWidgets')
+ if mod is not None and mod.QApplication.instance() is not None:
+ other_core = sys.modules.get(f'{binding}.QtCore')
+ _api.warn_external(
+ f'Matplotlib is using {QT_API} which wraps '
+ f'{QtCore.qVersion()} however an instantiated '
+ f'QApplication from {binding} which wraps '
+ f'{other_core.qVersion()} exists. Mixing Qt major '
+ 'versions may not work as expected.'
+ )
+ break
try:
QtWidgets.QApplication.setHighDpiScaleFactorRoundingPolicy(
QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py
index 0774356ff8c5..3c6b2c66a845 100644
--- a/lib/matplotlib/backends/backend_qt5.py
+++ b/lib/matplotlib/backends/backend_qt5.py
@@ -1,4 +1,9 @@
-from .backend_qt import (
+from .. import backends
+
+backends._QT_FORCE_QT5_BINDING = True
+
+
+from .backend_qt import ( # noqa
backend_version, SPECIAL_KEYS,
# Public API
cursord, _create_qApp, _BackendQT, TimerQT, MainWindow, FigureCanvasQT,
@@ -9,8 +14,15 @@
FigureCanvasBase, FigureManagerBase, MouseButton, NavigationToolbar2,
TimerBase, ToolContainerBase, figureoptions, Gcf
)
+from . import backend_qt as _backend_qt # noqa
@_BackendQT.export
class _BackendQT5(_BackendQT):
pass
+
+
+def __getattr__(name):
+ if name == 'qApp':
+ return _backend_qt.qApp
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
diff --git a/lib/matplotlib/backends/backend_qt5agg.py b/lib/matplotlib/backends/backend_qt5agg.py
index d4f618df8ea7..c81fa6f6ccb3 100644
--- a/lib/matplotlib/backends/backend_qt5agg.py
+++ b/lib/matplotlib/backends/backend_qt5agg.py
@@ -1,10 +1,11 @@
"""
Render to qt from agg
"""
+from .. import backends
-from .backend_qtagg import _BackendQTAgg
-from .backend_qtagg import ( # noqa: F401 # pylint: disable=W0611
- FigureCanvasQTAgg, FigureManagerQT, NavigationToolbar2QT,
+backends._QT_FORCE_QT5_BINDING = True
+from .backend_qtagg import ( # noqa: F401, E402 # pylint: disable=W0611
+ _BackendQTAgg, FigureCanvasQTAgg, FigureManagerQT, NavigationToolbar2QT,
backend_version, FigureCanvasAgg, FigureCanvasQT
)
diff --git a/lib/matplotlib/backends/backend_qt5cairo.py b/lib/matplotlib/backends/backend_qt5cairo.py
index 02cf9920ce61..a4263f597119 100644
--- a/lib/matplotlib/backends/backend_qt5cairo.py
+++ b/lib/matplotlib/backends/backend_qt5cairo.py
@@ -1,6 +1,9 @@
-from .backend_qtcairo import _BackendQTCairo
-from .backend_qtcairo import ( # noqa: F401 # pylint: disable=W0611
- FigureCanvasQTCairo, FigureCanvasCairo, FigureCanvasQT)
+from .. import backends
+
+backends._QT_FORCE_QT5_BINDING = True
+from .backend_qtcairo import ( # noqa: F401, E402 # pylint: disable=W0611
+ _BackendQTCairo, FigureCanvasQTCairo, FigureCanvasCairo, FigureCanvasQT
+)
@_BackendQTCairo.export
diff --git a/lib/matplotlib/backends/qt_compat.py b/lib/matplotlib/backends/qt_compat.py
index fd35b31dd7e1..47c1cedff741 100644
--- a/lib/matplotlib/backends/qt_compat.py
+++ b/lib/matplotlib/backends/qt_compat.py
@@ -25,6 +25,7 @@
import matplotlib as mpl
from matplotlib import _api
+from . import _QT_FORCE_QT5_BINDING
QT_API_PYQT6 = "PyQt6"
QT_API_PYSIDE6 = "PySide6"
@@ -57,10 +58,16 @@
# requested backend actually matches). Use dict.__getitem__ to avoid
# triggering backend resolution (which can result in a partially but
# incompletely imported backend_qt5).
-elif dict.__getitem__(mpl.rcParams, "backend") in ["Qt5Agg", "Qt5Cairo"]:
+elif (
+ isinstance(dict.__getitem__(mpl.rcParams, "backend"), str) and
+ dict.__getitem__(mpl.rcParams, "backend").lower() in [
+ "qt5agg", "qt5cairo"
+ ]
+):
if QT_API_ENV in ["pyqt5", "pyside2"]:
QT_API = _ETS[QT_API_ENV]
else:
+ _QT_FORCE_QT5_BINDING = True # noqa
QT_API = None
# A non-Qt backend was selected but we still got there (possible, e.g., when
# fully manually embedding Matplotlib in a Qt app without using pyplot).
@@ -112,12 +119,19 @@ def _isdeleted(obj):
if QT_API in [QT_API_PYQT6, QT_API_PYQT5, QT_API_PYSIDE6, QT_API_PYSIDE2]:
_setup_pyqt5plus()
elif QT_API is None: # See above re: dict.__getitem__.
- _candidates = [
- (_setup_pyqt5plus, QT_API_PYQT6),
- (_setup_pyqt5plus, QT_API_PYSIDE6),
- (_setup_pyqt5plus, QT_API_PYQT5),
- (_setup_pyqt5plus, QT_API_PYSIDE2),
- ]
+ if _QT_FORCE_QT5_BINDING:
+ _candidates = [
+ (_setup_pyqt5plus, QT_API_PYQT5),
+ (_setup_pyqt5plus, QT_API_PYSIDE2),
+ ]
+ else:
+ _candidates = [
+ (_setup_pyqt5plus, QT_API_PYQT6),
+ (_setup_pyqt5plus, QT_API_PYSIDE6),
+ (_setup_pyqt5plus, QT_API_PYQT5),
+ (_setup_pyqt5plus, QT_API_PYSIDE2),
+ ]
+
for _setup, QT_API in _candidates:
try:
_setup()
diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py
index 60887b574645..48131a5b1950 100644
--- a/lib/matplotlib/pyplot.py
+++ b/lib/matplotlib/pyplot.py
@@ -104,7 +104,6 @@ def _copy_docstring_and_deprecators(method, func=None):
## Global ##
-
_IP_REGISTERED = None
_INSTALL_FIG_OBSERVER = False
@@ -207,6 +206,28 @@ def _get_required_interactive_framework(backend_mod):
# Inline this once the deprecation elapses.
return backend_mod.FigureCanvas.required_interactive_framework
+_backend_mod = None
+
+
+def _get_backend_mod():
+ """
+ Ensure that a backend is selected and return it.
+
+ This is currently private, but may be made public in the future.
+ """
+ if _backend_mod is None:
+ # Use __getitem__ here to avoid going through the fallback logic (which
+ # will (re)import pyplot and then call switch_backend if we need to
+ # resolve the auto sentinel)
+ switch_backend(dict.__getitem__(rcParams, "backend"))
+ # Just to be safe. Interactive mode can be turned on without calling
+ # `plt.ion()` so register it again here. This is safe because multiple
+ # calls to `install_repl_displayhook` are no-ops and the registered
+ # function respects `mpl.is_interactive()` to determine if it should
+ # trigger a draw.
+ install_repl_displayhook()
+ return _backend_mod
+
def switch_backend(newbackend):
"""
@@ -297,7 +318,7 @@ class backend_mod(matplotlib.backend_bases._Backend):
def _warn_if_gui_out_of_main_thread():
- if (_get_required_interactive_framework(_backend_mod)
+ if (_get_required_interactive_framework(_get_backend_mod())
and threading.current_thread() is not threading.main_thread()):
_api.warn_external(
"Starting a Matplotlib GUI outside of the main thread will likely "
@@ -308,7 +329,7 @@ def _warn_if_gui_out_of_main_thread():
def new_figure_manager(*args, **kwargs):
"""Create a new figure manager instance."""
_warn_if_gui_out_of_main_thread()
- return _backend_mod.new_figure_manager(*args, **kwargs)
+ return _get_backend_mod().new_figure_manager(*args, **kwargs)
# This function's signature is rewritten upon backend-load by switch_backend.
@@ -321,7 +342,7 @@ def draw_if_interactive(*args, **kwargs):
End users will typically not have to call this function because the
the interactive mode takes care of this.
"""
- return _backend_mod.draw_if_interactive(*args, **kwargs)
+ return _get_backend_mod().draw_if_interactive(*args, **kwargs)
# This function's signature is rewritten upon backend-load by switch_backend.
@@ -370,7 +391,7 @@ def show(*args, **kwargs):
explicitly there.
"""
_warn_if_gui_out_of_main_thread()
- return _backend_mod.show(*args, **kwargs)
+ return _get_backend_mod().show(*args, **kwargs)
def isinteractive():
@@ -2226,15 +2247,6 @@ def polar(*args, **kwargs):
set(_interactive_bk) - {'WebAgg', 'nbAgg'})
and cbook._get_running_interactive_framework()):
dict.__setitem__(rcParams, "backend", rcsetup._auto_backend_sentinel)
-# Set up the backend.
-switch_backend(rcParams["backend"])
-
-# Just to be safe. Interactive mode can be turned on without
-# calling `plt.ion()` so register it again here.
-# This is safe because multiple calls to `install_repl_displayhook`
-# are no-ops and the registered function respect `mpl.is_interactive()`
-# to determine if they should trigger a draw.
-install_repl_displayhook()
################# REMAINING CONTENT GENERATED BY boilerplate.py ##############
diff --git a/lib/matplotlib/testing/__init__.py b/lib/matplotlib/testing/__init__.py
index 754277c41f43..eba878e0a4a3 100644
--- a/lib/matplotlib/testing/__init__.py
+++ b/lib/matplotlib/testing/__init__.py
@@ -1,12 +1,13 @@
"""
Helper functions for testing.
"""
-
+from pathlib import Path
+from tempfile import TemporaryDirectory
import locale
import logging
+import os
import subprocess
-from pathlib import Path
-from tempfile import TemporaryDirectory
+import sys
import matplotlib as mpl
from matplotlib import _api
@@ -49,6 +50,46 @@ def setup():
set_reproducibility_for_testing()
+def subprocess_run_helper(func, *args, timeout, **extra_env):
+ """
+ Run a function in a sub-process
+
+ Parameters
+ ----------
+ func : function
+ The function to be run. It must be in a module that is importable.
+
+ *args : str
+ Any additional command line arguments to be passed in
+ the first argument to subprocess.run
+
+ **extra_env : Dict[str, str]
+ Any additional envromental variables to be set for
+ the subprocess.
+
+ """
+ target = func.__name__
+ module = func.__module__
+ proc = subprocess.run(
+ [sys.executable,
+ "-c",
+ f"""
+from {module} import {target}
+{target}()
+""",
+ *args],
+ env={
+ **os.environ,
+ "SOURCE_DATE_EPOCH": "0",
+ **extra_env
+ },
+ timeout=timeout, check=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ universal_newlines=True)
+ return proc
+
+
def _check_for_pgf(texsystem):
"""
Check if a given TeX system + pgf is available
diff --git a/lib/matplotlib/tests/test_backend_tk.py b/lib/matplotlib/tests/test_backend_tk.py
index c3dac0556087..f7bb141d2541 100644
--- a/lib/matplotlib/tests/test_backend_tk.py
+++ b/lib/matplotlib/tests/test_backend_tk.py
@@ -64,6 +64,7 @@ def test_func():
def test_blit(): # pragma: no cover
import matplotlib.pyplot as plt
import numpy as np
+ import matplotlib.backends.backend_tkagg # noqa
from matplotlib.backends import _tkagg
fig, ax = plt.subplots()
diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py
index 346e0e2b967e..2818f3d21cca 100644
--- a/lib/matplotlib/tests/test_backends_interactive.py
+++ b/lib/matplotlib/tests/test_backends_interactive.py
@@ -14,6 +14,7 @@
import matplotlib as mpl
from matplotlib import _c_internal_utils
+from matplotlib.testing import subprocess_run_helper as _run_helper
# Minimal smoke-testing of the backends for which the dependencies are
@@ -87,8 +88,8 @@ def _test_interactive_impl():
"webagg.open_in_browser": False,
"webagg.port_retries": 1,
})
- if len(sys.argv) >= 2: # Second argument is json-encoded rcParams.
- rcParams.update(json.loads(sys.argv[1]))
+
+ rcParams.update(json.loads(sys.argv[1]))
backend = plt.rcParams["backend"].lower()
assert_equal = TestCase().assertEqual
assert_raises = TestCase().assertRaises
@@ -163,27 +164,16 @@ def test_interactive_backend(env, toolbar):
if env["MPLBACKEND"] == "macosx":
if toolbar == "toolmanager":
pytest.skip("toolmanager is not implemented for macosx.")
+ proc = _run_helper(_test_interactive_impl,
+ json.dumps({"toolbar": toolbar}),
+ timeout=_test_timeout,
+ **env)
- proc = subprocess.run(
- [sys.executable, "-c",
- inspect.getsource(_test_interactive_impl)
- + "\n_test_interactive_impl()",
- json.dumps({"toolbar": toolbar})],
- env={**os.environ, "SOURCE_DATE_EPOCH": "0", **env},
- timeout=_test_timeout,
- stdout=subprocess.PIPE, universal_newlines=True)
- if proc.returncode:
- pytest.fail("The subprocess returned with non-zero exit status "
- f"{proc.returncode}.")
assert proc.stdout.count("CloseEvent") == 1
-# The source of this function gets extracted and run in another process, so it
-# must be fully self-contained.
def _test_thread_impl():
from concurrent.futures import ThreadPoolExecutor
- import json
- import sys
from matplotlib import pyplot as plt, rcParams
@@ -191,8 +181,6 @@ def _test_thread_impl():
"webagg.open_in_browser": False,
"webagg.port_retries": 1,
})
- if len(sys.argv) >= 2: # Second argument is json-encoded rcParams.
- rcParams.update(json.loads(sys.argv[1]))
# Test artist creation and drawing does not crash from thread
# No other guarantees!
@@ -246,15 +234,125 @@ def _test_thread_impl():
@pytest.mark.parametrize("env", _thread_safe_backends)
@pytest.mark.flaky(reruns=3)
def test_interactive_thread_safety(env):
- proc = subprocess.run(
- [sys.executable, "-c",
- inspect.getsource(_test_thread_impl) + "\n_test_thread_impl()"],
- env={**os.environ, "SOURCE_DATE_EPOCH": "0", **env},
- timeout=_test_timeout, check=True,
- stdout=subprocess.PIPE, universal_newlines=True)
+ proc = _run_helper(_test_thread_impl,
+ timeout=_test_timeout, **env)
assert proc.stdout.count("CloseEvent") == 1
+def _impl_test_lazy_auto_backend_selection():
+ import matplotlib
+ import matplotlib.pyplot as plt
+ # just importing pyplot should not be enough to trigger resolution
+ bk = dict.__getitem__(matplotlib.rcParams, 'backend')
+ assert not isinstance(bk, str)
+ assert plt._backend_mod is None
+ # but actually plotting should
+ plt.plot(5)
+ assert plt._backend_mod is not None
+ bk = dict.__getitem__(matplotlib.rcParams, 'backend')
+ assert isinstance(bk, str)
+
+
+def test_lazy_auto_backend_selection():
+ _run_helper(_impl_test_lazy_auto_backend_selection,
+ timeout=_test_timeout)
+
+
+def _implqt5agg():
+ import matplotlib.backends.backend_qt5agg # noqa
+ import sys
+
+ assert 'PyQt6' not in sys.modules
+ assert 'pyside6' not in sys.modules
+ assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules
+
+ import matplotlib.backends.backend_qt5
+ matplotlib.backends.backend_qt5.qApp
+
+
+def _implcairo():
+ import matplotlib.backends.backend_qt5cairo # noqa
+ import sys
+
+ assert 'PyQt6' not in sys.modules
+ assert 'pyside6' not in sys.modules
+ assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules
+
+ import matplotlib.backends.backend_qt5
+ matplotlib.backends.backend_qt5.qApp
+
+
+def _implcore():
+ import matplotlib.backends.backend_qt5
+ import sys
+
+ assert 'PyQt6' not in sys.modules
+ assert 'pyside6' not in sys.modules
+ assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules
+ matplotlib.backends.backend_qt5.qApp
+
+
+def test_qt5backends_uses_qt5():
+ qt5_bindings = [
+ dep for dep in ['PyQt5', 'pyside2']
+ if importlib.util.find_spec(dep) is not None
+ ]
+ qt6_bindings = [
+ dep for dep in ['PyQt6', 'pyside6']
+ if importlib.util.find_spec(dep) is not None
+ ]
+ if len(qt5_bindings) == 0 or len(qt6_bindings) == 0:
+ pytest.skip('need both QT6 and QT5 bindings')
+ _run_helper(_implqt5agg, timeout=_test_timeout)
+ if importlib.util.find_spec('pycairo') is not None:
+ _run_helper(_implcairo, timeout=_test_timeout)
+ _run_helper(_implcore, timeout=_test_timeout)
+
+
+def _impl_test_cross_Qt_imports():
+ import sys
+ import importlib
+ import pytest
+
+ _, host_binding, mpl_binding = sys.argv
+ # import the mpl binding. This will force us to use that binding
+ importlib.import_module(f'{mpl_binding}.QtCore')
+ mpl_binding_qwidgets = importlib.import_module(f'{mpl_binding}.QtWidgets')
+ import matplotlib.backends.backend_qt
+ host_qwidgets = importlib.import_module(f'{host_binding}.QtWidgets')
+
+ host_app = host_qwidgets.QApplication(["mpl testing"])
+ with pytest.warns(UserWarning, match="Mixing Qt major"):
+ matplotlib.backends.backend_qt._create_qApp()
+
+
+def test_cross_Qt_imports():
+ qt5_bindings = [
+ dep for dep in ['PyQt5', 'PySide2']
+ if importlib.util.find_spec(dep) is not None
+ ]
+ qt6_bindings = [
+ dep for dep in ['PyQt6', 'PySide6']
+ if importlib.util.find_spec(dep) is not None
+ ]
+ if len(qt5_bindings) == 0 or len(qt6_bindings) == 0:
+ pytest.skip('need both QT6 and QT5 bindings')
+
+ for qt5 in qt5_bindings:
+ for qt6 in qt6_bindings:
+ for pair in ([qt5, qt6], [qt6, qt5]):
+ try:
+ _run_helper(_impl_test_cross_Qt_imports,
+ *pair,
+ timeout=_test_timeout)
+ except subprocess.CalledProcessError as ex:
+ # if segfault, carry on. We do try to warn the user they
+ # are doing something that we do not expect to work
+ if ex.returncode == -11:
+ continue
+ raise
+
+
@pytest.mark.skipif('TF_BUILD' in os.environ,
reason="this test fails an azure for unknown reasons")
@pytest.mark.skipif(os.name == "nt", reason="Cannot send SIGINT on Windows.")
@@ -263,7 +361,7 @@ def test_webagg():
proc = subprocess.Popen(
[sys.executable, "-c",
inspect.getsource(_test_interactive_impl)
- + "\n_test_interactive_impl()"],
+ + "\n_test_interactive_impl()", "{}"],
env={**os.environ, "MPLBACKEND": "webagg", "SOURCE_DATE_EPOCH": "0"})
url = "http://{}:{}".format(
mpl.rcParams["webagg.address"], mpl.rcParams["webagg.port"])
@@ -285,37 +383,33 @@ def test_webagg():
assert proc.wait(timeout=_test_timeout) == 0
+def _lazy_headless():
+ import os
+ import sys
+
+ # make it look headless
+ os.environ.pop('DISPLAY', None)
+ os.environ.pop('WAYLAND_DISPLAY', None)
+
+ # we should fast-track to Agg
+ import matplotlib.pyplot as plt
+ plt.get_backend() == 'agg'
+ assert 'PyQt5' not in sys.modules
+
+ # make sure we really have pyqt installed
+ import PyQt5 # noqa
+ assert 'PyQt5' in sys.modules
+
+ # try to switch and make sure we fail with ImportError
+ try:
+ plt.switch_backend('qt5agg')
+ except ImportError:
+ ...
+ else:
+ sys.exit(1)
+
+
@pytest.mark.skipif(sys.platform != "linux", reason="this a linux-only test")
@pytest.mark.backend('QtAgg', skip_on_importerror=True)
def test_lazy_linux_headless():
- test_script = """
-import os
-import sys
-
-# make it look headless
-os.environ.pop('DISPLAY', None)
-os.environ.pop('WAYLAND_DISPLAY', None)
-
-# we should fast-track to Agg
-import matplotlib.pyplot as plt
-plt.get_backend() == 'agg'
-assert 'PyQt5' not in sys.modules
-
-# make sure we really have pyqt installed
-import PyQt5
-assert 'PyQt5' in sys.modules
-
-# try to switch and make sure we fail with ImportError
-try:
- plt.switch_backend('qt5agg')
-except ImportError:
- ...
-else:
- sys.exit(1)
-
-"""
- proc = subprocess.run([sys.executable, "-c", test_script],
- env={**os.environ, "MPLBACKEND": ""})
- if proc.returncode:
- pytest.fail("The subprocess returned with non-zero exit status "
- f"{proc.returncode}.")
+ proc = _run_helper(_lazy_headless, timeout=_test_timeout, MPLBACKEND="")
diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py
index 2b2e36e2a516..75b6f727f799 100644
--- a/lib/matplotlib/tests/test_rcparams.py
+++ b/lib/matplotlib/tests/test_rcparams.py
@@ -497,7 +497,8 @@ def test_backend_fallback_headless(tmpdir):
[sys.executable, "-c",
"import matplotlib;"
"matplotlib.use('tkagg');"
- "import matplotlib.pyplot"
+ "import matplotlib.pyplot;"
+ "matplotlib.pyplot.plot(42);"
],
env=env, check=True, stderr=subprocess.DEVNULL)
pFad - Phonifier reborn
Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy