Skip to content

Commit

Permalink
Merge pull request #6012 from jenshnielsen/typed_kwargs
Browse files Browse the repository at this point in the history
Use typeddicts for better kwargs typing in Instrument classes
  • Loading branch information
jenshnielsen committed May 17, 2024
2 parents d91be17 + 90451da commit 239e936
Show file tree
Hide file tree
Showing 69 changed files with 1,066 additions and 376 deletions.
10 changes: 10 additions & 0 deletions docs/changes/newsfragments/6012.breaking
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
The keyword arguments expected to be passed to ``InstrumentBase`` and ``VisaInstrument`` subclasses are now
documented as TypedDics classes that can be used to type `**kwargs` in the subclass constructors.
See `Creating QCoDeS instrument drivers` for usage examples.

This also means that the these arguments **must** be passed as keyword arguments, and not as positional arguments.
This specifically includeds passing ``label`` and ``metadata`` to direct subclasses of ``Instrument`` as well as
``terminator`` to subclasses of ``VisaInstrument``.

All drivers shipping with qcodes for Vendors from A-K have been updated.
The remaining drivers will be updated in a subsequent pull request.
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,12 @@
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Logging hadn't been started.\n",
"Activating auto-logging. Current session state plus future input saved.\n",
"Filename : C:\\Users\\jenielse\\.qcodes\\logs\\command_history.log\n",
"Mode : append\n",
"Output logging : True\n",
"Raw input log : False\n",
"Timestamping : True\n",
"State : active\n",
"Qcodes Logfile : C:\\Users\\jenielse\\.qcodes\\logs\\220617-25816-qcodes.log\n"
]
}
],
"outputs": [],
"source": [
"from typing import TYPE_CHECKING\n",
"\n",
"if TYPE_CHECKING:\n",
" from typing_extensions import Unpack\n",
"import os\n",
"\n",
"import numpy as np\n",
Expand All @@ -44,7 +32,7 @@
" load_or_create_experiment,\n",
" plot_dataset,\n",
")\n",
"from qcodes.instrument import Instrument\n",
"from qcodes.instrument import Instrument, InstrumentBaseKWArgs\n",
"from qcodes.parameters import Parameter, ParameterWithSetpoints"
]
},
Expand Down Expand Up @@ -111,7 +99,7 @@
"\n",
"class OzzyLowScope(Instrument):\n",
"\n",
" def __init__(self, name, **kwargs):\n",
" def __init__(self, name: str, **kwargs: \"Unpack[InstrumentBaseKWArgs]\"):\n",
"\n",
" super().__init__(name, **kwargs)\n",
"\n",
Expand Down Expand Up @@ -494,7 +482,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.13"
"version": "3.12.1"
},
"toc": {
"base_numbering": 1,
Expand Down
46 changes: 33 additions & 13 deletions docs/examples/writing_drivers/Creating-Instrument-Drivers.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,23 @@
"import ctypes # only for DLL-based instrument\n",
"import sys\n",
"import time\n",
"from typing import Any\n",
"from typing import TYPE_CHECKING, Any\n",
"\n",
"if TYPE_CHECKING:\n",
" from typing_extensions import (\n",
" Unpack, # can be imported from typing if python >= 3.12\n",
" )\n",
"\n",
"import numpy as np\n",
"\n",
"from qcodes import validators as vals\n",
"from qcodes.instrument import Instrument, InstrumentChannel, VisaInstrument\n",
"from qcodes.instrument import (\n",
" Instrument,\n",
" InstrumentBaseKWArgs,\n",
" InstrumentChannel,\n",
" VisaInstrument,\n",
" VisaInstrumentKWArgs,\n",
")\n",
"from qcodes.instrument_drivers.AlazarTech.utils import TraceParameter\n",
"from qcodes.parameters import ManualParameter, MultiParameter, Parameter"
]
Expand Down Expand Up @@ -196,10 +207,17 @@
" \"\"\"\n",
"\n",
" # all instrument constructors should accept **kwargs and pass them on to\n",
" # super().__init__\n",
" def __init__(self, name, address, **kwargs):\n",
" # supplying the terminator means you don't need to remove it from every response\n",
" super().__init__(name, address, terminator=\"\\r\", **kwargs)\n",
" # super().__init__ By using Unpack[VisaKWArgs] we ensure that the instrument class takes exactly the same keyword args as the base class\n",
" # Visa instruments are also required to take name, address and terminator as arguments.\n",
" # name and address should be positional arguments. To overwrite the default terminator or timeout\n",
" # the attribute default_terminator or default_timeout should be defined as a class attribute in the instrument class\n",
" # as shown below for the default_terminator\n",
" default_terminator = \"\\r\"\n",
"\n",
" def __init__(\n",
" self, name: str, address: str, **kwargs: \"Unpack[VisaInstrumentKWArgs]\"\n",
" ):\n",
" super().__init__(name, address, **kwargs)\n",
"\n",
" self.attenuation = Parameter(\n",
" \"attenuation\",\n",
Expand Down Expand Up @@ -266,20 +284,21 @@
" SMUA and SMUB.\n",
" \"\"\"\n",
"\n",
" def __init__(self, parent: Instrument, name: str, channel: str) -> None:\n",
" def __init__(self, parent: Instrument, name: str, channel: str, **kwargs: \"Unpack[InstrumentBaseKWArgs]\") -> None:\n",
" \"\"\"\n",
" Args:\n",
" parent: The Instrument instance to which the channel is\n",
" to be attached.\n",
" name: The 'colloquial' name of the channel\n",
" channel: The name used by the Keithley, i.e. either\n",
" 'smua' or 'smub'\n",
" **kwargs: Forwarded to base class.\n",
" \"\"\"\n",
"\n",
" if channel not in [\"smua\", \"smub\"]:\n",
" raise ValueError('channel must be either \"smub\" or \"smua\"')\n",
"\n",
" super().__init__(parent, name)\n",
" super().__init__(parent, name, **kwargs)\n",
" self.model = self._parent.model\n",
"\n",
" self.volt = Parameter(\n",
Expand Down Expand Up @@ -342,15 +361,16 @@
" This is the qcodes driver for the Keithley2600 Source-Meter series,\n",
" tested with Keithley2614B\n",
" \"\"\"\n",
" default_terminator = \"\\n\"\n",
"\n",
" def __init__(self, name: str, address: str, **kwargs) -> None:\n",
" def __init__(self, name: str, address: str, **kwargs: \"Unpack[VisaInstrumentKWArgs]\") -> None:\n",
" \"\"\"\n",
" Args:\n",
" name: Name to use internally in QCoDeS\n",
" address: VISA ressource address\n",
" **kwargs: kwargs are forwarded to the base class.\n",
" \"\"\"\n",
" super().__init__(name, address, terminator=\"\\n\", **kwargs)\n",
" super().__init__(name, address, **kwargs)\n",
"\n",
" model = self.ask(\"localnode.model\")\n",
"\n",
Expand Down Expand Up @@ -416,7 +436,7 @@
"class AlazarTechATS(Instrument):\n",
" dll_path = \"C:\\\\WINDOWS\\\\System32\\\\ATSApi\"\n",
"\n",
" def __init__(self, name, system_id=1, board_id=1, dll_path=None, **kwargs):\n",
" def __init__(self, name, system_id=1, board_id=1, dll_path=None, **kwargs: \"Unpack[InstrumentBaseKWArgs]\"):\n",
" super().__init__(name, **kwargs)\n",
"\n",
" # connect to the DLL\n",
Expand Down Expand Up @@ -471,7 +491,7 @@
"class SomeDLLInstrument(Instrument):\n",
" dll_path = \"C:\\\\WINDOWS\\\\System32\\\\ATSApi\"\n",
"\n",
" def __init__(self, name, dll_path=None, **kwargs):\n",
" def __init__(self, name, dll_path=None, **kwargs: \"Unpack[InstrumentBaseKWArgs]\"):\n",
" super().__init__(name, **kwargs)\n",
"\n",
" if sys.platform != \"win32\":\n",
Expand Down Expand Up @@ -557,7 +577,7 @@
" This is a virtual driver only and will not talk to your instrument.\n",
" \"\"\"\n",
"\n",
" def __init__(self, name, **kwargs):\n",
" def __init__(self, name: str, **kwargs: \"Unpack[InstrumentBaseKWArgs]\"):\n",
" super().__init__(name, **kwargs)\n",
"\n",
" # ManualParameter has an \"initial_value\" kwarg, but if you use this\n",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,20 +66,26 @@
}
],
"source": [
"from typing import TYPE_CHECKING\n",
"\n",
"import numpy as np\n",
"\n",
"import qcodes.validators as vals\n",
"from qcodes.instrument.visa import VisaInstrument\n",
"from qcodes.instrument.visa import VisaInstrument, VisaInstrumentKWArgs\n",
"\n",
"if TYPE_CHECKING:\n",
" from typing_extensions import Unpack\n",
"\n",
"\n",
"class Weinschel8320(VisaInstrument):\n",
" \"\"\"\n",
" QCoDeS driver for the stepped attenuator\n",
" Weinschel is formerly known as Aeroflex/Weinschel\n",
" \"\"\"\n",
" default_terminator = \"\\r\"\n",
"\n",
" def __init__(self, name, address, **kwargs):\n",
" super().__init__(name, address, terminator='\\r', **kwargs)\n",
" def __init__(self, name: str, address: str, **kwargs: 'Unpack[VisaInstrumentKWArgs]'):\n",
" super().__init__(name, address, **kwargs)\n",
"\n",
" self.add_parameter('attenuation', unit='dB',\n",
" set_cmd='ATTN ALL {:02.0f}',\n",
Expand Down
6 changes: 4 additions & 2 deletions src/qcodes/instrument/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,20 @@

from .channel import ChannelList, ChannelTuple, InstrumentChannel, InstrumentModule
from .instrument import Instrument, find_or_create_instrument
from .instrument_base import InstrumentBase
from .instrument_base import InstrumentBase, InstrumentBaseKWArgs
from .ip import IPInstrument
from .visa import VisaInstrument
from .visa import VisaInstrument, VisaInstrumentKWArgs

__all__ = [
"ChannelList",
"ChannelTuple",
"IPInstrument",
"Instrument",
"InstrumentBase",
"InstrumentBaseKWArgs",
"InstrumentChannel",
"InstrumentModule",
"VisaInstrument",
"VisaInstrumentKWArgs",
"find_or_create_instrument",
]
12 changes: 7 additions & 5 deletions src/qcodes/instrument/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
from .instrument_base import InstrumentBase

if TYPE_CHECKING:
from typing_extensions import Unpack

from .instrument import Instrument
from .instrument_base import InstrumentBaseKWArgs


class InstrumentModule(InstrumentBase):
Expand All @@ -33,15 +36,14 @@ class InstrumentModule(InstrumentBase):
Args:
parent: The instrument to which this module should be
attached.
name: The name of this module.
**kwargs: Forwarded to the base class.
"""

def __init__(self,
parent: InstrumentBase,
name: str,
**kwargs: Any) -> None:
def __init__(
self, parent: InstrumentBase, name: str, **kwargs: Unpack[InstrumentBaseKWArgs]
) -> None:
# need to specify parent before `super().__init__` so that the right
# `full_name` is available in that scope. `full_name` is used for
# registering the filter for the log messages. It is composed by
Expand Down
14 changes: 4 additions & 10 deletions src/qcodes/instrument/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@
from qcodes.utils import strip_attrs
from qcodes.validators import Anything

from .instrument_base import InstrumentBase
from .instrument_base import InstrumentBase, InstrumentBaseKWArgs
from .instrument_meta import InstrumentMeta

if TYPE_CHECKING:
from collections.abc import Mapping
from typing_extensions import Unpack

from qcodes.logger.instrument_logger import InstrumentLoggerAdapter


log = logging.getLogger(__name__)


Expand Down Expand Up @@ -66,16 +65,11 @@ class Instrument(InstrumentBase, metaclass=instrument_meta_class):
_type: type[Instrument] | None = None
_instances: weakref.WeakSet[Instrument] = weakref.WeakSet()

def __init__(
self,
name: str,
metadata: Mapping[Any, Any] | None = None,
label: str | None = None,
) -> None:
def __init__(self, name: str, **kwargs: Unpack[InstrumentBaseKWArgs]) -> None:

self._t0 = time.time()

super().__init__(name=name, metadata=metadata, label=label)
super().__init__(name=name, **kwargs)

self.add_parameter("IDN", get_cmd=self.get_idn, vals=Anything())

Expand Down
21 changes: 21 additions & 0 deletions src/qcodes/instrument/instrument_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from typing import TYPE_CHECKING, Any, ClassVar

import numpy as np
from typing_extensions import TypedDict

from qcodes.logger import get_instrument_logger
from qcodes.metadatable import Metadatable, MetadatableWithName
Expand All @@ -16,6 +17,8 @@
if TYPE_CHECKING:
from collections.abc import Callable, Mapping, Sequence

from typing_extensions import NotRequired

from qcodes.instrument.channel import ChannelTuple, InstrumentModule
from qcodes.logger.instrument_logger import InstrumentLoggerAdapter

Expand All @@ -24,6 +27,24 @@
log = logging.getLogger(__name__)


class InstrumentBaseKWArgs(TypedDict):
"""
This TypedDict defines the type of the kwargs that can be passed to the InstrumentBase class.
A subclass of VisaInstrument should take ``**kwargs: Unpack[InstrumentBaseKWArgs]`` as input
and forward this to the super class to ensure that it can accept all the arguments defined here.
"""

metadata: NotRequired[Mapping[Any, Any] | None]
"""
Additional static metadata to add to this
instrument's JSON snapshot.
"""
label: NotRequired[str | None]
"""
Nicely formatted name of the instrument; if None,
the ``name`` is used.
"""

class InstrumentBase(MetadatableWithName, DelegateAttributes):
"""
Base class for all QCodes instruments and instrument channels
Expand Down

0 comments on commit 239e936

Please sign in to comment.