Repository: Qiskit/qiskit-terra (python)

Issue:
Avoid potential import cycles from Aer and IBMQ
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues to confirm this idea does not exist. -->

### What is the expected enhancement?

Right now the effort to remove namespace packaging in #5089 is blocked on the modules level imports of `Aer` and `IBMQ`. This is because this causes an import cycle when those factory objects are no longer in the qiskit namespace. Basically, `qiskit_aer` imports `qiskit` whichi imports `qiskit_aer.Aer` which can't be resolved by python. To move forward on this we need to make these objects lazily load `qiskit.providers.aer`/`qiskit_aer` and `qiskit.providers.ibmq`/`qiskit_ibmq_provider`. The easiest way to handle this is to wait for python 3.7 to be the minimum python version and then we can just use a module level `__getattr__` and import when `Aer` or `IBMQ` are accessed. But that is likely 6+ months away since 0.17.0 will start the deprecation timer on Python 3.6 support. In the meantime we should be able do something like:

```python3
class _AerWrapper:

    def __init__(self):
       self.aer = None
       
    def __getattr__(self, attr):
       if self._aer is None:
           try:
                from qiskit.providers.aer import Aer
            except ImportError:
                raise ImportError("useful message")
            self.aer = Aer
      return getattr(self.aer, attr)
```
(note this is completely untested, just my rough idea).

This should hopefully enable us to remove the import cycle at the module level. It would also have a noticeable benefit for #5100 as we're not unconditionally importing ibmq and aer all the time.

Pull request:
Wrap qiskit.Aer and qiskit.IBMQ with lazy loading object
<!--
⚠️ If you do not respect this template, your pull request will be closed.
⚠️ Your pull request title should be short detailed and understandable for all.
⚠️ Also, please add a release note file using reno if the change needs to be
  documented in the release notes.
⚠️ If your pull request fixes an open issue, please link to the issue.

- [ ] I have added the tests to cover my changes.
- [ ] I have updated the documentation accordingly.
- [ ] I have read the CONTRIBUTING document.
-->

### Summary

