From 0438a4bb59090d13b9d52c6aa451f00c1140680c Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Fri, 4 Aug 2023 19:24:57 +0100 Subject: [PATCH] Add no-optionals and full-optionals test runs (#8967) * Add no-optionals and full-optionals test runs This restructures the CI slightly to perform a complete "no-optionals" run, and a complete "all optionals" run (for the optionals that are readily accessible as Python packages, without complex additional setup). Previously, we only did a partial test with some of the oldest optional components, which could have allowed for behaviour to accidentally require optional components without us noticing. This splits the `requirements-dev.txt` file into two; lines that remain in the file are what is actually _required_ to run the test suite, run the style checks, and do the documentation build. The rest (and everything that was missing) is added to a new `requirements-optional.txt` file, which can be used to pull in (almost) all of the packages that Terra can use to provide additional / accelerated functionality. Several tests needed to gain additional skips to account for this change. There is a good chance that some tests are missing skips for some libraries that are not the first point of failure, but it's hard to test explicitly for these in one go. * Fix typo in coverage workflow * Try relaxing ipython constraints * Squash newly exposed lint failures * Fix typo in tutorials pipeline * Update the 'also update' comments * Remove unneeded qiskit-toqm dependency * Section requirements-optional.txt * Test all optionals on min not max Python version Optionals are generally more likely to have been made available on the older Pythons, and some may take excessively long to provide wheels for the latest versions of Python. * Add missing test skip * Fix optional call * Use correct boolean syntax * Fix tests relying on Jupyter * Install ipykernel in tutorials * Remove HAS_PDFLATEX skip from quantum_info tests For simple LaTeX tests, IPython/Jupyter can handle the compilation internally using MathJax, and doesn't actually need a `pdflatex` installation. We only need that when we're depending on LaTeX libraries that are beyond what MathJax can handle natively. * Include additional tutorials dependencies * Install all of Terra's optionals in tutorial run * Do not install all optionals in docs build * Use class-level skips where appropriate * Do not install ibmq-provider in tutorials run * Include matplotlib in docs requirements * Remove unnecessary whitespace * Split long pip line * Only install graphviz when installOptionals Co-authored-by: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> * Install visualization extras in docs build * Don't `--upgrade` when installing optionals This is to prevent any optionals that have a dependency on Terra from potentially upgrading it to some PyPI version during their installation. This shouldn't happen with the current development model of having only one supported stable branch and the main branch, but the `-U` is unnecessary, and not having it is safer for the future. * Update secondary installation of Terra in docs build * Install all optionals during the docs build I yoyo-ed on this, not wanting it to be too onerous to build the documentation, but in the end, we need to have the optional features installed in order to build most of the documentation of those features, and some unrelated parts of the documentation may use these features to enhance their own output. * Fix test setup job * Remove duplication of matplotlib in requirements files * Update image test installation command * Restore editable build * Move `pip check` to `pip` section * Remove redundant "post-install" description * Expand comment on first-stage choices * pytohn lol --------- Co-authored-by: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> --- .azure/test-linux.yml | 35 ++++++++------ .azure/test-macos.yml | 11 +++++ .azure/test-windows.yml | 13 ++++- .azure/tutorials-linux.yml | 12 +---- .github/workflows/coverage.yml | 2 +- azure-pipelines.yml | 19 +++++++- .../passes/layout/_csp_custom_solver.py | 2 + qiskit/utils/optionals.py | 8 ++++ qiskit/visualization/bloch.py | 2 + requirements-dev.txt | 47 +++++++++++-------- requirements-optional.txt | 40 ++++++++++++++++ requirements-tutorials.txt | 10 ++++ setup.py | 4 +- test/python/circuit/test_equivalence.py | 1 + .../quantum_info/states/test_densitymatrix.py | 3 ++ .../quantum_info/states/test_statevector.py | 3 ++ test/python/tools/jupyter/test_notebooks.py | 6 ++- test/python/transpiler/test_coupling.py | 1 + test/python/transpiler/test_csp_layout.py | 2 + .../visualization/test_circuit_drawer.py | 1 + .../visualization/test_circuit_latex.py | 2 + test/python/visualization/test_dag_drawer.py | 7 ++- test/python/visualization/test_gate_map.py | 13 ++--- .../visualization/test_pass_manager_drawer.py | 5 +- .../visualization/test_plot_histogram.py | 14 +++--- test/python/visualization/test_utils.py | 7 +++ test/python/visualization/visualization.py | 9 +++- tox.ini | 8 ++-- 28 files changed, 210 insertions(+), 77 deletions(-) create mode 100644 requirements-optional.txt create mode 100644 requirements-tutorials.txt 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}