Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A new way of adding parameters that allows overriding inherited parameters (and sphinx auto documentation) #3098

Open
wants to merge 41 commits into
base: main
Choose a base branch
from
Open
Changes from 11 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
eb5eb2c
first prototype add_parameter decorator
caenrigen Jun 8, 2021
2466c8d
improved usability for instance attributes
caenrigen Jun 8, 2021
e6d5d76
some comments
caenrigen Jun 8, 2021
5220fb0
fix codacy
caenrigen Jun 8, 2021
1224732
Merge branch 'master' into feat/add_parameter_decorator
caenrigen Jun 8, 2021
6d3f06d
address review comments
caenrigen Jun 9, 2021
a1b9e8b
bug fix
caenrigen Jun 9, 2021
3599bb5
minor var rename
caenrigen Jun 9, 2021
918ba4b
bug fix
caenrigen Jun 9, 2021
4dc1864
Merge branch 'master' into feat/add_parameter_decorator
caenrigen Jun 9, 2021
6e23d34
proper use of Callable
caenrigen Jun 9, 2021
966a61e
Merge branch 'master' into feat/add_parameter_decorator
astafan8 Jun 14, 2021
df0b771
added sphinx extension
caenrigen Jun 16, 2021
c0e2ce4
Merge remote-tracking branch 'origin/feat/add_parameter_decorator' in…
caenrigen Jun 16, 2021
6868d38
started documenting
caenrigen Jun 16, 2021
5b0e14b
docs enh
caenrigen Jun 16, 2021
89c5968
some cleanup
caenrigen Jun 16, 2021
895d5c1
wip
caenrigen Jun 17, 2021
4c87cb9
solution to render rst in notebooks
caenrigen Jun 17, 2021
4c51987
done documenting
caenrigen Jun 17, 2021
9142fbf
missing some changes to docs
caenrigen Jun 21, 2021
44ca9a6
improve error handling
caenrigen Jun 21, 2021
9f86aa3
fix codacy issues
caenrigen Jun 21, 2021
198ded8
Merge branch 'master' into feat/add_parameter_decorator
astafan8 Jun 21, 2021
a92131e
allow decorated methods to specify return types, intended for parameters
caenrigen Jun 21, 2021
ad2ea22
Merge remote-tracking branch 'origin/feat/add_parameter_decorator' in…
caenrigen Jun 21, 2021
606c36b
Merge branch 'master' into feat/add_parameter_decorator
astafan8 Jun 23, 2021
a2a8afb
address comments
caenrigen Jun 23, 2021
5f46e5b
Merge remote-tracking branch 'origin/feat/add_parameter_decorator' in…
caenrigen Jun 23, 2021
63acaf3
address comments
caenrigen Jun 23, 2021
26ae403
remove comment
caenrigen Jun 23, 2021
99a07d1
codacy fix(?)
caenrigen Jun 23, 2021
d4118ce
Merge branch 'master' into feat/add_parameter_decorator
astafan8 Jun 23, 2021
1a29c21
Merge remote-tracking branch 'origin/feat/add_parameter_decorator' in…
caenrigen Jun 23, 2021
b07eafa
typo
caenrigen Jun 24, 2021
4436bf2
tests wip
caenrigen Jun 24, 2021
d00b149
raise errors asap
caenrigen Jun 25, 2021
5455e9d
finish test
caenrigen Jun 25, 2021
f8f1d09
fix codacy (?)
caenrigen Jun 25, 2021
9ba7bc0
fix list in sphinx
caenrigen Jul 5, 2021
b18c448
print lambda
caenrigen Jul 7, 2021
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
147 changes: 147 additions & 0 deletions qcodes/instrument/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@
Type,
Union,
cast,
TypeVar
)

caenrigen marked this conversation as resolved.
Show resolved Hide resolved
from functools import wraps
import inspect

import numpy as np

from qcodes.logger.instrument_logger import get_instrument_logger
Expand Down Expand Up @@ -74,6 +78,12 @@ def __init__(self, name: str, metadata: Optional[Mapping[Any, Any]] = None) -> N

self.log = get_instrument_logger(self, __name__)

if getattr(self, "_call_add_params_from_decorated_methods", True):
# setting self._call_add_params_from_decorated_methods=False before calling
# `super().__init__()` gives more control in driver whose parameters require
# information available only in the `__init__()` of a driver.
self._add_params_from_decorated_methods()

@property
def name(self) -> str:
"""Name of the instrument"""
Expand Down Expand Up @@ -401,6 +411,143 @@ def validate_status(self, verbose: bool = False) -> None:
print(f'validate_status: param {k}: {value}')
p.validate(value)

def _add_params_from_decorated_methods(self) -> None:
"""
Transforms methods decorated with `@add_parameter` into actual parameters and
uses `self.add_parameter(...)` to add them to this instrument.

Intended to be called in the `__init__()` of `InstrumentBase` or in a driver
subclass.

.. seealso::

- :obj:`qcodes.instrument.base.add_parameter`
- :obj:`qcodes.instrument.base.InstanceAttr`
- :obj:`qcodes.instrument.base.DECORATED_METHOD_PREFIX`
- :obj:`qcodes.instrument.base.ADD_PARAMETER_ATTR_NAME`

"""

