Repository: Qiskit/qiskit

Issue:
Supporting singledispatchmethod
### What should we add?

Currently we cannot use the method dispatch mechanism in Qiskit because `functuools.singledispatchmethod` is available in python 3.8+. I often  prefer to use method dispatch to make code readable (rather than writing long if-clause), but EOL of python 3.7 is  2023-06-27. 

Alternatively we can add this package to dependency.
https://pypi.org/project/singledispatchmethod/

I'm not sure if adding more dependency makes sense since we can still write reasonable code without dispatch mechanism.


Discussion:
Maybe this could help simplify `BasisTranslator` which has to call  `functools.singledispatch` outside the class.
I'm in favor, in general. Can we smoothly drop the dependency once we drop support for 3.7?

I don't have a strong opinion on the decorator but for limiting it to just 3.7 we already do this for other things: https://github.com/Qiskit/qiskit-terra/blob/main/requirements.txt#L11-L12 and https://github.com/Qiskit/qiskit-terra/blob/main/requirements.txt#L18-L22 where we are using specific libraries only with 3.7. When we drop 3.7 support next year we can just remove those lines

Resolving patch:
diff --git a/qiskit/circuit/classicalfunction/__init__.py b/qiskit/circuit/classicalfunction/__init__.py
--- a/qiskit/circuit/classicalfunction/__init__.py
+++ b/qiskit/circuit/classicalfunction/__init__.py
@@ -96,6 +96,13 @@ def grover_oracle(a: Int1, b: Int1, c: Int1, d: Int1) -> Int1:
    ClassicalFunctionCompilerTypeError
 
 """
+
+from qiskit.utils import optionals as _optionals
+
+_optionals.HAS_TWEEDLEDUM.require_now("classical oracle creation")
+
+# pylint: disable=wrong-import-position
+
 from .classicalfunction import ClassicalFunction
 from .exceptions import (
     ClassicalFunctionParseError,
diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py
--- a/qiskit/transpiler/passes/basis/basis_translator.py
+++ b/qiskit/transpiler/passes/basis/basis_translator.py
@@ -14,12 +14,12 @@
 
 """Translates gates to a target basis using a given equivalence library."""
 
+import sys
 import time
 import logging
 
 from itertools import zip_longest
 from collections import defaultdict
-from functools import singledispatch
 
 import retworkx
 
@@ -30,6 +30,10 @@
 from qiskit.transpiler.basepasses import TransformationPass
 from qiskit.transpiler.exceptions import TranspilerError
 
+if sys.version_info >= (3, 8):
+    from functools import singledispatchmethod  # pylint: disable=no-name-in-module
+else:
+    from singledispatchmethod import singledispatchmethod
 
 logger = logging.getLogger(__name__)
 
@@ -123,7 +127,7 @@ def run(self, dag):
         if self._target is None:
             basic_instrs = ["measure", "reset", "barrier", "snapshot", "delay"]
             target_basis = set(self._target_basis)
-            source_basis = set(_extract_basis(dag))
+            source_basis = set(self._extract_basis(dag))
             qargs_local_source_basis = {}
         else:
             basic_instrs = ["barrier", "snapshot"]
@@ -294,6 +298,29 @@ def _replace_node(self, dag, node, instr_map):
         else:
             dag.substitute_node_with_dag(node, bound_target_dag)
 
+    @singledispatchmethod
+    def _extract_basis(self, circuit):
+        return circuit
+
+    @_extract_basis.register
+    def _(self, dag: DAGCircuit):
+        for node in dag.op_nodes():
+            if not dag.has_calibration_for(node):
+                yield (node.name, node.op.num_qubits)
+            if isinstance(node.op, ControlFlowOp):
+                for block in node.op.blocks:
+                    yield from self._extract_basis(block)
+
+    @_extract_basis.register
+    def _(self, circ: QuantumCircuit):
+        for instr_context in circ.data:
+            instr, _, _ = instr_context
+            if not circ.has_calibration_for(instr_context):
+                yield (instr.name, instr.num_qubits)
+            if isinstance(instr, ControlFlowOp):
+                for block in instr.blocks:
+                    yield from self._extract_basis(block)
+
     def _extract_basis_target(
         self, dag, qarg_indices, source_basis=None, qargs_local_source_basis=None
     ):
@@ -336,34 +363,6 @@ def _extract_basis_target(
         return source_basis, qargs_local_source_basis
 
 
-# this could be singledispatchmethod and included in above class when minimum
-# supported python version=3.8.
-@singledispatch
-def _extract_basis(circuit):
-    return circuit
-
-
-@_extract_basis.register
-def _(dag: DAGCircuit):
-    for node in dag.op_nodes():
-        if not dag.has_calibration_for(node):
-            yield (node.name, node.op.num_qubits)
-        if isinstance(node.op, ControlFlowOp):
-            for block in node.op.blocks:
-                yield from _extract_basis(block)
-
-
-@_extract_basis.register
-def _(circ: QuantumCircuit):
-    for instr_context in circ.data:
-        instr, _, _ = instr_context
-        if not circ.has_calibration_for(instr_context):
-            yield (instr.name, instr.num_qubits)
-        if isinstance(instr, ControlFlowOp):
-            for block in instr.blocks:
-                yield from _extract_basis(block)
-
-
 class StopIfBasisRewritable(Exception):
     """Custom exception that signals `retworkx.dijkstra_search` to stop."""
 


Test patch: