diff --git a/.azure/test-linux.yml b/.azure/test-linux.yml index eb4f8517644d..2f8a12c8b451 100644 --- a/.azure/test-linux.yml +++ b/.azure/test-linux.yml @@ -12,6 +12,10 @@ parameters: - name: "testImages" type: boolean + - name: "installOptionals" + type: boolean + default: false + - name: "installFromSdist" type: boolean default: false @@ -84,27 +88,27 @@ jobs: env: SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" + - ${{ if eq(parameters.installOptionals, true) }}: + - bash: | + set -e + source test-job/bin/activate + python -m pip install -r requirements-optional.txt -c constraints.txt + python -m pip check + displayName: "Install optional packages" + + - bash: | + set -e + sudo apt-get update + sudo apt-get install -y graphviz + displayName: 'Install optional non-Python dependencies' + - bash: | set -e source test-job/bin/activate - python -m pip install -U \ - -c constraints.txt \ - "cplex ; python_version < '3.11'" \ - "qiskit-aer" \ - "tweedledum ; python_version < '3.11'" \ - "z3-solver" mkdir -p /tmp/terra-tests cp -r test /tmp/terra-tests/. cp .stestr.conf /tmp/terra-tests/. cp -r .stestr /tmp/terra-tests/. || : - sudo apt-get update - sudo apt-get install -y graphviz - pip check - displayName: 'Install post-install optional dependencies' - - - bash: | - set -e - source test-job/bin/activate pushd /tmp/terra-tests export PYTHONHASHSEED=$(python -S -c "import random; print(random.randint(1, 4294967295))") echo "PYTHONHASHSEED=$PYTHONHASHSEED" @@ -178,7 +182,8 @@ jobs: -c constraints.txt \ -r requirements.txt \ -r requirements-dev.txt \ - -e ".[visualization]" + -r requirements-optional.txt \ + -e . sudo apt-get update sudo apt-get install -y graphviz pandoc image_tests/bin/pip check diff --git a/.azure/test-macos.yml b/.azure/test-macos.yml index 6799d70fdc4e..2b195afbdae3 100644 --- a/.azure/test-macos.yml +++ b/.azure/test-macos.yml @@ -3,6 +3,10 @@ parameters: type: string displayName: "Version of Python to test" + - name: "installOptionals" + type: boolean + default: false + jobs: - job: "MacOS_Tests_Python${{ replace(parameters.pythonVersion, '.', '') }}" displayName: "Test macOS Python ${{ parameters.pythonVersion }}" @@ -45,6 +49,13 @@ jobs: env: SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" + - ${{ if eq(parameters.installOptionals, true) }}: + - bash: | + set -e + source test-job/bin/activate + pip install -r requirements-optional.txt -c constraints.txt + displayName: "Install optional packages" + - bash: | set -e source test-job/bin/activate diff --git a/.azure/test-windows.yml b/.azure/test-windows.yml index e4d4c5b4065b..30591a5dadbe 100644 --- a/.azure/test-windows.yml +++ b/.azure/test-windows.yml @@ -3,6 +3,10 @@ parameters: type: string displayName: "Versions of Python to test" + - name: "installOptionals" + type: boolean + default: false + jobs: - job: "Windows_Tests_Python${{ replace(parameters.pythonVersion, '.', '') }}" displayName: "Test Windows Python ${{ parameters.pythonVersion }}" @@ -38,13 +42,20 @@ jobs: -c constraints.txt \ -r requirements.txt \ -r requirements-dev.txt \ - "z3-solver" \ -e . pip check displayName: 'Install dependencies' env: SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" + - ${{ if eq(parameters.installOptionals, true) }}: + - bash: | + set -e + source test-job/Scripts/activate + pip install -c constraints.txt -r requirements-optional.txt + pip check + displayName: "Install optional packages" + - bash: | set -e chcp.com 65001 diff --git a/.azure/tutorials-linux.yml b/.azure/tutorials-linux.yml index 72beadc37b7d..3e8b3678cabd 100644 --- a/.azure/tutorials-linux.yml +++ b/.azure/tutorials-linux.yml @@ -26,16 +26,8 @@ jobs: -c constraints.txt \ -r requirements.txt \ -r requirements-dev.txt \ - "qiskit-ibmq-provider" \ - "qiskit-aer" \ - "z3-solver" \ - "networkx" \ - "matplotlib>=3.3.0" \ - sphinx \ - nbsphinx \ - sphinx_rtd_theme \ - "tweedledum ; python_version < '3.11'" \ - cvxpy \ + -r requirements-optional.txt \ + -r requirements-tutorials.txt \ -e . sudo apt-get update sudo apt-get install -y graphviz pandoc diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 71681644b2d0..de3485c5fdce 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -52,7 +52,7 @@ jobs: - name: Generate unittest coverage report run: | set -e - python -m pip install -c constraints.txt -r requirements-dev.txt qiskit-aer + python -m pip install -c constraints.txt -r requirements-dev.txt -r requirements-optional.txt stestr run # We set the --source-dir to '.' because we want all paths to appear relative to the repo # root (we need to combine them with the Python ones), but we only care about `grcov` diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 91aee4bd562e..2f04ae2e2ef8 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -103,14 +103,17 @@ stages: testQPY: false testImages: false testRust: false + installOptionals: true - template: ".azure/test-macos.yml" parameters: pythonVersion: ${{ version }} + installOptionals: true - template: ".azure/test-windows.yml" parameters: pythonVersion: ${{ version }} + installOptionals: true - stage: "Nightly_Failure" displayName: "Comment on nightly failure" @@ -150,9 +153,15 @@ stages: - template: ".azure/test-linux.yml" parameters: pythonVersion: ${{ parameters.minimumPythonVersion }} + # A PR is more likely to fail CI because it introduces a logic error + # into an existing test than because it adds a new test / optional + # dependency that isn't accounted for in the test-skipping logic + # (and such a failure would need fewer iterations to fix). We want + # to fail fast in the first CI stage. + installOptionals: true testRust: true testQPY: true - testImages: false + testImages: true # The rest of the PR pipeline is to test the oldest and newest supported # versions of Python, along with the integration tests (via the tutorials). @@ -172,24 +181,29 @@ stages: pythonVersion: ${{ parameters.maximumPythonVersion }} testRust: false testQPY: false - testImages: true + testImages: false installFromSdist: true + installOptionals: false - template: ".azure/test-macos.yml" parameters: pythonVersion: ${{ parameters.minimumPythonVersion }} + installOptionals: true - template: ".azure/test-macos.yml" parameters: pythonVersion: ${{ parameters.maximumPythonVersion }} + installOptionals: false - template: ".azure/test-windows.yml" parameters: pythonVersion: ${{ parameters.minimumPythonVersion }} + installOptionals: true - template: ".azure/test-windows.yml" parameters: pythonVersion: ${{ parameters.maximumPythonVersion }} + installOptionals: false # Push to main or the stable branches. The triggering branches also need to # be in the triggers at the top of this file. @@ -202,6 +216,7 @@ stages: testRust: true testQPY: true testImages: true + installOptionals: false # Push to a tag. The triggering tags are set in the triggers at the top of # this file. diff --git a/qiskit/transpiler/passes/layout/_csp_custom_solver.py b/qiskit/transpiler/passes/layout/_csp_custom_solver.py index d700a60e4c85..8d9884c7c4a0 100644 --- a/qiskit/transpiler/passes/layout/_csp_custom_solver.py +++ b/qiskit/transpiler/passes/layout/_csp_custom_solver.py @@ -25,6 +25,8 @@ class CustomSolver(RecursiveBacktrackingSolver): """A wrap to RecursiveBacktrackingSolver to support ``call_limit``""" + # pylint: disable=invalid-name + def __init__(self, call_limit=None, time_limit=None): self.call_limit = call_limit self.time_limit = time_limit diff --git a/qiskit/utils/optionals.py b/qiskit/utils/optionals.py index 7f87b84444bb..68be4fdc2a6b 100644 --- a/qiskit/utils/optionals.py +++ b/qiskit/utils/optionals.py @@ -87,6 +87,10 @@ - Some methods of gradient calculation within :mod:`.opflow.gradients` require `JAX `__ for autodifferentiation. + * - .. py:data:: HAS_JUPYTER + - Some of the tests require a complete `Jupyter `__ installation to test + interactivity features. + * - .. py:data:: HAS_MATPLOTLIB - Qiskit Terra provides several visualisation tools in the :mod:`.visualization` module. Almost all of these are built using `Matplotlib `__, which must @@ -205,6 +209,9 @@ .. autoclass:: qiskit.utils.LazySubprocessTester """ +# NOTE: If you're changing this file, sync it with `requirements-optional.txt` and potentially +# `setup.py` as well. + import logging as _logging from .lazy_tester import ( @@ -256,6 +263,7 @@ name="jax", install="pip install jax", ) +HAS_JUPYTER = _LazyImportTester(["jupyter", "nbformat", "nbconvert"], install="pip install jupyter") HAS_MATPLOTLIB = _LazyImportTester( ("matplotlib.patches", "matplotlib.pyplot"), name="matplotlib", diff --git a/qiskit/visualization/bloch.py b/qiskit/visualization/bloch.py index e7b4cb96817f..778e4a013546 100644 --- a/qiskit/visualization/bloch.py +++ b/qiskit/visualization/bloch.py @@ -62,6 +62,8 @@ class Arrow3D(Patch3D, FancyArrowPatch): """Makes a fancy arrow""" + # pylint: disable=missing-function-docstring,invalid-name + # Nasty hack around a poorly implemented deprecation warning in Matplotlib 3.5 that issues two # deprecation warnings if an artist's module does not claim to be part of the below module. # This revolves around the method `Patch3D.do_3d_projection(self, renderer=None)`. The diff --git a/requirements-dev.txt b/requirements-dev.txt index fe67acb6ef98..b373c7b08c3b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,30 +1,39 @@ -coverage>=4.4.0,<7.0 -hypothesis>=4.24.3 -python-constraint>=1.4 -ipython<7.22.0 -ipykernel<5.5.2 -ipywidgets>=7.3.0 -jupyter -matplotlib>=3.3 -pillow>=4.2.1 +# Requirements to develop Terra, and the minimum needed to run its CI. All +# optional requirements should go in `requirements-optionals.txt` instead. +# +# Version requirements here can be more restrictive than elsewhere, because they +# never become actual package requirements, but still try to be as relaxed as +# possible so it's easy to develop multiple packages from the same venv. + +# Style black[jupyter]~=22.0 -pydot + + +# Lint +# +# These versions are pinned precisely because pylint frequently includes new +# on-by-default lint failures in new versions, which breaks our CI. astroid==2.14.2 pylint==2.16.2 ruff==0.0.267 + + +# Tests +coverage>=4.4.0 +hypothesis>=4.24.3 stestr>=2.0.0,!=4.0.0 -pylatexenc>=1.4 ddt>=1.2.0,!=1.4.0,!=1.4.3 -seaborn>=0.9.0 + + +# Documentation tooling. +# +# This alone is not sufficient to fully build the documentation, because several +# components of Terra use some of its optional dependencies in order to document +# themselves. These are the requirements that are _only_ required for the docs +# build, and are not used by Terra itself. + # TODO: switch to stable release when 4.1 is released reno @ git+https://github.com/openstack/reno.git@81587f616f17904336cdc431e25c42b46cd75b8f Sphinx>=5.0 qiskit-sphinx-theme~=1.11.0 sphinx-design>=0.2.0 -pygments>=2.4 -scikit-learn>=0.20.0 -scikit-quant<=0.7;platform_system != 'Windows' -jax;platform_system != 'Windows' -jaxlib;platform_system != 'Windows' -docplex -qiskit-qasm3-import diff --git a/requirements-optional.txt b/requirements-optional.txt new file mode 100644 index 000000000000..6afa25271ab9 --- /dev/null +++ b/requirements-optional.txt @@ -0,0 +1,40 @@ +# Optional dependencies of Terra that can (mostly) reliably be installed with +# `pip`. This file is still called `requirements-optional.txt` just to match +# standard pip conventions, even though none of these are required. +# +# If updating this, you probably want to update `qiskit.utils.optionals` and +# maybe `setup.py` too. + +# Test-runner enhancements. +fixtures +testtools +jupyter + +# Interactivity. +ipykernel +ipython +ipywidgets>=7.3.0 +matplotlib>=3.3 +pillow>=4.2.1 +pydot +pygments>=2.4 +pylatexenc>=1.4 +seaborn>=0.9.0 + +# Functionality and accelerators. +qiskit-aer +qiskit-qasm3-import +python-constraint>=1.4 +cplex; python_version < '3.11' +cvxpy +docplex +jax; platform_system != 'Windows' +jaxlib; platform_system != 'Windows' +scikit-learn>=0.20.0 +scikit-quant<=0.7; platform_system != 'Windows' +SQSnobFit +z3-solver>=4.7 +# Tweedledum is unmaintained and its existing Mac wheels are unreliable. If you +# manage to get a working install on a Mac the functionality should still work, +# but as a convenience this file won't attempt the install itself. +tweedledum; python_version<'3.11' and platform_system!="Darwin" diff --git a/requirements-tutorials.txt b/requirements-tutorials.txt new file mode 100644 index 000000000000..5aa9d0c412c0 --- /dev/null +++ b/requirements-tutorials.txt @@ -0,0 +1,10 @@ +# Requirements for the tutorials CI run that go beyond the others in `requirements-optional.txt`. +# This may also include some requirements that are only in `requirements-dev.txt`, since those +# aren't runtime dependencies or optionals of Terra. + +networkx>=2.2 +jupyter +Sphinx +nbsphinx +qiskit_sphinx_theme +pyscf diff --git a/setup.py b/setup.py index 21f64a17b94d..c80aa0296778 100644 --- a/setup.py +++ b/setup.py @@ -31,12 +31,12 @@ flags=re.S | re.M, ) - # If RUST_DEBUG is set, force compiling in debug mode. Else, use the default behavior of whether # it's an editable installation. rust_debug = True if os.getenv("RUST_DEBUG") == "1" else None - +# If modifying these optional extras, make sure to sync with `requirements-optional.txt` and +# `qiskit.utils.optionals` as well. qasm3_import_extras = [ "qiskit-qasm3-import>=0.1.0", ] diff --git a/test/python/circuit/test_equivalence.py b/test/python/circuit/test_equivalence.py index ec3022ce58ea..2a110f2726a3 100644 --- a/test/python/circuit/test_equivalence.py +++ b/test/python/circuit/test_equivalence.py @@ -515,6 +515,7 @@ class TestEquivalenceLibraryVisualization(QiskitVisualizationTestCase): """Test cases for EquivalenceLibrary visualization.""" @unittest.skipUnless(optionals.HAS_GRAPHVIZ, "Graphviz not installed") + @unittest.skipUnless(optionals.HAS_PIL, "PIL not installed") def test_equivalence_draw(self): """Verify EquivalenceLibrary drawing with reference image.""" sel = EquivalenceLibrary() diff --git a/test/python/quantum_info/states/test_densitymatrix.py b/test/python/quantum_info/states/test_densitymatrix.py index 16fbbbc1b9a4..3185758cb469 100644 --- a/test/python/quantum_info/states/test_densitymatrix.py +++ b/test/python/quantum_info/states/test_densitymatrix.py @@ -26,6 +26,7 @@ from qiskit.quantum_info.random import random_density_matrix, random_pauli, random_unitary from qiskit.quantum_info.states import DensityMatrix, Statevector from qiskit.test import QiskitTestCase +from qiskit.utils import optionals logger = logging.getLogger(__name__) @@ -1191,6 +1192,8 @@ def test_reverse_qargs(self): state2 = DensityMatrix.from_instruction(circ2) self.assertEqual(state1.reverse_qargs(), state2) + @unittest.skipUnless(optionals.HAS_MATPLOTLIB, "requires matplotlib") + @unittest.skipUnless(optionals.HAS_PYLATEX, "requires pylatexenc") def test_drawings(self): """Test draw method""" qc1 = QFT(5) diff --git a/test/python/quantum_info/states/test_statevector.py b/test/python/quantum_info/states/test_statevector.py index 4db49ccdd122..a7228d6bf5d2 100644 --- a/test/python/quantum_info/states/test_statevector.py +++ b/test/python/quantum_info/states/test_statevector.py @@ -26,6 +26,7 @@ from qiskit import transpile from qiskit.circuit.library import HGate, QFT, GlobalPhaseGate from qiskit.providers.basicaer import QasmSimulatorPy +from qiskit.utils import optionals from qiskit.quantum_info.random import random_unitary, random_statevector, random_pauli from qiskit.quantum_info.states import Statevector @@ -1200,6 +1201,8 @@ def test_reverse_qargs(self): state2 = Statevector.from_instruction(circ2) self.assertEqual(state1.reverse_qargs(), state2) + @unittest.skipUnless(optionals.HAS_MATPLOTLIB, "requires matplotlib") + @unittest.skipUnless(optionals.HAS_PYLATEX, "requires pylatexenc") def test_drawings(self): """Test draw method""" qc1 = QFT(5) diff --git a/test/python/tools/jupyter/test_notebooks.py b/test/python/tools/jupyter/test_notebooks.py index 14e892947a92..d8369d33fabc 100644 --- a/test/python/tools/jupyter/test_notebooks.py +++ b/test/python/tools/jupyter/test_notebooks.py @@ -16,8 +16,6 @@ import sys import unittest -import nbformat -from nbconvert.preprocessors import ExecutePreprocessor from qiskit.utils import optionals from qiskit.test import Path, QiskitTestCase, slow_test @@ -29,6 +27,7 @@ @unittest.skipUnless(optionals.HAS_IBMQ, "requires IBMQ provider") +@unittest.skipUnless(optionals.HAS_JUPYTER, "involves running Jupyter notebooks") class TestJupyter(QiskitTestCase): """Notebooks test case.""" @@ -41,6 +40,9 @@ def setUp(self): ) def _execute_notebook(self, filename): + import nbformat + from nbconvert.preprocessors import ExecutePreprocessor + # Create the preprocessor. execute_preprocessor = ExecutePreprocessor(timeout=TIMEOUT, kernel_name=JUPYTER_KERNEL) diff --git a/test/python/transpiler/test_coupling.py b/test/python/transpiler/test_coupling.py index 5a41326b1bbf..f573d9abc819 100644 --- a/test/python/transpiler/test_coupling.py +++ b/test/python/transpiler/test_coupling.py @@ -524,6 +524,7 @@ def test_equality(self): class CouplingVisualizationTest(QiskitVisualizationTestCase): @unittest.skipUnless(optionals.HAS_GRAPHVIZ, "Graphviz not installed") + @unittest.skipUnless(optionals.HAS_PIL, "Pillow not installed") def test_coupling_draw(self): """Test that the coupling map drawing with respect to the reference file is correct.""" cmap = CouplingMap([[0, 1], [1, 2], [2, 3], [2, 4], [2, 5], [2, 6]]) diff --git a/test/python/transpiler/test_csp_layout.py b/test/python/transpiler/test_csp_layout.py index 85163b120a8b..7ab369d7a6e8 100644 --- a/test/python/transpiler/test_csp_layout.py +++ b/test/python/transpiler/test_csp_layout.py @@ -21,8 +21,10 @@ from qiskit.converters import circuit_to_dag from qiskit.test import QiskitTestCase from qiskit.providers.fake_provider import FakeTenerife, FakeRueschlikon, FakeTokyo, FakeYorktownV2 +from qiskit.utils import optionals +@unittest.skipUnless(optionals.HAS_CONSTRAINT, "needs python-constraint") class TestCSPLayout(QiskitTestCase): """Tests the CSPLayout pass""" diff --git a/test/python/visualization/test_circuit_drawer.py b/test/python/visualization/test_circuit_drawer.py index 46ed46f19183..663a9a4f8958 100644 --- a/test/python/visualization/test_circuit_drawer.py +++ b/test/python/visualization/test_circuit_drawer.py @@ -201,6 +201,7 @@ def test_reverse_bits(self): result = visualization.circuit_drawer(circuit, output="text", reverse_bits=True) self.assertEqual(result.__str__(), expected) + @unittest.skipUnless(optionals.HAS_PYLATEX, "needs pylatexenc for LaTeX conversion") def test_no_explict_cregbundle(self): """Test no explicit cregbundle should not raise warnings about being disabled See: https://github.com/Qiskit/qiskit-terra/issues/8690""" diff --git a/test/python/visualization/test_circuit_latex.py b/test/python/visualization/test_circuit_latex.py index ed95ab64d515..e61a3b2f0bf3 100644 --- a/test/python/visualization/test_circuit_latex.py +++ b/test/python/visualization/test_circuit_latex.py @@ -26,11 +26,13 @@ from qiskit.circuit import Parameter, Qubit, Clbit from qiskit.circuit.library import IQP from qiskit.quantum_info.random import random_unitary +from qiskit.utils import optionals from .visualization import QiskitVisualizationTestCase pi = np.pi +@unittest.skipUnless(optionals.HAS_PYLATEX, "needs pylatexenc") class TestLatexSourceGenerator(QiskitVisualizationTestCase): """Qiskit latex source generator tests.""" diff --git a/test/python/visualization/test_dag_drawer.py b/test/python/visualization/test_dag_drawer.py index 5c32411aa278..bc5c588ff570 100644 --- a/test/python/visualization/test_dag_drawer.py +++ b/test/python/visualization/test_dag_drawer.py @@ -16,8 +16,6 @@ import tempfile import unittest -from PIL import Image - from qiskit.circuit import QuantumRegister, QuantumCircuit, Qubit, Clbit from qiskit.visualization import dag_drawer from qiskit.exceptions import InvalidFileError @@ -39,6 +37,7 @@ def setUp(self): self.dag = circuit_to_dag(circuit) @unittest.skipUnless(_optionals.HAS_GRAPHVIZ, "Graphviz not installed") + @unittest.skipUnless(_optionals.HAS_PIL, "PIL not installed") def test_dag_drawer_invalid_style(self): """Test dag draw with invalid style.""" with self.assertRaisesRegex(VisualizationError, "Invalid style multicolor"): @@ -53,6 +52,7 @@ def test_dag_drawer_checks_filename_correct_format(self): dag_drawer(self.dag, filename="aaabc") @unittest.skipUnless(_optionals.HAS_GRAPHVIZ, "Graphviz not installed") + @unittest.skipUnless(_optionals.HAS_PIL, "PIL not installed") def test_dag_drawer_checks_filename_extension(self): """filename must have a valid extension""" with self.assertRaisesRegex( @@ -63,8 +63,11 @@ def test_dag_drawer_checks_filename_extension(self): dag_drawer(self.dag, filename="aa.abc") @unittest.skipUnless(_optionals.HAS_GRAPHVIZ, "Graphviz not installed") + @unittest.skipUnless(_optionals.HAS_PIL, "PIL not installed") def test_dag_drawer_no_register(self): """Test dag visualization with a circuit with no registers.""" + from PIL import Image # pylint: disable=import-error + qubit = Qubit() clbit = Clbit() qc = QuantumCircuit([qubit, clbit]) diff --git a/test/python/visualization/test_gate_map.py b/test/python/visualization/test_gate_map.py index ead5f96e4dc3..29f9d7286b36 100644 --- a/test/python/visualization/test_gate_map.py +++ b/test/python/visualization/test_gate_map.py @@ -14,7 +14,6 @@ import unittest from io import BytesIO -from PIL import Image from ddt import ddt, data from qiskit.providers.fake_provider import ( FakeProvider, @@ -36,9 +35,14 @@ if optionals.HAS_MATPLOTLIB: import matplotlib.pyplot as plt +if optionals.HAS_PIL: + from PIL import Image @ddt +@unittest.skipUnless(optionals.HAS_MATPLOTLIB, "matplotlib not available.") +@unittest.skipUnless(optionals.HAS_PIL, "PIL not available") +@unittest.skipUnless(optionals.HAS_SEABORN, "seaborn not available") class TestGateMap(QiskitVisualizationTestCase): """visual tests for plot_gate_map""" @@ -51,7 +55,6 @@ class TestGateMap(QiskitVisualizationTestCase): ) @data(*backends) - @unittest.skipIf(not optionals.HAS_MATPLOTLIB, "matplotlib not available.") def test_plot_gate_map(self, backend): """tests plotting of gate map of a device (20 qubit, 16 qubit, 14 qubit and 5 qubit)""" n = backend.configuration().n_qubits @@ -64,7 +67,6 @@ def test_plot_gate_map(self, backend): plt.close(fig) @data(*backends) - @unittest.skipIf(not optionals.HAS_MATPLOTLIB, "matplotlib not available.") def test_plot_circuit_layout(self, backend): """tests plot_circuit_layout for each device""" layout_length = int(backend._configuration.n_qubits / 2) @@ -84,7 +86,6 @@ def test_plot_circuit_layout(self, backend): self.assertImagesAreEqual(Image.open(img_buffer), img_ref, 0.1) plt.close(fig) - @unittest.skipIf(not optionals.HAS_MATPLOTLIB, "matplotlib not available.") def test_plot_gate_map_no_backend(self): """tests plotting of gate map without a device""" n_qubits = 8 @@ -100,7 +101,6 @@ def test_plot_gate_map_no_backend(self): self.assertImagesAreEqual(Image.open(img_buffer), img_ref, 0.2) plt.close(fig) - @unittest.skipIf(not optionals.HAS_MATPLOTLIB, "matplotlib not available.") def test_plot_error_map_backend_v1(self): """Test plotting error map with fake backend v1.""" backend = FakeKolkata() @@ -112,7 +112,6 @@ def test_plot_error_map_backend_v1(self): self.assertImagesAreEqual(Image.open(img_buffer), img_ref, 0.2) plt.close(fig) - @unittest.skipIf(not optionals.HAS_MATPLOTLIB, "matplotlib not available.") def test_plot_error_map_backend_v2(self): """Test plotting error map with fake backend v2.""" backend = FakeKolkataV2() @@ -124,7 +123,6 @@ def test_plot_error_map_backend_v2(self): self.assertImagesAreEqual(Image.open(img_buffer), img_ref, 0.2) plt.close(fig) - @unittest.skipIf(not optionals.HAS_MATPLOTLIB, "matplotlib not available.") def test_plot_error_map_over_100_qubit(self): """Test plotting error map with large fake backend.""" backend = FakeWashington() @@ -136,7 +134,6 @@ def test_plot_error_map_over_100_qubit(self): self.assertImagesAreEqual(Image.open(img_buffer), img_ref, 0.2) plt.close(fig) - @unittest.skipIf(not optionals.HAS_MATPLOTLIB, "matplotlib not available.") def test_plot_error_map_over_100_qubit_backend_v2(self): """Test plotting error map with large fake backendv2.""" backend = FakeWashingtonV2() diff --git a/test/python/visualization/test_pass_manager_drawer.py b/test/python/visualization/test_pass_manager_drawer.py index a34fd37006dc..9b8e51ce593c 100644 --- a/test/python/visualization/test_pass_manager_drawer.py +++ b/test/python/visualization/test_pass_manager_drawer.py @@ -31,6 +31,9 @@ from .visualization import QiskitVisualizationTestCase, path_to_diagram_reference +@unittest.skipUnless(optionals.HAS_GRAPHVIZ, "Graphviz not installed.") +@unittest.skipUnless(optionals.HAS_PYDOT, "pydot not installed") +@unittest.skipUnless(optionals.HAS_PIL, "Pillow not installed") class TestPassManagerDrawer(QiskitVisualizationTestCase): """Qiskit pass manager drawer tests.""" @@ -54,7 +57,6 @@ def setUp(self): self.pass_manager.append(GateDirection(coupling_map)) self.pass_manager.append(RemoveResetInZeroState()) - @unittest.skipIf(not optionals.HAS_GRAPHVIZ, "Graphviz not installed.") def test_pass_manager_drawer_basic(self): """Test to see if the drawer draws a normal pass manager correctly""" filename = "current_standard.dot" @@ -67,7 +69,6 @@ def test_pass_manager_drawer_basic(self): finally: os.remove(filename) - @unittest.skipIf(not optionals.HAS_GRAPHVIZ, "Graphviz not installed.") def test_pass_manager_drawer_style(self): """Test to see if the colours are updated when provided by the user""" # set colours for some passes, but leave others to take the default values diff --git a/test/python/visualization/test_plot_histogram.py b/test/python/visualization/test_plot_histogram.py index f44cef3d0ece..7c530851326d 100644 --- a/test/python/visualization/test_plot_histogram.py +++ b/test/python/visualization/test_plot_histogram.py @@ -16,18 +16,20 @@ from io import BytesIO from collections import Counter -import matplotlib as mpl -from PIL import Image - from qiskit.visualization import plot_histogram from qiskit.utils import optionals from .visualization import QiskitVisualizationTestCase +if optionals.HAS_MATPLOTLIB: + import matplotlib as mpl +if optionals.HAS_PIL: + from PIL import Image + +@unittest.skipUnless(optionals.HAS_MATPLOTLIB, "matplotlib not available.") class TestPlotHistogram(QiskitVisualizationTestCase): """Qiskit plot_histogram tests.""" - @unittest.skipUnless(optionals.HAS_MATPLOTLIB, "matplotlib not available.") def test_different_counts_lengths(self): """Test plotting two different length dists works""" exact_dist = { @@ -113,21 +115,19 @@ def test_different_counts_lengths(self): fig = plot_histogram([raw_dist, exact_dist]) self.assertIsInstance(fig, mpl.figure.Figure) - @unittest.skipUnless(optionals.HAS_MATPLOTLIB, "matplotlib not available.") def test_with_number_to_keep(self): """Test plotting using number_to_keep""" dist = {"00": 3, "01": 5, "11": 8, "10": 11} fig = plot_histogram(dist, number_to_keep=2) self.assertIsInstance(fig, mpl.figure.Figure) - @unittest.skipUnless(optionals.HAS_MATPLOTLIB, "matplotlib not available.") def test_with_number_to_keep_multiple_executions(self): """Test plotting using number_to_keep with multiple executions""" dist = [{"00": 3, "01": 5, "11": 8, "10": 11}, {"00": 3, "01": 7, "10": 11}] fig = plot_histogram(dist, number_to_keep=2) self.assertIsInstance(fig, mpl.figure.Figure) - @unittest.skipUnless(optionals.HAS_MATPLOTLIB, "matplotlib not available.") + @unittest.skipUnless(optionals.HAS_PIL, "matplotlib not available.") def test_with_number_to_keep_multiple_executions_correct_image(self): """Test plotting using number_to_keep with multiple executions""" data_noisy = { diff --git a/test/python/visualization/test_utils.py b/test/python/visualization/test_utils.py index 38bb1ba4d401..b4f2f5a8e2a2 100644 --- a/test/python/visualization/test_utils.py +++ b/test/python/visualization/test_utils.py @@ -20,6 +20,7 @@ from qiskit.visualization.circuit import _utils from qiskit.visualization import array_to_latex from qiskit.test import QiskitTestCase +from qiskit.utils import optionals class TestVisualizationUtils(QiskitTestCase): @@ -355,10 +356,12 @@ def test_get_layered_instructions_op_with_cargs(self): expected, [[(op.name, op.qargs, op.cargs) for op in ops] for ops in layered_ops] ) + @unittest.skipUnless(optionals.HAS_PYLATEX, "needs pylatexenc") def test_generate_latex_label_nomathmode(self): """Test generate latex label default.""" self.assertEqual("abc", _utils.generate_latex_label("abc")) + @unittest.skipUnless(optionals.HAS_PYLATEX, "needs pylatexenc") def test_generate_latex_label_nomathmode_utf8char(self): """Test generate latex label utf8 characters.""" self.assertEqual( @@ -366,6 +369,7 @@ def test_generate_latex_label_nomathmode_utf8char(self): _utils.generate_latex_label("∭X∀Y"), ) + @unittest.skipUnless(optionals.HAS_PYLATEX, "needs pylatexenc") def test_generate_latex_label_mathmode_utf8char(self): """Test generate latex label mathtext with utf8.""" self.assertEqual( @@ -373,6 +377,7 @@ def test_generate_latex_label_mathmode_utf8char(self): _utils.generate_latex_label("$abc_$∭X∀Y"), ) + @unittest.skipUnless(optionals.HAS_PYLATEX, "needs pylatexenc") def test_generate_latex_label_mathmode_underscore_outside(self): """Test generate latex label with underscore outside mathmode.""" self.assertEqual( @@ -380,10 +385,12 @@ def test_generate_latex_label_mathmode_underscore_outside(self): _utils.generate_latex_label("$abc$_∭X∀Y"), ) + @unittest.skipUnless(optionals.HAS_PYLATEX, "needs pylatexenc") def test_generate_latex_label_escaped_dollar_signs(self): """Test generate latex label with escaped dollarsign.""" self.assertEqual("${\\ensuremath{\\forall}}$", _utils.generate_latex_label(r"\$∀\$")) + @unittest.skipUnless(optionals.HAS_PYLATEX, "needs pylatexenc") def test_generate_latex_label_escaped_dollar_sign_in_mathmode(self): """Test generate latex label with escaped dollar sign in mathmode.""" self.assertEqual( diff --git a/test/python/visualization/visualization.py b/test/python/visualization/visualization.py index 4fa2bb9b062c..16ab6cee61ed 100644 --- a/test/python/visualization/visualization.py +++ b/test/python/visualization/visualization.py @@ -19,11 +19,14 @@ import unittest from filecmp import cmp as cmpfile from shutil import copyfile -import matplotlib from qiskit.test import QiskitTestCase +from qiskit.utils import optionals as _optionals -matplotlib.use("ps") +if _optionals.HAS_MATPLOTLIB: + import matplotlib + + matplotlib.use("ps") def path_to_diagram_reference(filename): @@ -55,9 +58,11 @@ def assertEqualToReference(self, result): else: raise self.failureException("Result and reference do not match.") + @_optionals.HAS_PIL.require_in_call def assertImagesAreEqual(self, current, expected, diff_tolerance=0.001): """Checks if both images are similar enough to be considered equal. Similarity is controlled by the ```diff_tolerance``` argument.""" + from PIL import Image, ImageChops if isinstance(current, str): diff --git a/tox.ini b/tox.ini index 764253f46d4b..36fb269f6133 100644 --- a/tox.ini +++ b/tox.ini @@ -59,7 +59,7 @@ setenv = PYTHON=coverage3 run --source qiskit --parallel-mode deps = -r{toxinidir}/requirements.txt -r{toxinidir}/requirements-dev.txt - qiskit-aer + -r{toxinidir}/requirements-optional.txt commands = stestr run {posargs} coverage3 combine @@ -76,11 +76,11 @@ setenv = deps = setuptools_rust # This is work around for the bug of tox 3 (see #8606 for more details.) -r{toxinidir}/requirements-dev.txt - qiskit-aer - # Aer depends on Terra. We want to make sure pip satisfies that requirement from a local + -r{toxinidir}/requirements-optional.txt + # Some optionals depend on Terra. We want to make sure pip satisfies that requirement from a local # installation, not from PyPI. But Tox normally doesn't install the local installation until # after `deps` is installed. So, instead, we tell pip to do the local installation at the same - # time as Aer. See https://github.com/Qiskit/qiskit-terra/pull/9477. + # time as the optionals. See https://github.com/Qiskit/qiskit-terra/pull/9477. . commands = sphinx-build -W -j auto -T --keep-going -b html docs/ docs/_build/html {posargs}