This commit migrates the qiskit.Aer and qiskit.IBMQ module attributes to
be lazy loading instances of lazy loading wrapper classes. The intent
here is to avoid importing from qiskit-aer or qiskit-ibmq-provider from
qiskit-terra, while this is safe while the packages share a shared
namespace we've been actively working to remove the use of namespace
packaging (see #5089, #4767, and Qiskit/qiskit#559) and the circular
dependency caused by re-exporting these attributes is blocking progress
on this. By using a lazy loading wrapper class we avoid an import type
circular dependency and opportunistically use qiskit-aer and/or
qiskit-ibmq-provider at runtime only if they're present after everything
is imported. This also may have some benefit for the overall import
performance of qiskit (being tracked in #5100) as it removes qiskit-aer
and qiskit-ibmq-provider from the import path unless they're being used.

### Details and comments

Fixes #5532


Patch:
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 375a2fb10..3c0f2b206 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -624,7 +624,7 @@ stages:
             python -m pip install --upgrade pip
             pip install -U -r requirements.txt -r requirements-dev.txt -c constraints.txt
             pip install -c constraints.txt -e .
-            pip install "qiskit-ibmq-provider" "qiskit-aer" "z3-solver" "qiskit-ignis" "qiskit-aqua" "pyscf<1.7.4" "matplotlib<3.3.0" sphinx nbsphinx sphinx_rtd_theme cvxpy -c constraints.txt
+            pip install "qiskit-ibmq-provider" "qiskit-aer" "z3-solver" "git+https://github.com/Qiskit/qiskit-ignis" "qiskit-aqua" "pyscf<1.7.4" "matplotlib<3.3.0" sphinx nbsphinx sphinx_rtd_theme cvxpy -c constraints.txt
             python setup.py build_ext --inplace
             sudo apt install -y graphviz pandoc
             pip check
diff --git a/qiskit/__init__.py b/qiskit/__init__.py
index 318c04f77..b49b86d91 100644
--- a/qiskit/__init__.py
+++ b/qiskit/__init__.py
@@ -15,7 +15,6 @@
 
 """Main Qiskit public functionality."""
 
-
 import pkgutil
 import sys
 import warnings
@@ -49,28 +48,6 @@ from qiskit.providers.basicaer import BasicAer
 
 _config = _user_config.get_config()
 
-# Try to import the Aer provider if installed.
-try:
-    from qiskit.providers.aer import Aer
-except ImportError:
-    suppress_warnings = os.environ.get('QISKIT_SUPPRESS_PACKAGING_WARNINGS', '')
-    if suppress_warnings.upper() != 'Y':
-        if not _config.get('suppress_packaging_warnings') or suppress_warnings.upper() == 'N':
-            warnings.warn('Could not import the Aer provider from the qiskit-aer '
-                          'package. Install qiskit-aer or check your installation.',
-                          RuntimeWarning)
-# Try to import the IBMQ provider if installed.
-try:
-    from qiskit.providers.ibmq import IBMQ
-except ImportError:
-    suppress_warnings = os.environ.get('QISKIT_SUPPRESS_PACKAGING_WARNINGS', '')
-    if suppress_warnings.upper() != 'Y':
-        if not _config.get('suppress_packaging_warnings') or suppress_warnings.upper() == 'N':
-            warnings.warn('Could not import the IBMQ provider from the '
-                          'qiskit-ibmq-provider package. Install '
-                          'qiskit-ibmq-provider or check your installation.',
-                          RuntimeWarning)
-
 # Moved to after IBMQ and Aer imports due to import issues
 # with other modules that check for IBMQ (tools)
 from qiskit.execute_function import execute  # noqa
@@ -88,3 +65,62 @@ if sys.version_info[0] == 3 and sys.version_info[1] == 6:
         "Using Qiskit with Python 3.6 is deprecated as of the 0.17.0 release. "
         "Support for running Qiskit with Python 3.6 will be removed in a "
         "future release.", DeprecationWarning)
+
+
+class AerWrapper:
+    """Lazy loading wrapper for Aer provider."""
+
+    def __init__(self):
+        self.aer = None
+
+    def __bool__(self):
+        if self.aer is None:
+            try:
+                from qiskit.providers import aer
+                self.aer = aer.Aer
+            except ImportError:
+                return False
+        return True
+
+    def __getattr__(self, attr):
+        if not self.aer:
+            try:
+                from qiskit.providers import aer
+                self.aer = aer.Aer
+            except ImportError as exc:
+                raise ImportError('Could not import the Aer provider from the '
+                                  'qiskit-aer package. Install qiskit-aer or '
+                                  'check your installation.') from exc
+        return getattr(self.aer, attr)
+
+
+class IBMQWrapper:
+    """Lazy loading wraooer for IBMQ provider."""
+
+    def __init__(self):
+        self.ibmq = None
+
+    def __bool__(self):
+        if self.ibmq is None:
+            try:
+                from qiskit.providers import ibmq
+                self.ibmq = ibmq.IBMQ
+            except ImportError:
+                return False
+        return True
+
+    def __getattr__(self, attr):
+        if not self.ibmq:
+            try:
+                from qiskit.providers import ibmq
+                self.ibmq = ibmq.IBMQ
+            except ImportError as exc:
+                raise ImportError('Could not import the IBMQ provider from the '
+                                  'qiskit-ibmq-provider package. Install '
+                                  'qiskit-ibmq-provider or check your  '
+                                  'installation.') from exc
+        return getattr(self.ibmq, attr)
+
+
+Aer = AerWrapper()
+IBMQ = IBMQWrapper()
diff --git a/qiskit/user_config.py b/qiskit/user_config.py
index d609a4154..b9c0e6faa 100644
--- a/qiskit/user_config.py
+++ b/qiskit/user_config.py
@@ -41,7 +41,6 @@ class UserConfig:
     circuit_mpl_style = default
     circuit_mpl_style_path = ~/.qiskit:<default location>
     transpile_optimization_level = 1
-    suppress_packaging_warnings = False
     parallel = False
     num_processes = 4
 
@@ -116,12 +115,6 @@ class UserConfig:
                 self.settings['transpile_optimization_level'] = (
                     transpile_optimization_level)
 
-            # Parse package warnings
-            package_warnings = self.config_parser.getboolean(
-                'default', 'suppress_packaging_warnings', fallback=False)
-            if package_warnings:
-                self.settings['suppress_packaging_warnings'] = package_warnings
-
             # Parse parallel
             parallel_enabled = self.config_parser.getboolean(
                 'default', 'parallel', fallback=PARALLEL_DEFAULT)
diff --git a/releasenotes/notes/lazy-load-provider-factory-bdfb3925a2514ef7.yaml b/releasenotes/notes/lazy-load-provider-factory-bdfb3925a2514ef7.yaml
new file mode 100644
index 000000000..4681b1e26
--- /dev/null
+++ b/releasenotes/notes/lazy-load-provider-factory-bdfb3925a2514ef7.yaml
@@ -0,0 +1,57 @@
+---
+upgrade:
+  - |
+    The ``qiskit.Aer`` and ``qiskit.IBMQ`` top level attributes are now lazy
+    loaded. This means that the objects will always now exist and warnings will
+    no longer be raised on import if ``qiskit-aer`` or ``qiskit-ibmq-provider``
+    are not installed (or can't be found by Python). If you were checking for
+    the presence of ``qiskit-aer`` or ``qiskit-ibmq-provider`` using these
+    module attributes and explicitly comparing to ``None`` or looking for the
+    absence of the attribute this no longer will work because they are always
+    defined as an object now. In other words running something like::
+
+        try:
+            from qiskit import Aer
+        except ImportError:
+            print("Aer not available")
+
+        or::
+
+        try:
+            from qiskit import IBMQ
+        except ImportError:
+            print("IBMQ not available")
+
+    will no longer work. Instead to determine if those providers are present
+    you can either explicitly use ``qiskit.providers.aer.Aer`` and
+    ``qiskit.providers.ibmq.IBMQ``::
+
+        try:
+            from qiskit.providers.aer import Aer
+        except ImportError:
+            print("Aer not available")
+
+        try:
+            from qiskit.providers.ibmq import IBMQ
+        except ImportError:
+            print("IBMQ not available")
+
+    or check ``bool(qiskit.Aer)`` and ``bool(qiskit.IBMQ)`` instead, for
+    example::
+
+        import qiskit
+
+        if not qiskit.Aer:
+            print("Aer not available")
+        if not qiskit.IBMQ:
+            print("IBMQ not available")
+
+    This change was necessary to avoid potential import cycle issues between
+    the qiskit packages and also to improve the import time when Aer or IBMQ
+    are not being used.
+  - |
+    The user config file option ``suppress_packaging_warnings`` option in the
+    user config file and the ``QISKIT_SUPPRESS_PACKAGING_WARNINGS`` environment
+    variable no longer has any effect and will be silently ignored. These
+    warnings have been removed and will no longer be emitted at import time
+    from the qiskit module.


Test patch:
diff --git a/qiskit/test/mock/fake_backend.py b/qiskit/test/mock/fake_backend.py
index 1c96936cf..0fa2491a6 100644
--- a/qiskit/test/mock/fake_backend.py
+++ b/qiskit/test/mock/fake_backend.py
@@ -23,7 +23,7 @@ from qiskit.providers import BaseBackend
 from qiskit.exceptions import QiskitError
 
 try:
-    from qiskit import Aer
+    from qiskit.providers.aer import Aer
     HAS_AER = True
 except ImportError:
     HAS_AER = False
diff --git a/test/python/algorithms/test_vqe.py b/test/python/algorithms/test_vqe.py
index ac0f211ac..9806fba33 100644
--- a/test/python/algorithms/test_vqe.py
+++ b/test/python/algorithms/test_vqe.py
@@ -10,6 +10,8 @@
 # copyright notice, and modified files need to carry a notice indicating
 # that they have been altered from the originals.
 
+# pylint: disable=no-name-in-module,import-error
+
 """ Test VQE """
 
 import unittest
@@ -167,7 +169,7 @@ class TestVQE(QiskitAlgorithmsTestCase):
         """Test VQE with Aer's statevector_simulator."""
         try:
             # pylint: disable=import-outside-toplevel
-            from qiskit import Aer
+            from qiskit.providers.aer import Aer
         except Exception as ex:  # pylint: disable=broad-except
             self.skipTest("Aer doesn't appear to be installed. Error: '{}'".format(str(ex)))
             return
@@ -190,7 +192,7 @@ class TestVQE(QiskitAlgorithmsTestCase):
         """Test VQE with Aer's qasm_simulator."""
         try:
             # pylint: disable=import-outside-toplevel
-            from qiskit import Aer
+            from qiskit.providers.aer import Aer
         except Exception as ex:  # pylint: disable=broad-except
             self.skipTest("Aer doesn't appear to be installed. Error: '{}'".format(str(ex)))
             return
@@ -215,7 +217,7 @@ class TestVQE(QiskitAlgorithmsTestCase):
         """Test the VQE using Aer's qasm_simulator snapshot mode."""
         try:
             # pylint: disable=import-outside-toplevel
-            from qiskit import Aer
+            from qiskit.providers.aer import Aer
         except Exception as ex:  # pylint: disable=broad-except
             self.skipTest("Aer doesn't appear to be installed. Error: '{}'".format(str(ex)))
             return
@@ -308,7 +310,7 @@ class TestVQE(QiskitAlgorithmsTestCase):
         """Test expectation selection with Aer's qasm_simulator."""
         try:
             # pylint: disable=import-outside-toplevel
-            from qiskit import Aer
+            from qiskit.providers.aer import Aer
         except Exception as ex:  # pylint: disable=broad-except
             self.skipTest("Aer doesn't appear to be installed. Error: '{}'".format(str(ex)))
             return
diff --git a/test/python/opflow/test_state_op_meas_evals.py b/test/python/opflow/test_state_op_meas_evals.py
index 7268bc5e7..f42a2a8b5 100644
--- a/test/python/opflow/test_state_op_meas_evals.py
+++ b/test/python/opflow/test_state_op_meas_evals.py
@@ -10,6 +10,8 @@
 # copyright notice, and modified files need to carry a notice indicating
 # that they have been altered from the originals.
 
+# pylint: disable=no-name-in-module,import-error
+
 """ Test Operator construction, including OpPrimitives and singletons. """
 
 import unittest
@@ -63,7 +65,7 @@ class TestStateOpMeasEvals(QiskitOpflowTestCase):
         """Test that the coefficients in SummedOp and states are correctly used."""
         try:
             # pylint: disable=import-outside-toplevel
-            from qiskit import Aer
+            from qiskit.providers.aer import Aer
         except Exception as ex:  # pylint: disable=broad-except
             self.skipTest("Aer doesn't appear to be installed. Error: '{}'".format(str(ex)))
             return
@@ -89,7 +91,7 @@ class TestStateOpMeasEvals(QiskitOpflowTestCase):
         """Test if is_measurement property of StateFn is propagated to converted StateFn."""
         try:
             # pylint: disable=import-outside-toplevel
-            from qiskit import Aer
+            from qiskit.providers.aer import Aer
         except Exception as ex:  # pylint: disable=broad-except
             self.skipTest("Aer doesn't appear to be installed. Error: '{}'".format(str(ex)))
             return
@@ -103,7 +105,7 @@ class TestStateOpMeasEvals(QiskitOpflowTestCase):
         """Test passing a ListOp with differing parameters works with the circuit sampler."""
         try:
             # pylint: disable=import-outside-toplevel
-            from qiskit import Aer
+            from qiskit.providers.aer import Aer
         except Exception as ex:  # pylint: disable=broad-except
             self.skipTest("Aer doesn't appear to be installed. Error: '{}'".format(str(ex)))
             return
diff --git a/test/python/test_user_config.py b/test/python/test_user_config.py
index 9f70a9918..36dc3ee5c 100644
--- a/test/python/test_user_config.py
+++ b/test/python/test_user_config.py
@@ -31,18 +31,6 @@ class TestUserConfig(QiskitTestCase):
         self.assertEqual({},
                          config.settings)
 
-    def test_invalid_suppress_packaging_warnings(self):
-        test_config = """
-        [default]
-        suppress_packaging_warnings = 76
-        """
-        self.addCleanup(os.remove, self.file_path)
-        with open(self.file_path, 'w') as file:
-            file.write(test_config)
-            file.flush()
-            config = user_config.UserConfig(self.file_path)
-            self.assertRaises(ValueError, config.read_config_file)
-
     def test_invalid_optimization_level(self):
         test_config = """
         [default]
@@ -100,37 +88,6 @@ class TestUserConfig(QiskitTestCase):
                  'parallel_enabled': user_config.PARALLEL_DEFAULT},
                 config.settings)
 
-    def test_valid_suppress_packaging_warnings_false(self):
-        test_config = """
-        [default]
-        suppress_packaging_warnings = false
-        """
-        self.addCleanup(os.remove, self.file_path)
-        with open(self.file_path, 'w') as file:
-            file.write(test_config)
-            file.flush()
-            config = user_config.UserConfig(self.file_path)
-            config.read_config_file()
-            self.assertEqual(
-                {'parallel_enabled': user_config.PARALLEL_DEFAULT},
-                config.settings)
-
-    def test_valid_suppress_packaging_warnings_true(self):
-        test_config = """
-        [default]
-        suppress_packaging_warnings = true
-        """
-        self.addCleanup(os.remove, self.file_path)
-        with open(self.file_path, 'w') as file:
-            file.write(test_config)
-            file.flush()
-            config = user_config.UserConfig(self.file_path)
-            config.read_config_file()
-            self.assertEqual(
-                {'parallel_enabled': user_config.PARALLEL_DEFAULT,
-                 'suppress_packaging_warnings': True},
-                config.settings)
-
     def test_invalid_num_processes(self):
         test_config = """
         [default]
@@ -197,6 +154,5 @@ class TestUserConfig(QiskitTestCase):
                               'circuit_mpl_style_path': ['~', '~/.qiskit'],
                               'transpile_optimization_level': 3,
                               'num_processes': 15,
-                              'parallel_enabled': False,
-                              'suppress_packaging_warnings': True},
+                              'parallel_enabled': False},
                              config.settings)