From ef4bc3c4a771767e40c1457f1037ca7f6fcae754 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 15 Jul 2021 12:26:53 +0100 Subject: [PATCH 1/4] Unify setup and teardown methods of TestCases The previous split was to avoid having testtools as a core testing dependency, even though it was present in all CI tests. This eventually led to more fracturing of the two classes, where various extra functionality was added into FullQiskitTestCase, but not into BasicQiskitTestCase (see #6746). This meant downstream consumers of QiskitTestCase sometimes had to care about how Terra's tests were configured (see Qiskit/qiskit-aer#1283), and running simple local tests without setting the environment variable QISKIT_TEST_CAPTURE_STREAMS would run with different configurations. This unifies the two classes, such that QiskitTestCase is now the parent class of FullQiskitTestCase. All non-testtools/non-fixtures related additions are handled in QiskitTestCase, and FullQiskitTestCase adds on the testtools-specific functionality. FullQiskitTestCase is still not made a subclass of testtools.TestClass because the same issues mentioned in #3982 are still present in testtools. Whether to capture streams and whether to use the testtools machinery are two separate concerns, with the availability of the former being strictly dependent on the latter. We can bind to the "full" testtools-based class provided testtools is available, and it internally decides whether it should also add the stream-capturing mechanisms. The shared functionality added to the setUp, setUpClass and tearDown methods means that it is important all subclasses always call up the stack with super(). testtools has always enforced this using custom _run_setup() methods, but this is not available to us when it is not being used as the runner. Instead, the parent class is modified after creation, wrapping certain methods with additional checks, and the __init_subclass__ method is used to influence the creation of children, to add similar tests when they are created. --- qiskit/test/base.py | 231 ++++++++----------- qiskit/test/decorators.py | 172 ++++++++++++++ test/python/circuit/test_gate_definitions.py | 1 + 3 files changed, 275 insertions(+), 129 deletions(-) diff --git a/qiskit/test/base.py b/qiskit/test/base.py index 493448d69948..db0aa371d00a 100644 --- a/qiskit/test/base.py +++ b/qiskit/test/base.py @@ -41,6 +41,7 @@ HAS_FIXTURES = False from qiskit.exceptions import MissingOptionalLibraryError +from .decorators import enforce_subclasses_call from .runtest import RunTest, MultipleExceptions from .utils import Path, setup_test_logging @@ -88,9 +89,107 @@ def gather_details(source_dict, target_dict): target_dict[name] = _copy_content(content_object) -class BaseQiskitTestCase(unittest.TestCase): +@enforce_subclasses_call(["setUp", "setUpClass", "tearDown", "tearDownClass"]) +class QiskitTestCase(unittest.TestCase): """Common extra functionality on top of unittest.""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__setup_called = False + self.__teardown_called = False + + def setUp(self): + super().setUp() + if self.__setup_called: + raise ValueError( + "In File: %s\n" + "TestCase.setUp was already called. Do not explicitly call " + "setUp from your tests. In your own setUp, use super to call " + "the base setUp." % (sys.modules[self.__class__.__module__].__file__,) + ) + self.__setup_called = True + + def tearDown(self): + super().tearDown() + if self.__teardown_called: + raise ValueError( + "In File: %s\n" + "TestCase.tearDown was already called. Do not explicitly call " + "tearDown from your tests. In your own tearDown, use super to " + "call the base tearDown." % (sys.modules[self.__class__.__module__].__file__,) + ) + self.__teardown_called = True + # Reset the default providers, as in practice they acts as a singleton + # due to importing the instances from the top-level qiskit namespace. + from qiskit.providers.basicaer import BasicAer + + BasicAer._backends = BasicAer._verify_backends() + + @classmethod + def setUpClass(cls): + super().setUpClass() + # Determines if the TestCase is using IBMQ credentials. + cls.using_ibmq_credentials = False + # Set logging to file and stdout if the LOG_LEVEL envar is set. + cls.log = logging.getLogger(cls.__name__) + if os.getenv("LOG_LEVEL"): + filename = "%s.log" % os.path.splitext(inspect.getfile(cls))[0] + setup_test_logging(cls.log, os.getenv("LOG_LEVEL"), filename) + + warnings.filterwarnings("error", category=DeprecationWarning) + allow_DeprecationWarning_modules = [ + "test.ipynb.mpl.test_circuit_matplotlib_drawer", + "test.python.pulse.test_parameters", + "test.python.pulse.test_transforms", + "test.python.circuit.test_gate_power", + "test.python.pulse.test_builder", + "test.python.pulse.test_block", + "test.python.quantum_info.operators.symplectic.test_legacy_pauli", + "qiskit.quantum_info.operators.pauli", + "pybobyqa", + "numba", + "qiskit.utils.measurement_error_mitigation", + "qiskit.circuit.library.standard_gates.x", + "qiskit.pulse.schedule", + "qiskit.pulse.instructions.instruction", + "qiskit.pulse.instructions.play", + "qiskit.pulse.library.parametric_pulses", + "qiskit.quantum_info.operators.symplectic.pauli", + "test.python.dagcircuit.test_dagcircuit", + "test.python.quantum_info.operators.test_operator", + "test.python.quantum_info.operators.test_scalar_op", + "test.python.quantum_info.operators.test_superop", + "test.python.quantum_info.operators.channel.test_kraus", + "test.python.quantum_info.operators.channel.test_choi", + "test.python.quantum_info.operators.channel.test_chi", + "test.python.quantum_info.operators.channel.test_superop", + "test.python.quantum_info.operators.channel.test_stinespring", + "test.python.quantum_info.operators.symplectic.test_sparse_pauli_op", + "test.python.quantum_info.operators.channel.test_ptm", + ] + for mod in allow_DeprecationWarning_modules: + warnings.filterwarnings("default", category=DeprecationWarning, module=mod) + allow_DeprecationWarning_message = [ + r".*LogNormalDistribution.*", + r".*NormalDistribution.*", + r".*UniformDistribution.*", + r".*QuantumCircuit\.combine.*", + r".*QuantumCircuit\.__add__.*", + r".*QuantumCircuit\.__iadd__.*", + r".*QuantumCircuit\.extend.*", + r".*psi @ U.*", + r".*qiskit\.circuit\.library\.standard_gates\.ms import.*", + r"elementwise comparison failed.*", + r"The jsonschema validation included in qiskit-terra.*", + r"The DerivativeBase.parameter_expression_grad method.*", + r"Back-references to from Bit instances.*", + r"The QuantumCircuit.u. method.*", + r"The QuantumCircuit.cu.", + r"The CXDirection pass has been deprecated", + ] + for msg in allow_DeprecationWarning_message: + warnings.filterwarnings("default", category=DeprecationWarning, message=msg) + @staticmethod def _get_resource_path(filename, path=Path.TEST): """Get the absolute path to a resource. @@ -138,29 +237,7 @@ def assertDictAlmostEqual( raise self.failureException(msg) -class BasicQiskitTestCase(BaseQiskitTestCase): - """Helper class that contains common functionality.""" - - @classmethod - def setUpClass(cls): - # Determines if the TestCase is using IBMQ credentials. - cls.using_ibmq_credentials = False - - # Set logging to file and stdout if the LOG_LEVEL envar is set. - cls.log = logging.getLogger(cls.__name__) - if os.getenv("LOG_LEVEL"): - filename = "%s.log" % os.path.splitext(inspect.getfile(cls))[0] - setup_test_logging(cls.log, os.getenv("LOG_LEVEL"), filename) - - def tearDown(self): - # Reset the default providers, as in practice they acts as a singleton - # due to importing the instances from the top-level qiskit namespace. - from qiskit.providers.basicaer import BasicAer - - BasicAer._backends = BasicAer._verify_backends() - - -class FullQiskitTestCase(BaseQiskitTestCase): +class FullQiskitTestCase(QiskitTestCase): """Helper class that contains common functionality that captures streams.""" run_tests_with = RunTest @@ -191,8 +268,6 @@ def _reset(self): # Generators to ensure unique traceback ids. Maps traceback label to # iterators. self._traceback_id_gens = {} - self.__setup_called = False - self.__teardown_called = False self.__details = None def onException(self, exc_info, tb_label="traceback"): @@ -208,14 +283,6 @@ def onException(self, exc_info, tb_label="traceback"): def _run_teardown(self, result): """Run the tearDown function for this test.""" self.tearDown() - if not self.__teardown_called: - raise ValueError( - "In File: %s\n" - "TestCase.tearDown was not called. Have you upcalled all the " - "way up the hierarchy from your tearDown? e.g. Call " - "super(%s, self).tearDown() from your tearDown()." - % (sys.modules[self.__class__.__module__].__file__, self.__class__.__name__) - ) def _get_test_method(self): method_name = getattr(self, "_testMethodName") @@ -278,14 +345,6 @@ def reraise(exc_class, exc_obj, exc_tb, _marker=object()): def _run_setup(self, result): """Run the setUp function for this test.""" self.setUp() - if not self.__setup_called: - raise ValueError( - "In File: %s\n" - "TestCase.setUp was not called. Have you upcalled all the " - "way up the hierarchy from your setUp? e.g. Call " - "super(%s, self).setUp() from your setUp()." - % (sys.modules[self.__class__.__module__].__file__, self.__class__.__name__) - ) def _add_reason(self, reason): self.addDetail("reason", content.text_content(reason)) @@ -342,14 +401,6 @@ def run(self, result=None): def setUp(self): super().setUp() - if self.__setup_called: - raise ValueError( - "In File: %s\n" - "TestCase.setUp was already called. Do not explicitly call " - "setUp from your tests. In your own setUp, use super to call " - "the base setUp." % (sys.modules[self.__class__.__module__].__file__,) - ) - self.__setup_called = True if os.environ.get("QISKIT_TEST_CAPTURE_STREAMS"): stdout = self.useFixture(fixtures.StringStream("stdout")).stream self.useFixture(fixtures.MonkeyPatch("sys.stdout", stdout)) @@ -357,22 +408,6 @@ def setUp(self): self.useFixture(fixtures.MonkeyPatch("sys.stderr", stderr)) self.useFixture(fixtures.LoggerFixture(nuke_handlers=False, level=None)) - def tearDown(self): - super().tearDown() - if self.__teardown_called: - raise ValueError( - "In File: %s\n" - "TestCase.tearDown was already called. Do not explicitly call " - "tearDown from your tests. In your own tearDown, use super to " - "call the base tearDown." % (sys.modules[self.__class__.__module__].__file__,) - ) - self.__teardown_called = True - # Reset the default providers, as in practice they acts as a singleton - # due to importing the instances from the top-level qiskit namespace. - from qiskit.providers.basicaer import BasicAer - - BasicAer._backends = BasicAer._verify_backends() - def addDetail(self, name, content_object): """Add a detail to be reported with this test's outcome. @@ -409,66 +444,6 @@ def getDetails(self): self.__details = {} return self.__details - @classmethod - def setUpClass(cls): - # Determines if the TestCase is using IBMQ credentials. - cls.using_ibmq_credentials = False - cls.log = logging.getLogger(cls.__name__) - - warnings.filterwarnings("error", category=DeprecationWarning) - allow_DeprecationWarning_modules = [ - "test.ipynb.mpl.test_circuit_matplotlib_drawer", - "test.python.pulse.test_parameters", - "test.python.pulse.test_transforms", - "test.python.circuit.test_gate_power", - "test.python.pulse.test_builder", - "test.python.pulse.test_block", - "test.python.quantum_info.operators.symplectic.test_legacy_pauli", - "qiskit.quantum_info.operators.pauli", - "pybobyqa", - "numba", - "qiskit.utils.measurement_error_mitigation", - "qiskit.circuit.library.standard_gates.x", - "qiskit.pulse.schedule", - "qiskit.pulse.instructions.instruction", - "qiskit.pulse.instructions.play", - "qiskit.pulse.library.parametric_pulses", - "qiskit.quantum_info.operators.symplectic.pauli", - "test.python.dagcircuit.test_dagcircuit", - "test.python.quantum_info.operators.test_operator", - "test.python.quantum_info.operators.test_scalar_op", - "test.python.quantum_info.operators.test_superop", - "test.python.quantum_info.operators.channel.test_kraus", - "test.python.quantum_info.operators.channel.test_choi", - "test.python.quantum_info.operators.channel.test_chi", - "test.python.quantum_info.operators.channel.test_superop", - "test.python.quantum_info.operators.channel.test_stinespring", - "test.python.quantum_info.operators.symplectic.test_sparse_pauli_op", - "test.python.quantum_info.operators.channel.test_ptm", - ] - for mod in allow_DeprecationWarning_modules: - warnings.filterwarnings("default", category=DeprecationWarning, module=mod) - allow_DeprecationWarning_message = [ - r".*LogNormalDistribution.*", - r".*NormalDistribution.*", - r".*UniformDistribution.*", - r".*QuantumCircuit\.combine.*", - r".*QuantumCircuit\.__add__.*", - r".*QuantumCircuit\.__iadd__.*", - r".*QuantumCircuit\.extend.*", - r".*psi @ U.*", - r".*qiskit\.circuit\.library\.standard_gates\.ms import.*", - r"elementwise comparison failed.*", - r"The jsonschema validation included in qiskit-terra.*", - r"The DerivativeBase.parameter_expression_grad method.*", - r"Back-references to from Bit instances.*", - r"The QuantumCircuit.u. method.*", - r"The QuantumCircuit.cu.", - r"The CXDirection pass has been deprecated", - ] - for msg in allow_DeprecationWarning_message: - warnings.filterwarnings("default", category=DeprecationWarning, message=msg) - def dicts_almost_equal(dict1, dict2, delta=None, places=None, default_value=0): """Test if two dictionaries with numeric values are almost equal. @@ -529,7 +504,5 @@ def valid_comparison(value): return "" -if not HAS_FIXTURES or not os.environ.get("QISKIT_TEST_CAPTURE_STREAMS"): - QiskitTestCase = BasicQiskitTestCase -else: +if HAS_FIXTURES: QiskitTestCase = FullQiskitTestCase diff --git a/qiskit/test/decorators.py b/qiskit/test/decorators.py index 40f22c648215..df1d421d26a4 100644 --- a/qiskit/test/decorators.py +++ b/qiskit/test/decorators.py @@ -14,6 +14,7 @@ """Decorator for using with Qiskit unit tests.""" import functools +import inspect import os import socket import sys @@ -203,4 +204,175 @@ def _wrapper(self, *args, **kwargs): return _wrapper +class _WrappedMethodCall: + """Method call with extra functionality before and after. This is returned by + ``_WrappedMethod`` when accessed as an atrribute.""" + + def __init__(self, descriptor, obj, objtype): + self.descriptor = descriptor + self.obj = obj + self.objtype = objtype + + def __call__(self, *args, **kwargs): + if self.descriptor.isclassmethod: + ref = self.objtype + else: + # obj if we're being accessed as an instance method, or objtype if as a class method. + ref = self.obj if self.obj is not None else self.objtype + for before in self.descriptor.before: + before(ref, *args, **kwargs) + out = self.descriptor.method(ref, *args, **kwargs) + for after in self.descriptor.after: + after(ref, *args, **kwargs) + return out + + +class _WrappedMethod: + """Descriptor which calls its two arguments in succession, correctly handling instance- and + class-method calls. + + It is intended that this class will replace the attribute that ``inner`` previously was on a + class or instance. When accessed as that attribute, this descriptor will behave it is the same + function call, but with the ``function`` called after. + """ + + def __init__(self, cls, name, before=None, after=None): + # Find the actual definition of the method, not just the descriptor output from getattr. + for cls_ in inspect.getmro(cls): + try: + self.method = cls_.__dict__[name] + break + except KeyError: + pass + else: + raise ValueError(f"Method '{name}' is not defined for class '{cls.__class__.__name__}'") + before = (before,) if before is not None else () + after = (after,) if after is not None else () + if isinstance(self.method, type(self)): + self.isclassmethod = self.method.isclassmethod + self.before = before + self.method.before + self.after = self.method.after + after + self.method = self.method.method + else: + self.isclassmethod = False + self.before = before + self.after = after + if isinstance(self.method, classmethod): + self.method = self.method.__func__ + self.isclassmethod = True + + def __get__(self, obj, objtype=None): + # No functools.wraps because we're probably about to be bound to a different context. + return _WrappedMethodCall(self, obj, objtype) + + +def _wrap_method(cls, name, before=None, after=None): + """Wrap the functionality the instance- or class-method ``{cls}.{name}`` with ``before`` and + ``after``. + + This mutates ``cls``, replacing the attribute ``name`` with the new functionality. + + If either ``before`` or ``after`` are given, they should be callables with a compatible + signature to the method referred to. They will be called immediately before or after the method + as appropriate, and any return value will be ignored. + """ + setattr(cls, name, _WrappedMethod(cls, name, before, after)) + + +def enforce_subclasses_call(methods, attr="_enforce_subclasses_call_cache"): + """Class decorator which enforces that if any subclasses define on of the ``methods``, they must + call ``super().()`` or face a ``ValueError`` at runtime. This is unlikely to be useful + for concrete test classes, who are not normally subclassed. It should not be used on + user-facing code, because it prevents subclasses from being free to override parent-class + behavior, even when the parent-class behavior is not needed. + + This adds behavior to the ``__init__`` and ``__init_subclass__`` methods of the class, in + addition to the named methods of this class and all subclasses. The checks could be averted in + grandchildren if a child class overrides ``__init_subclass__`` without up-calling the decorated + class's method, though this would typically break inheritance principles. + + Parameters + ---------- + methods: str or iterable of str + Names of the methods to add the enforcement to. These do not necessarily need to be defined + in the class body, provided they are somewhere in the method-resolution tree. + + attr: str, optional + The attribute which will be added to all instances of this class and subclasses, in order to + manage the call enforcement. This can be changed to avoid clashes. + + Returns + ------- + type + The class with the relevant methods modified to include checks, and injection code in the + ``__init_subclass__`` method. + """ + + methods = {methods} if isinstance(methods, str) else set(methods) + + def initialize_call_memory(self, *args, **kwargs): + """Add the extra attribute used for tracking the method calls.""" + setattr(self, attr, set()) + + def save_call_status(name): + """Decorator, whose return saves the fact that the top-level method call occurred.""" + + def out(self, *args, **kwargs): + getattr(self, attr).add(name) + + return out + + def clear_call_status(name): + """Decorator, whose return clears the call status of the method ``name``. This prepares the + call tracking for the child class's method call.""" + + def out(self, *args, **kwargs): + getattr(self, attr).discard(name) + + return out + + def enforce_call_occurred(name): + """Decorator, whose return checks that the top-level method call occurred, and raises + ``ValueError`` if not. Concretely, this is an assertion that ``save_call_status`` ran.""" + + def out(self, *args, **kwargs): + cache = getattr(self, attr) + if name not in cache: + classname = self.__name__ if isinstance(self, type) else type(self).__name__ + raise ValueError( + f"Parent '{name}' method was not called by '{classname}.{name}'." + f" Ensure you have put in calls to 'super().{name}()'." + ) + + return out + + def wrap_subclass_methods(cls): + """Wrap all the ``methods`` of ``cls`` with the call-tracking assertions that the top-level + versions of the methods were called (likely via ``super()``).""" + # Only wrap methods who are directly defined in this class; if we're resolving to a method + # higher up the food chain, then it will already have been wrapped. + for name in set(cls.__dict__) & methods: + _wrap_method( + cls, + name, + before=clear_call_status(name), + after=enforce_call_occurred(name), + ) + + def decorator(cls): + # Add a class-level memory on, so class methods will work as well. Instances will override + # this on instantiation, to keep the "namespace" of class- and instance-methods separate. + initialize_call_memory(cls) + # Do the extra bits after the main body of __init__ so we can check we're not overwriting + # anything, and after __init_subclass__ in case the decorated class wants to influence the + # creation of the subclass's methods before we get to them. + _wrap_method(cls, "__init__", after=initialize_call_memory) + for name in methods: + _wrap_method(cls, name, before=save_call_status(name)) + _wrap_method(cls, "__init_subclass__", after=wrap_subclass_methods) + return cls + + return decorator + + TEST_OPTIONS = get_test_options() diff --git a/test/python/circuit/test_gate_definitions.py b/test/python/circuit/test_gate_definitions.py index ed2bf2ea2b3b..6b723cdba515 100644 --- a/test/python/circuit/test_gate_definitions.py +++ b/test/python/circuit/test_gate_definitions.py @@ -252,6 +252,7 @@ class TestGateEquivalenceEqual(QiskitTestCase): @classmethod def setUpClass(cls): + super().setUpClass() class_list = Gate.__subclasses__() + ControlledGate.__subclasses__() exclude = { "ControlledGate", From 9ae7c83142c880dda13a466966aa0fa2ff51c24c Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 15 Jul 2021 13:21:11 +0100 Subject: [PATCH 2/4] Explicitly mark unused arguments The decorator's internal functions take arguments *args and **kwargs, in order to wrap arbitrary functions, but pylint complains about them not being used. The arguments are explicitly meant to be ignored, which in pylint-speak means prepending with an underscore. --- qiskit/test/decorators.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit/test/decorators.py b/qiskit/test/decorators.py index df1d421d26a4..c2aab763183a 100644 --- a/qiskit/test/decorators.py +++ b/qiskit/test/decorators.py @@ -310,14 +310,14 @@ def enforce_subclasses_call(methods, attr="_enforce_subclasses_call_cache"): methods = {methods} if isinstance(methods, str) else set(methods) - def initialize_call_memory(self, *args, **kwargs): + def initialize_call_memory(self, *_args, **_kwargs): """Add the extra attribute used for tracking the method calls.""" setattr(self, attr, set()) def save_call_status(name): """Decorator, whose return saves the fact that the top-level method call occurred.""" - def out(self, *args, **kwargs): + def out(self, *_args, **_kwargs): getattr(self, attr).add(name) return out @@ -326,7 +326,7 @@ def clear_call_status(name): """Decorator, whose return clears the call status of the method ``name``. This prepares the call tracking for the child class's method call.""" - def out(self, *args, **kwargs): + def out(self, *_args, **_kwargs): getattr(self, attr).discard(name) return out @@ -335,7 +335,7 @@ def enforce_call_occurred(name): """Decorator, whose return checks that the top-level method call occurred, and raises ``ValueError`` if not. Concretely, this is an assertion that ``save_call_status`` ran.""" - def out(self, *args, **kwargs): + def out(self, *_args, **_kwargs): cache = getattr(self, attr) if name not in cache: classname = self.__name__ if isinstance(self, type) else type(self).__name__ From 1b9b76bfd6a266ced528c82d9bc57e10c117a3f3 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 3 Aug 2021 16:04:23 +0100 Subject: [PATCH 3/4] Retain the BaseQiskitTestCase class The previous commit removed this base class without issuing any deprecation warnings. That's clearly wrong. Further, we actually want to properly formalise a separation between a completely public test-additions API, for _all_ Qiskit-family packages to use, and additions which are specific to Terra. `assertDictsAreEqual` is an example of the former, but logging and warning control are examples of the latter. Deciding on a complete split is out-of-scope for this PR, but this commit reinstates the previous names, to avoid breaking downstream code. --- qiskit/test/base.py | 116 +++++++++++++++++++++++++------------------- 1 file changed, 67 insertions(+), 49 deletions(-) diff --git a/qiskit/test/base.py b/qiskit/test/base.py index ecfbe3ff436b..11065bd3dfb9 100644 --- a/qiskit/test/base.py +++ b/qiskit/test/base.py @@ -90,8 +90,11 @@ def gather_details(source_dict, target_dict): @enforce_subclasses_call(["setUp", "setUpClass", "tearDown", "tearDownClass"]) -class QiskitTestCase(unittest.TestCase): - """Common extra functionality on top of unittest.""" +class BaseQiskitTestCase(unittest.TestCase): + """Additions for test cases for all Qiskit-family packages. + + The additions here are intended for all packages, not just Terra. Terra-specific logic should + be in the Terra-specific classes.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -119,6 +122,59 @@ def tearDown(self): "call the base tearDown." % (sys.modules[self.__class__.__module__].__file__,) ) self.__teardown_called = True + + @staticmethod + def _get_resource_path(filename, path=Path.TEST): + """Get the absolute path to a resource. + + Args: + filename (string): filename or relative path to the resource. + path (Path): path used as relative to the filename. + + Returns: + str: the absolute path to the resource. + """ + return os.path.normpath(os.path.join(path.value, filename)) + + def assertDictAlmostEqual( + self, dict1, dict2, delta=None, msg=None, places=None, default_value=0 + ): + """Assert two dictionaries with numeric values are almost equal. + + Fail if the two dictionaries are unequal as determined by + comparing that the difference between values with the same key are + not greater than delta (default 1e-8), or that difference rounded + to the given number of decimal places is not zero. If a key in one + dictionary is not in the other the default_value keyword argument + will be used for the missing value (default 0). If the two objects + compare equal then they will automatically compare almost equal. + + Args: + dict1 (dict): a dictionary. + dict2 (dict): a dictionary. + delta (number): threshold for comparison (defaults to 1e-8). + msg (str): return a custom message on failure. + places (int): number of decimal places for comparison. + default_value (number): default value for missing keys. + + Raises: + TypeError: if the arguments are not valid (both `delta` and + `places` are specified). + AssertionError: if the dictionaries are not almost equal. + """ + + error_msg = dicts_almost_equal(dict1, dict2, delta, places, default_value) + + if error_msg: + msg = self._formatMessage(msg, error_msg) + raise self.failureException(msg) + + +class QiskitTestCase(BaseQiskitTestCase): + """Terra-specific extra functionality for test cases.""" + + def tearDown(self): + super().tearDown() # Reset the default providers, as in practice they acts as a singleton # due to importing the instances from the top-level qiskit namespace. from qiskit.providers.basicaer import BasicAer @@ -189,55 +245,14 @@ def setUpClass(cls): for msg in allow_DeprecationWarning_message: warnings.filterwarnings("default", category=DeprecationWarning, message=msg) - @staticmethod - def _get_resource_path(filename, path=Path.TEST): - """Get the absolute path to a resource. - - Args: - filename (string): filename or relative path to the resource. - path (Path): path used as relative to the filename. - - Returns: - str: the absolute path to the resource. - """ - return os.path.normpath(os.path.join(path.value, filename)) - - def assertDictAlmostEqual( - self, dict1, dict2, delta=None, msg=None, places=None, default_value=0 - ): - """Assert two dictionaries with numeric values are almost equal. - - Fail if the two dictionaries are unequal as determined by - comparing that the difference between values with the same key are - not greater than delta (default 1e-8), or that difference rounded - to the given number of decimal places is not zero. If a key in one - dictionary is not in the other the default_value keyword argument - will be used for the missing value (default 0). If the two objects - compare equal then they will automatically compare almost equal. - - Args: - dict1 (dict): a dictionary. - dict2 (dict): a dictionary. - delta (number): threshold for comparison (defaults to 1e-8). - msg (str): return a custom message on failure. - places (int): number of decimal places for comparison. - default_value (number): default value for missing keys. - - Raises: - TypeError: if the arguments are not valid (both `delta` and - `places` are specified). - AssertionError: if the dictionaries are not almost equal. - """ - - error_msg = dicts_almost_equal(dict1, dict2, delta, places, default_value) - - if error_msg: - msg = self._formatMessage(msg, error_msg) - raise self.failureException(msg) - class FullQiskitTestCase(QiskitTestCase): - """Helper class that contains common functionality that captures streams.""" + """Terra-specific further additions for test cases, if ``testtools`` is available. + + It is not normally safe to derive from this class by name; on import, Terra checks if the + necessary packages are available, and binds this class to the name :obj:`~QiskitTestCase` if so. + If you derive directly from it, you may try and instantiate the class without satisfying its + dependencies.""" run_tests_with = RunTest @@ -503,5 +518,8 @@ def valid_comparison(value): return "" +# Maintain naming backwards compatibility for downstream packages. +BasicQiskitTestCase = QiskitTestCase + if HAS_FIXTURES: QiskitTestCase = FullQiskitTestCase From 9abf2d5bac996c07034aa93a9c36952546f12892 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 3 Aug 2021 16:16:35 +0100 Subject: [PATCH 4/4] Convert docstrings to googledoc format --- qiskit/test/decorators.py | 43 ++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/qiskit/test/decorators.py b/qiskit/test/decorators.py index c2aab763183a..7add6916ab45 100644 --- a/qiskit/test/decorators.py +++ b/qiskit/test/decorators.py @@ -18,6 +18,7 @@ import os import socket import sys +from typing import Union, Callable, Type, Iterable import unittest from warnings import warn @@ -206,7 +207,7 @@ def _wrapper(self, *args, **kwargs): class _WrappedMethodCall: """Method call with extra functionality before and after. This is returned by - ``_WrappedMethod`` when accessed as an atrribute.""" + :obj:`~_WrappedMethod` when accessed as an atrribute.""" def __init__(self, descriptor, obj, objtype): self.descriptor = descriptor @@ -279,33 +280,33 @@ def _wrap_method(cls, name, before=None, after=None): setattr(cls, name, _WrappedMethod(cls, name, before, after)) -def enforce_subclasses_call(methods, attr="_enforce_subclasses_call_cache"): +def enforce_subclasses_call( + methods: Union[str, Iterable[str]], attr: str = "_enforce_subclasses_call_cache" +) -> Callable[[Type], Type]: """Class decorator which enforces that if any subclasses define on of the ``methods``, they must - call ``super().()`` or face a ``ValueError`` at runtime. This is unlikely to be useful - for concrete test classes, who are not normally subclassed. It should not be used on - user-facing code, because it prevents subclasses from being free to override parent-class - behavior, even when the parent-class behavior is not needed. + call ``super().()`` or face a ``ValueError`` at runtime. + + This is unlikely to be useful for concrete test classes, who are not normally subclassed. It + should not be used on user-facing code, because it prevents subclasses from being free to + override parent-class behavior, even when the parent-class behavior is not needed. This adds behavior to the ``__init__`` and ``__init_subclass__`` methods of the class, in addition to the named methods of this class and all subclasses. The checks could be averted in grandchildren if a child class overrides ``__init_subclass__`` without up-calling the decorated class's method, though this would typically break inheritance principles. - Parameters - ---------- - methods: str or iterable of str - Names of the methods to add the enforcement to. These do not necessarily need to be defined - in the class body, provided they are somewhere in the method-resolution tree. - - attr: str, optional - The attribute which will be added to all instances of this class and subclasses, in order to - manage the call enforcement. This can be changed to avoid clashes. - - Returns - ------- - type - The class with the relevant methods modified to include checks, and injection code in the - ``__init_subclass__`` method. + Arguments: + methods: + Names of the methods to add the enforcement to. These do not necessarily need to be + defined in the class body, provided they are somewhere in the method-resolution tree. + + attr: + The attribute which will be added to all instances of this class and subclasses, in + order to manage the call enforcement. This can be changed to avoid clashes. + + Returns: + A decorator, which returns its input class with the class with the relevant methods modified + to include checks, and injection code in the ``__init_subclass__`` method. """ methods = {methods} if isinstance(methods, str) else set(methods)