Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions qiskit_experiments/framework/base_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__

Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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::

Expand All @@ -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)
Expand All @@ -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
77 changes: 43 additions & 34 deletions test/test_settings.py → test/test_store_init_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,91 +13,94 @@
"""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
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"}
Expand All @@ -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"}
Expand All @@ -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"}
Expand All @@ -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"},
)