diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index 2d3f6735f0..b79ed0ecf3 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -26,7 +26,7 @@ from qiskit.exceptions import QiskitError from qiskit.qobj.utils import MeasLevel from qiskit.providers.options import Options -from qiskit_experiments.framework.settings import Settings +from qiskit_experiments.framework.store_init_args import StoreInitArgs from qiskit_experiments.framework.experiment_data import ExperimentData from qiskit_experiments.version import __version__ @@ -84,7 +84,7 @@ def experiment(self) -> "BaseExperiment": raise QiskitError("{}\nError Message:\n{}".format(msg, str(ex))) from ex -class BaseExperiment(ABC, Settings): +class BaseExperiment(ABC, StoreInitArgs): """Abstract base class for experiments. Class Attributes: diff --git a/qiskit_experiments/framework/settings.py b/qiskit_experiments/framework/store_init_args.py similarity index 50% rename from qiskit_experiments/framework/settings.py rename to qiskit_experiments/framework/store_init_args.py index c8c407515c..ff255bdd82 100644 --- a/qiskit_experiments/framework/settings.py +++ b/qiskit_experiments/framework/store_init_args.py @@ -16,15 +16,15 @@ import inspect from collections import OrderedDict from functools import wraps -from typing import Dict, Any -class Settings: - """Class mixing for storing instance init settings. +class StoreInitArgs: + """Class mixing for storing class and subclass instance init args. This mixin adds a ``__new__`` method that stores the values of args - and kwargs passed to the class instances ``__init__`` method and a - ``settings`` property that returns an ordered dict of these values. + and kwargs passed to the class instances ``__init__`` method. + These are stored as ordered dicts under attributes ``__init_args__` + and ``__init_kwargs__`` respectively. .. note:: @@ -34,48 +34,56 @@ class Settings: Note that there is small performance overhead to initializing classes with this mixin so it should not be used for adding settings to all - classes without consideration. For classes that already store values - required to recover the ``__init__`` args they should instead - implement an appropriate :meth:`settings` property directly. + classes without consideration. """ def __new__(cls, *args, **kwargs): # This method automatically stores all arg and kwargs from subclass # init methods spec = inspect.getfullargspec(cls.__init__) + ord_args = OrderedDict() + ord_kwargs = OrderedDict() + + # Parse spec args + defaults = spec.defaults or [] + if defaults: + size = len(spec.args) - len(spec.defaults) + init_args = spec.args[1:size] + init_kwargs = spec.args[size:] + else: + init_args = spec.args[1:] + init_kwargs = [] + + # Initialize defaults to preserve correct arg order + num_args = len(init_args) + ord_args.update(zip(init_args, num_args * [None])) + ord_kwargs.update(zip(init_kwargs, defaults)) + + if init_args and args: + # Add named args + ord_args.update(zip(init_args, args)) + if init_kwargs and args: + # Update non-default values + ord_kwargs.update(zip(init_kwargs, args[num_args:])) + + # Parse variadic args if spec.varargs: - # raise exception if class init accepts variadic positional args - raise TypeError( - "Settings mixin cannot be used with an init method that " - " accepts variadic positional args " + num_varargs = len(args) - num_args + ord_args.update( + ((f"{spec.varargs}[{i}]", args[num_args + i]) for i in range(num_varargs)) ) - # Get lists of named args and kwargs for classes init method - init_args = spec.args[1:] - defaults_kwargs = spec.defaults or [] - num_named_kwargs = len(defaults_kwargs) - num_named_args = len(init_args) - num_named_kwargs - named_args = init_args[0:num_named_args] - named_kwargs = init_args[num_named_args:] - - # Initialize ordered dicts for named args and kwargs using the - # argspec ordering - ord_args = OrderedDict(zip(named_args, [None] * num_named_args)) - ord_kwargs = OrderedDict(zip(named_kwargs, defaults_kwargs)) - - # Sort called positional args - for i, (argname, argval) in enumerate(zip(init_args, args)): - if i < num_named_args: - ord_args[argname] = argval - else: - ord_kwargs[argname] = argval + # Add defaults for kwonly args + for kwarg in spec.kwonlyargs: + if kwarg not in ord_kwargs: + ord_kwargs[kwarg] = spec.kwonlydefaults.get(kwarg, None) - # Sort called kwargs - for argname, argval in kwargs.items(): - if argname in named_args: - ord_args[argname] = argval + # Parse kwargs + for arg, argval in kwargs.items(): + if arg in init_args: + ord_args[arg] = argval else: - ord_kwargs[argname] = argval + ord_kwargs[arg] = argval # pylint: disable = attribute-defined-outside-init instance = super().__new__(cls) @@ -98,17 +106,3 @@ def __new__(sub_cls, *args, **kwargs): # Monkey patch the subclass new method with the method with # fixed documentation annotations cls.__new__ = __new__ - - @property - def settings(self) -> Dict[str, Any]: - """Return the settings used to initialize this instance.""" - settings = {} - # Note that this relies on dicts entries being implicitly ordered - # to store init args as kwargs. - args = getattr(self, "__init_args__", {}) - for key, val in args.items(): - settings[key] = val - kwargs = getattr(self, "__init_kwargs__", {}) - for key, val in kwargs.items(): - settings[key] = val - return settings diff --git a/test/test_settings.py b/test/test_store_init_args.py similarity index 70% rename from test/test_settings.py rename to test/test_store_init_args.py index 6e6594eb4f..df46c68a8d 100644 --- a/test/test_settings.py +++ b/test/test_store_init_args.py @@ -13,18 +13,15 @@ """Tests for base experiment framework.""" from qiskit.test import QiskitTestCase -from qiskit_experiments.framework.settings import Settings +from qiskit_experiments.framework.store_init_args import StoreInitArgs -class ExampleSettingsVariadic(Settings): +class StoreArgsBase(StoreInitArgs): """Test class with args and kwargs property""" - def __init__(self, a, b, c="default_c", d="default_d", **kwargs): - pass - @property def args(self): - """Return sotred init args""" + """Return stored init args""" return tuple(getattr(self, "__init_args__", {}).values()) @property @@ -32,72 +29,78 @@ def kwargs(self): """Return stored init kwargs""" return dict(getattr(self, "__init_kwargs__", {})) + @property + def settings(self): + """Return settings dict of args and kwargs""" + ret = dict(getattr(self, "__init_args__", {})) + ret.update(**self.kwargs) + return ret -class ExampleSettings(Settings): + +class StoreArgsVariadic(StoreArgsBase): """Test class with args and kwargs property""" - def __init__(self, a, b, c="default_c", d="default_d"): + def __init__(self, a, *args, b, c="default_c", d="default_d", **kwargs): pass - @property - def args(self): - """Return sotred init args""" - return tuple(getattr(self, "__init_args__", {}).values()) - @property - def kwargs(self): - """Return stored init kwargs""" - return dict(getattr(self, "__init_kwargs__", {})) +class StoreArgsVariadicKw(StoreArgsBase): + """Test class with args and kwargs property""" + + def __init__(self, a, b, c="default_c", d="default_d", **kwargs): + pass + + +class StoreArgs(StoreArgsBase): + """Test class with args and kwargs property""" + + def __init__(self, a, b, c="default_c", d="default_d"): + pass class TestSettings(QiskitTestCase): """Test Settings mixin""" + # pylint: disable = missing-function-docstring + def test_standard(self): - """Test mixing for standard init class""" - obj = ExampleSettings(1, 2, c="custom_c") + obj = StoreArgs(1, 2, c="custom_c") self.assertEqual(obj.args, (1, 2)) self.assertEqual(obj.kwargs, {"c": "custom_c", "d": "default_d"}) self.assertEqual(obj.settings, {"a": 1, "b": 2, "c": "custom_c", "d": "default_d"}) def test_standard_pos_kwargs(self): - """Test mixing for standard init class with kwargs passed positionally""" - obj = ExampleSettings(1, 2, "custom_c") + obj = StoreArgs(1, 2, "custom_c") self.assertEqual(obj.args, (1, 2)) self.assertEqual(obj.kwargs, {"c": "custom_c", "d": "default_d"}) self.assertEqual(obj.settings, {"a": 1, "b": 2, "c": "custom_c", "d": "default_d"}) def test_standard_named_args(self): - """Test mixing for standard init class with kwargs passed positionally""" - obj = ExampleSettings(b=2, a=1, c="custom_c") + obj = StoreArgs(b=2, a=1, c="custom_c") self.assertEqual(obj.args, (1, 2)) self.assertEqual(obj.kwargs, {"c": "custom_c", "d": "default_d"}) self.assertEqual(obj.settings, {"a": 1, "b": 2, "c": "custom_c", "d": "default_d"}) def test_variadic(self): - """Test mixing for standard init class""" - obj = ExampleSettingsVariadic(1, 2, c="custom_c") + obj = StoreArgsVariadicKw(1, 2, c="custom_c") self.assertEqual(obj.args, (1, 2)) self.assertEqual(obj.kwargs, {"c": "custom_c", "d": "default_d"}) self.assertEqual(obj.settings, {"a": 1, "b": 2, "c": "custom_c", "d": "default_d"}) def test_variadic_pos_kwargs(self): - """Test mixing for standard init class with kwargs passed positionally""" - obj = ExampleSettingsVariadic(1, 2, "custom_c") + obj = StoreArgsVariadicKw(1, 2, "custom_c") self.assertEqual(obj.args, (1, 2)) self.assertEqual(obj.kwargs, {"c": "custom_c", "d": "default_d"}) self.assertEqual(obj.settings, {"a": 1, "b": 2, "c": "custom_c", "d": "default_d"}) def test_variadic_named_args(self): - """Test mixing for standard init class with kwargs passed positionally""" - obj = ExampleSettingsVariadic(b=2, a=1, c="custom_c") + obj = StoreArgsVariadicKw(b=2, a=1, c="custom_c") self.assertEqual(obj.args, (1, 2)) self.assertEqual(obj.kwargs, {"c": "custom_c", "d": "default_d"}) self.assertEqual(obj.settings, {"a": 1, "b": 2, "c": "custom_c", "d": "default_d"}) def test_variadic_kwargs(self): - """Test mixing for standard init class""" - obj = ExampleSettingsVariadic(1, 2, d="custom_d", f="kwarg_f", g="kwarg_g") + obj = StoreArgsVariadicKw(1, 2, d="custom_d", f="kwarg_f", g="kwarg_g") self.assertEqual(obj.args, (1, 2)) self.assertEqual( obj.kwargs, {"c": "default_c", "d": "custom_d", "f": "kwarg_f", "g": "kwarg_g"} @@ -108,8 +111,7 @@ def test_variadic_kwargs(self): ) def test_variadic_kwargs_pos_kwargs(self): - """Test mixing for standard init class""" - obj = ExampleSettingsVariadic(1, 2, "custom_c", f="kwarg_f", g="kwarg_g") + obj = StoreArgsVariadicKw(1, 2, "custom_c", f="kwarg_f", g="kwarg_g") self.assertEqual(obj.args, (1, 2)) self.assertEqual( obj.kwargs, {"c": "custom_c", "d": "default_d", "f": "kwarg_f", "g": "kwarg_g"} @@ -120,8 +122,7 @@ def test_variadic_kwargs_pos_kwargs(self): ) def test_variadic_kwargs_named_args(self): - """Test mixing for standard init class""" - obj = ExampleSettingsVariadic(b=2, a=1, d="custom_d", f="kwarg_f", g="kwarg_g") + obj = StoreArgsVariadicKw(b=2, a=1, d="custom_d", f="kwarg_f", g="kwarg_g") self.assertEqual(obj.args, (1, 2)) self.assertEqual( obj.kwargs, {"c": "default_c", "d": "custom_d", "f": "kwarg_f", "g": "kwarg_g"} @@ -130,3 +131,11 @@ def test_variadic_kwargs_named_args(self): obj.settings, {"a": 1, "b": 2, "c": "default_c", "d": "custom_d", "f": "kwarg_f", "g": "kwarg_g"}, ) + + def test_variadic_args(self): + obj = StoreArgsVariadic(1, 2, b="custom_b", c="custom_c", f="kwarg_f", g="kwarg_g") + self.assertEqual(obj.args, (1, 2)) + self.assertEqual( + obj.kwargs, + {"b": "custom_b", "c": "custom_c", "d": "default_d", "f": "kwarg_f", "g": "kwarg_g"}, + )