def kwarg_to_attr(self, kwarg_name, kwarg_value, param_name) -> Any:
caenrigen marked this conversation as resolved.
Show resolved Hide resolved
"""
Returns the attribute that has the name `kwarg_value.attr_name`
of this Instrument instance if the `kwarg_value` is of type `InstanceAttr`,
otherwise just returns the `kwarg_value`.

Args:
kwarg_name: The name of the kwarg used in the definition of the
method decorated with `@add_parameter`. Only necessary for raising
informative exceptions.
kwarg_value: The default value of the kwarg used in the definition of
the method decorated with `@add_parameter`.
param_name: The name of the parameter that is to be added. Only
necessary for raising informative exceptions.

Raises:
AttributeError: When the `self` does not have the attribute the user
expected the Instrument instance to have.
"""
if isinstance(kwarg_value, InstanceAttr):
if not hasattr(self, kwarg_value.attr_name):
raise AttributeError(
f"Failed to determine the value of {kwarg_name!r} when creating"
f" the {param_name!r} parameter.\nInstance {self} does not have"
f" an attribute named {kwarg_value.attr_name!r}.\n"
f"Make sure the {kwarg_value.attr_name!r} attribute exist when "
"`self._add_params_from_decorated_methods()` is called."
)
return getattr(self, kwarg_value.attr_name)
caenrigen marked this conversation as resolved.
Show resolved Hide resolved
return kwarg_value

# decorated methods are required to have a specific prefix
has_parameter_prefix = lambda s: s.startswith(DECORATED_METHOD_PREFIX)

for obj_name in filter(has_parameter_prefix, dir(self)):
meth = getattr(self, obj_name)
# the `@add_parameter` decorator adds an attribute we check for here
if hasattr(meth, ADD_PARAMETER_ATTR_NAME):
param_name = meth.__name__[len(DECORATED_METHOD_PREFIX):]
kwargs = {
kw_name: kwarg_to_attr(self, kw_name, kw_value.default, param_name)
for kw_name, kw_value in inspect.signature(meth).parameters.items()
}
self.add_parameter(
name=param_name,
docstring=meth.__doc__,
caenrigen marked this conversation as resolved.
Show resolved Hide resolved
**kwargs,
)


class InstanceAttr:
"""
An auxiliary class to be used together with the
:obj:`qcodes.instrument.base.add_parameter` decorator to allow adding parameters
that require information available only during the `__init__()` of an Instrument
subclass.
"""
def __init__(self, attr_name: str) -> None:
"""Instantiates the class and saves the passed in attr_name."""
self.attr_name: str = attr_name

def __repr__(self) -> str:
"""Return the class name and `self.attr_name`."""
return f"{self.__class__.__name__}({self.attr_name!r})"


DECORATED_METHOD_PREFIX = "_parameter_"
"""
A constant defining the prefix of the methods on which
:obj:`qcodes.instrument.base.add_parameter` decorator can be used. The intention is to
keep the name of these methods fairly unique and private to avoid any foreseeable clash.
"""

ADD_PARAMETER_ATTR_NAME = "_add_parameter"
"""
A constant defining the name of the attribute set by
:obj:`qcodes.instrument.base.add_parameter` decorator to flag a method that will be
converted to parameter.
"""

_ParamSpec = TypeVar("_ParamSpec")
caenrigen marked this conversation as resolved.
Show resolved Hide resolved
"""
A custom type to annotate the type hints of `add_parameter`.
"""

def add_parameter(method: Callable[[_ParamSpec], None]) -> Callable[[_ParamSpec], None]:
"""
caenrigen marked this conversation as resolved.
Show resolved Hide resolved
A decorator function that wraps a method of an Instrument subclass such that the
new method will be converted into the corresponding :code:`param_class`
in the :code:`_add_params_from_decorated_methods`.

Args:
method: The method to be wrapped and flagged to be converted to parameter.
"""

if not method.__name__.startswith(DECORATED_METHOD_PREFIX):
raise ValueError(
f"Only methods prefixed with {DECORATED_METHOD_PREFIX!r} can be decorated "
f"with `add_parameter` decorator."
)

@wraps(method) # preserves info like `__doc__` and signature
caenrigen marked this conversation as resolved.
Show resolved Hide resolved
def kwargs_and_doc_container(self, *args, **kwargs) -> None:
caenrigen marked this conversation as resolved.
Show resolved Hide resolved
"""
Auxiliary function that serves as a container of information.

Raises:
RuntimeError: Always.
"""
raise RuntimeError(
f"Method not intended to be called.\n"
f"{method.__name__!r} is a special method used as information container "
f"for creating and assigning parameters to {self}."
)

# special attribute to flag method for conversion to parameter
setattr(kwargs_and_doc_container, ADD_PARAMETER_ATTR_NAME, True)

return kwargs_and_doc_container


class AbstractInstrument(ABC):
"""ABC that is useful for defining mixin classes for Instrument class"""
Expand Down