From e65e64b5b232fa3c335dfa19f48c34d2e5bc1816 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 19 Aug 2026 07:18:07 +0200 Subject: [PATCH 01/70] Narrow Arrays shape elements without callable() callable() does not narrow int | Callable[[], int], since a subclass of int could define __call__, so ty infers calling the element as returning object. Checking isinstance(s, int) first lets ty narrow the remaining branch to the callable. Elements that are not callable are still used as they are, so numpy integers in a shape keep working at runtime. --- src/qcodes/validators/validators.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/qcodes/validators/validators.py b/src/qcodes/validators/validators.py index f76331bc1531..457655c4d3f3 100644 --- a/src/qcodes/validators/validators.py +++ b/src/qcodes/validators/validators.py @@ -985,12 +985,12 @@ def shape_unevaluated(self) -> shape_tuple_type: def shape(self) -> tuple[int, ...] | None: if self._shape is None: return None - shape_array = [] + shape_array: list[int] = [] for s in self._shape: - if callable(s): - shape_array.append(s()) - else: + if isinstance(s, int) or not callable(s): shape_array.append(s) + else: + shape_array.append(s()) shape = tuple(shape_array) return shape From cb3ad4eadc391df09aaedb0133e537686ad6944b Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 19 Aug 2026 07:53:46 +0200 Subject: [PATCH 02/70] Add ty type checker configuration Scope ty to src and tests and exclude the legacy Decadac driver, mirroring the existing pyright config. Disable import resolution rules for the drivers that depend on optional packages, as already done for mypy. Check against all platforms so that Windows only drivers are type checked independently of the platform ty runs on. --- pyproject.toml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 93a6f1c39da4..2158bbb45d47 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -472,6 +472,38 @@ quote-annotations = true sdist = "versioningit.cmdclass.sdist" build_py = "versioningit.cmdclass.build_py" +[tool.ty.environment] +# a number of drivers are only usable on Windows. Checking against all +# platforms means that these are type checked no matter which platform ty +# runs on, and that the result does not depend on the platform of the developer. +python-platform = "all" + +[tool.ty.src] +# mirrors the include and ignore settings of pyright above +include = ["src", "tests"] +exclude = [ + "src/qcodes/instrument_drivers/Harvard/Decadac.py", + ] + +# these are packages that we import +# but don't have installed by default. +# Compare with ignore_missing_imports in the mypy config above +[[tool.ty.overrides]] +include = [ + "src/qcodes/instrument_drivers/Galil/dmc_41x3.py", + "src/qcodes/instrument_drivers/Minicircuits/USBHIDMixin.py", + "src/qcodes/instrument_drivers/Minicircuits/_minicircuits_usb_spdt.py", +] +[tool.ty.overrides.rules] +unresolved-import = "ignore" + +# clr is provided by pythonnet which is not installed by default +# so its members cannot be resolved either +[[tool.ty.overrides]] +include = ["src/qcodes/instrument_drivers/Minicircuits/_minicircuits_usb_spdt.py"] +[tool.ty.overrides.rules] +unresolved-attribute = "ignore" + [tool.towncrier] package = "qcodes" name = "QCoDeS" From 514cd7abac131822977537938a0b48c26aebdfa0 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 19 Aug 2026 20:04:20 +0200 Subject: [PATCH 03/70] Dispatch Parameter.get_raw/set_raw instead of overwriting them Parameter used to replace its own get_raw/set_raw methods with the implementation generated from get_cmd/set_cmd. Assigning over a method makes type checkers infer get_raw/set_raw to be instance attributes of Parameter, which made every subclass implementing them as regular methods an invalid override. Store the generated implementation on the instance and let get_raw and set_raw dispatch to it. They stay marked abstract so that _implements_get_raw keeps reporting False for Parameter itself. Clears 66 ty diagnostics. --- .../changes/newsfragments/8441.underthehood.1 | 9 +++ src/qcodes/parameters/parameter.py | 55 +++++++++++++++---- 2 files changed, 53 insertions(+), 11 deletions(-) create mode 100644 docs/changes/newsfragments/8441.underthehood.1 diff --git a/docs/changes/newsfragments/8441.underthehood.1 b/docs/changes/newsfragments/8441.underthehood.1 new file mode 100644 index 000000000000..b7ee1dd573a6 --- /dev/null +++ b/docs/changes/newsfragments/8441.underthehood.1 @@ -0,0 +1,9 @@ +:class:`.Parameter` no longer replaces its own ``get_raw``/``set_raw`` methods +with the implementation generated from ``get_cmd``/``set_cmd``. The generated +implementation is stored on the parameter instead, and ``get_raw``/``set_raw`` +are now regular methods that dispatch to it. Assigning over the methods made +static type checkers infer ``get_raw``/``set_raw`` to be instance attributes of +:class:`.Parameter`, which in turn made every subclass implementing them as +regular methods an invalid override. There is no change in behaviour; note only +that ``parameter.get_raw`` is now always a bound method rather than, depending +on the arguments, a ``Command`` instance. diff --git a/src/qcodes/parameters/parameter.py b/src/qcodes/parameters/parameter.py index f81dcc9d3036..564da8898520 100644 --- a/src/qcodes/parameters/parameter.py +++ b/src/qcodes/parameters/parameter.py @@ -10,6 +10,8 @@ from typing_extensions import TypedDict +from qcodes.utils import qcodes_abstractmethod + from .command import Command from .parameter_base import ( InstrumentTypeVar_co, @@ -286,6 +288,12 @@ class Parameter( """ + _get_raw_impl: Callable[[], ParamRawDataType] | None = None + """Implementation of ``get_raw`` generated from ``get_cmd``, if any.""" + + _set_raw_impl: Callable[[ParamRawDataType], None] | None = None + """Implementation of ``set_raw`` generated from ``set_cmd``, if any.""" + def __init__( self, name: str, @@ -382,9 +390,9 @@ def _set_manual_parameter( " get_raw is an error." ) elif not self._implements_get_raw and get_cmd is not False: + get_raw_impl: Callable[[], ParamRawDataType] if get_cmd is None: - # ignore typeerror since mypy does not allow setting a method dynamically - self.get_raw = MethodType(_get_manual_parameter, self) # type: ignore[method-assign] + get_raw_impl = MethodType(_get_manual_parameter, self) else: if isinstance(get_cmd, str) and instrument is None: raise TypeError( @@ -396,14 +404,14 @@ def _set_manual_parameter( exec_str_ask = getattr(instrument, "ask", None) if instrument else None # TODO get_raw should also be a method here. This should probably be done by wrapping # it with MethodType like above - # ignore typeerror since mypy does not allow setting a method dynamically - self.get_raw = Command( # type: ignore[method-assign] + get_raw_impl = Command( arg_count=0, cmd=get_cmd, exec_str=exec_str_ask, ) + self._get_raw_impl = get_raw_impl self._gettable = True - self.get = self._wrap_get(self.get_raw) + self.get = self._wrap_get(get_raw_impl) if self._implements_set_raw and set_cmd not in (None, False): raise TypeError( @@ -412,9 +420,9 @@ def _set_manual_parameter( " set_raw is an error." ) elif not self._implements_set_raw and set_cmd is not False: + set_raw_impl: Callable[[ParamRawDataType], None] if set_cmd is None: - # ignore typeerror since mypy does not allow setting a method dynamically - self.set_raw = MethodType(_set_manual_parameter, self) # type: ignore[method-assign] + set_raw_impl = MethodType(_set_manual_parameter, self) else: if isinstance(set_cmd, str) and instrument is None: raise TypeError( @@ -426,14 +434,14 @@ def _set_manual_parameter( exec_str_write = ( getattr(instrument, "write", None) if instrument else None ) - # TODO get_raw should also be a method here. This should probably be done by wrapping + # TODO set_raw should also be a method here. This should probably be done by wrapping # it with MethodType like above - # ignore typeerror since mypy does not allow setting a method dynamically - self.set_raw = Command( # type: ignore[assignment] + set_raw_impl = Command( arg_count=1, cmd=set_cmd, exec_str=exec_str_write ) + self._set_raw_impl = set_raw_impl self._settable = True - self.set = self._wrap_set(self.set_raw) + self.set = self._wrap_set(set_raw_impl) self._meta_attrs.extend(["label", "unit", "vals"]) @@ -459,6 +467,31 @@ def _set_manual_parameter( self._docstring = docstring self.__doc__ = self._build__doc__() + @qcodes_abstractmethod + def get_raw(self) -> ParamRawDataType: + """ + Call the ``get_raw`` implementation generated from ``get_cmd``. + + This method stays marked as abstract so that + :attr:`~ParameterBase._implements_get_raw` keeps reporting ``False`` + for :class:`Parameter` itself: a subclass is still expected to either + override ``get_raw`` or supply a ``get_cmd``. + """ + if self._get_raw_impl is None: + raise NotImplementedError + return self._get_raw_impl() + + @qcodes_abstractmethod + def set_raw(self, value: ParamRawDataType) -> None: + """ + Call the ``set_raw`` implementation generated from ``set_cmd``. + + See :meth:`get_raw` for why this method stays marked as abstract. + """ + if self._set_raw_impl is None: + raise NotImplementedError + self._set_raw_impl(value) + def _build__doc__(self) -> str: if len(self.validators) == 0: validator_docstrings = ["* `vals` None"] From e61ad1a91024227f61ea7117a747bbcb12eb6b30 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 19 Aug 2026 20:15:46 +0200 Subject: [PATCH 04/70] Default TParameter to Parameter[Any, Any] add_parameter always binds the new parameter to self, so defaulting TParameter to a bare Parameter, which expands to Parameter[Any, InstrumentBase | None], wrongly claimed the instrument was InstrumentBase | None. As InstrumentTypeVar_co is covariant this made the result unassignable to the Parameter[SomeType, Self] annotations drivers use. ty applies a PEP 696 typevar default before considering the return type context, so it hit the default rather than solving from the declared type. mypy and pyright were unaffected. Clears 33 ty diagnostics. --- docs/changes/newsfragments/8441.underthehood.2 | 9 +++++++++ src/qcodes/instrument/instrument_base.py | 8 +++++++- tests/test_instrument.py | 6 ++++-- 3 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 docs/changes/newsfragments/8441.underthehood.2 diff --git a/docs/changes/newsfragments/8441.underthehood.2 b/docs/changes/newsfragments/8441.underthehood.2 new file mode 100644 index 000000000000..674cd1067ae2 --- /dev/null +++ b/docs/changes/newsfragments/8441.underthehood.2 @@ -0,0 +1,9 @@ +The ``TParameter`` type variable used by :meth:`.InstrumentBase.add_parameter` +now defaults to ``Parameter[Any, Any]`` rather than to a bare ``Parameter``. +When ``add_parameter`` is called without an explicit ``parameter_class`` the +returned parameter is bound to the instrument it is added to, so the previous +default (which expands to ``Parameter[Any, InstrumentBase | None]``) wrongly +claimed that the instrument was ``InstrumentBase | None``. This made the result +unassignable to the ``Parameter[SomeType, Self]`` annotations that drivers use. +Code that relies on the inferred type of an unannotated +``instrument.add_parameter("name")`` will now see ``Parameter[Any, Any]``. diff --git a/src/qcodes/instrument/instrument_base.py b/src/qcodes/instrument/instrument_base.py index 1947e494f018..3affcc1bded4 100644 --- a/src/qcodes/instrument/instrument_base.py +++ b/src/qcodes/instrument/instrument_base.py @@ -32,7 +32,13 @@ log = logging.getLogger(__name__) # Cannot convert to PEP 695: uses default= which requires PEP 696 (Python 3.13+). -TParameter = TypeVar("TParameter", bound="ParameterBase", default="Parameter") +# The default is `Parameter[Any, Any]` rather than a bare `Parameter`: when +# `add_parameter` is called without a `parameter_class` the returned parameter is +# bound to `self`, so spelling the default as `Parameter` (which expands to +# `Parameter[Any, InstrumentBase | None]`) would wrongly claim that the +# instrument is `InstrumentBase | None` and make the result unassignable to the +# `Parameter[SomeType, Self]` annotations that drivers use. +TParameter = TypeVar("TParameter", bound="ParameterBase", default="Parameter[Any, Any]") TSubmodule = TypeVar( "TSubmodule", bound="InstrumentModule | ChannelTuple", default="InstrumentModule" ) diff --git a/tests/test_instrument.py b/tests/test_instrument.py index be6d9ec192ae..0d2c1248c2f4 100644 --- a/tests/test_instrument.py +++ b/tests/test_instrument.py @@ -226,8 +226,10 @@ def test_attr_access(testdummy: DummyInstrument) -> None: def test_parameter_property(testdummy: DummyInstrument) -> None: # since this is added dynamically we cannot know the type statically assert_type(testdummy.dac1, Any) - # this is an assigned attribute so we know it statically - assert_type(testdummy.fixed_parameter, Parameter) + # this is an assigned attribute so we know it statically. Without an + # explicit ``parameter_class`` the data and instrument types of the + # returned parameter are unknown, hence ``Parameter[Any, Any]``. + assert_type(testdummy.fixed_parameter, Parameter[Any, Any]) assert testdummy.fixed_parameter.get() == 5 testdummy.fixed_parameter.set(10) From 7e500e4722a9d48ffa11ae88a143c2d1933f6753 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:17:24 +0200 Subject: [PATCH 05/70] Fix element typing of standalone result dicts _finalize_res_dict_standalones built intermediate lists whose element type was inferred from the branch that built them rather than from the declaration. dict is invariant in its value type, so a list of dict[str, str] is not assignable to a list of dict[str, VALUE]. Append and extend directly instead, which gives the dict literals the declared element type as context. Note that spelling this as res_list += [...] is not enough, pyright does not propagate the element type through the augmented assignment. --- .../changes/newsfragments/8441.underthehood.3 | 5 +++++ src/qcodes/dataset/data_set.py | 21 +++++++------------ 2 files changed, 12 insertions(+), 14 deletions(-) create mode 100644 docs/changes/newsfragments/8441.underthehood.3 diff --git a/docs/changes/newsfragments/8441.underthehood.3 b/docs/changes/newsfragments/8441.underthehood.3 new file mode 100644 index 000000000000..81eef08c23d8 --- /dev/null +++ b/docs/changes/newsfragments/8441.underthehood.3 @@ -0,0 +1,5 @@ +``DataSet._finalize_res_dict_standalones`` now appends to its result list +directly instead of building intermediate lists. The intermediate lists took +their element type from the branch that built them rather than from the +declaration, and ``dict`` is invariant in its value type, so the result was not +assignable back. There is no change in behaviour. diff --git a/src/qcodes/dataset/data_set.py b/src/qcodes/dataset/data_set.py index fe5339339625..2d74bdb95c1a 100644 --- a/src/qcodes/dataset/data_set.py +++ b/src/qcodes/dataset/data_set.py @@ -1562,28 +1562,21 @@ def _finalize_res_dict_standalones( for param, value in result_dict.items(): if param.type == "text": if value.shape: - new_res: list[dict[str, VALUE]] = [ - {param.name: str(val)} for val in value - ] - res_list += new_res + res_list.extend({param.name: str(val)} for val in value) else: - new_res = [{param.name: str(value)}] - res_list += new_res + res_list.append({param.name: str(value)}) elif param.type == "numeric": if value.shape: - res_list += [{param.name: number} for number in value] + res_list.extend({param.name: number} for number in value) else: - new_res = [{param.name: float(value)}] - res_list += new_res + res_list.append({param.name: float(value)}) elif param.type == "complex": if value.shape: - res_list += [{param.name: number} for number in value] + res_list.extend({param.name: number} for number in value) else: - new_res = [{param.name: complex(value)}] - res_list += new_res + res_list.append({param.name: complex(value)}) else: - new_res = [{param.name: value}] - res_list += new_res + res_list.append({param.name: value}) return res_list From f7103c6484b6f4695e1d9347fcb371ebdf9ca9cc Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:17:30 +0200 Subject: [PATCH 06/70] Do not assume a ctypes errcheck callable has a __name__ _check_error_code read __name__ off a Callable, which the type system does not guarantee. Annotating the parameter more precisely would risk breaking the assignment to c_func.errcheck, since the parameter is contravariant against ctypes own typing, so fall back to repr instead. This also keeps the log line useful if errcheck is ever handed something that is not a function. --- docs/changes/newsfragments/8441.underthehood.4 | 4 ++++ .../instrument_drivers/AlazarTech/dll_wrapper.py | 10 +++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 docs/changes/newsfragments/8441.underthehood.4 diff --git a/docs/changes/newsfragments/8441.underthehood.4 b/docs/changes/newsfragments/8441.underthehood.4 new file mode 100644 index 000000000000..7cab22c1c784 --- /dev/null +++ b/docs/changes/newsfragments/8441.underthehood.4 @@ -0,0 +1,4 @@ +The Alazar DLL wrapper no longer assumes that the callable handed to a ctypes +``errcheck`` has a ``__name__``, which a plain ``Callable`` does not guarantee. +The error message falls back to the repr of the callable instead. This only +affects the text of an error that should not occur in practice. diff --git a/src/qcodes/instrument_drivers/AlazarTech/dll_wrapper.py b/src/qcodes/instrument_drivers/AlazarTech/dll_wrapper.py index 631c0c7dc125..983380112e14 100644 --- a/src/qcodes/instrument_drivers/AlazarTech/dll_wrapper.py +++ b/src/qcodes/instrument_drivers/AlazarTech/dll_wrapper.py @@ -64,17 +64,21 @@ def _check_error_code( if len(argrepr) > 100: argrepr = argrepr[:96] + "...]" + # ``errcheck`` is always handed a ctypes foreign function, which has a + # ``__name__``, but a plain ``Callable`` is not guaranteed to. + func_name = getattr(func, "__name__", repr(func)) + logger.error( f"Alazar API returned code {return_code} from function " - f"{func.__name__} with args {argrepr}" + f"{func_name} with args {argrepr}" ) if return_code not in ERROR_CODES: raise RuntimeError( - f"unknown error {return_code} from function {func.__name__} with args: {argrepr}" + f"unknown error {return_code} from function {func_name} with args: {argrepr}" ) raise RuntimeError( - f"error {return_code}: {ERROR_CODES[ReturnCode(return_code)]} from function {func.__name__} with args: {argrepr}" + f"error {return_code}: {ERROR_CODES[ReturnCode(return_code)]} from function {func_name} with args: {argrepr}" ) return arguments From 3d6c5ec9fd9f284d8765e6ecaf22071f299155cb Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:21:27 +0200 Subject: [PATCH 07/70] Suppress ty on the colorbar _inside workaround set_colorbar_extend deliberately writes to a private matplotlib attribute, as the surrounding docstring explains, because Colorbar has no setter for extend. Extend the existing mypy suppression to ty. --- src/qcodes/plotting/matplotlib_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qcodes/plotting/matplotlib_helpers.py b/src/qcodes/plotting/matplotlib_helpers.py index ce85b2774c89..e611e8f3b2fe 100644 --- a/src/qcodes/plotting/matplotlib_helpers.py +++ b/src/qcodes/plotting/matplotlib_helpers.py @@ -49,7 +49,7 @@ def _set_colorbar_extend( "min": slice(1, None), "max": slice(0, -1), } - colorbar._inside = _slice_dict[extend] # type: ignore[attr-defined] + colorbar._inside = _slice_dict[extend] # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def apply_color_scale_limits( From 80d7f8c5a3c3fd9f11852ee85bc4a4f639fbce96 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:21:41 +0200 Subject: [PATCH 08/70] Suppress ty on the qcodes_abstractmethod marker The decorator tags the decorated function with a marker attribute that ParameterBase later reads. A Callable has no such attribute as far as the type system is concerned, so extend the existing mypy suppression to ty. --- src/qcodes/utils/abstractmethod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qcodes/utils/abstractmethod.py b/src/qcodes/utils/abstractmethod.py index d20a6adaabcd..072820a8095c 100644 --- a/src/qcodes/utils/abstractmethod.py +++ b/src/qcodes/utils/abstractmethod.py @@ -16,5 +16,5 @@ def qcodes_abstractmethod[**input, output]( instantiated and we will use this property to detect if the method is abstract and should be overwritten. """ - funcobj.__qcodes_is_abstract_method__ = True # type: ignore[attr-defined] + funcobj.__qcodes_is_abstract_method__ = True # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] return funcobj From e73068366a06a975287b4492534688d44fdc2e26 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:23:22 +0200 Subject: [PATCH 09/70] Annotate the DynaCool server socket dictionary Without an annotation ty infers the value type of the dictionary as Any | None | tuple[str, int], picking up the None from the later pop(sock, None), which then makes indexing the address tuple an error. Declare the intended type instead. --- .../QuantumDesign/DynaCoolPPMS/private/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qcodes/instrument_drivers/QuantumDesign/DynaCoolPPMS/private/server.py b/src/qcodes/instrument_drivers/QuantumDesign/DynaCoolPPMS/private/server.py index 0494c21f161b..d08f587de900 100644 --- a/src/qcodes/instrument_drivers/QuantumDesign/DynaCoolPPMS/private/server.py +++ b/src/qcodes/instrument_drivers/QuantumDesign/DynaCoolPPMS/private/server.py @@ -31,7 +31,7 @@ def run_server() -> None: # Dictionary to keep track of sockets and addresses. # Keys are sockets and values are addresses. # Add server socket to the dictionary first. - socket_dict = {server_socket: (ADDRESS, PORT)} + socket_dict: dict[socket.socket, tuple[str, int]] = {server_socket: (ADDRESS, PORT)} print(f"Server started on port {PORT}.") print("Press ESC to exit.") From 646543bfd0b2438dbef49e83a933aae9c57e4406 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 22 Oct 2026 20:51:23 +0200 Subject: [PATCH 10/70] Include numpy scalar types in the VALUE type alias add_results and the other functions that store data in the database already support numpy integer, floating point and complex scalars at runtime, since sqlite adapters are registered for them. Include them in VALUE before numpy_ints, numpy_floats and numpy_complex are annotated with the numpy scalar types, since the tests that insert those values would otherwise fail type checking. --- docs/changes/newsfragments/8441.improved.16 | 7 +++++++ src/qcodes/dataset/sqlite/query_helpers.py | 12 +++++++++++- tests/dataset/test_dataset_basic.py | 4 +--- 3 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 docs/changes/newsfragments/8441.improved.16 diff --git a/docs/changes/newsfragments/8441.improved.16 b/docs/changes/newsfragments/8441.improved.16 new file mode 100644 index 000000000000..ca74f6687b9f --- /dev/null +++ b/docs/changes/newsfragments/8441.improved.16 @@ -0,0 +1,7 @@ +The type alias ``VALUE`` in ``qcodes.dataset.sqlite.query_helpers`` now includes +the numpy integer, floating point and complex scalar types. These are already +supported at runtime by ``DataSet.add_results`` and the other functions that +store data in the database, so passing numpy scalars to them no longer produces a +type checking error. Since ``VALUE`` is also used as the return type of methods +such as ``DataSet.get_metadata``, code that annotates the returned values may +need to handle numpy scalar types too. diff --git a/src/qcodes/dataset/sqlite/query_helpers.py b/src/qcodes/dataset/sqlite/query_helpers.py index dde5b8ee331a..82b07b5458af 100644 --- a/src/qcodes/dataset/sqlite/query_helpers.py +++ b/src/qcodes/dataset/sqlite/query_helpers.py @@ -25,7 +25,17 @@ import sqlite3 # represent the type of data we can/want map to sqlite column -VALUE = str | complex | list | ndarray | bool | None +VALUE = ( + str + | complex + | list + | ndarray + | bool + | np.integer + | np.floating + | np.complexfloating + | None +) VALUES = Sequence[VALUE] diff --git a/tests/dataset/test_dataset_basic.py b/tests/dataset/test_dataset_basic.py index 1af0d9af4b2b..b446bc0fb1ac 100644 --- a/tests/dataset/test_dataset_basic.py +++ b/tests/dataset/test_dataset_basic.py @@ -640,9 +640,7 @@ def test_numpy_complex(dataset: DataSet) -> None: dataset.mark_started() results = [{"x": tp(2.0 + 3.0j)} for tp in numpy_complex] - # TODO: VALUE in qcodes.dataset.sqlite.query_helpers does not include - # numpy scalar types although add_results supports them - dataset.add_results(results) # pyright: ignore[reportArgumentType] + dataset.add_results(results) expected_result = np.array([tp(2.0 + 3.0j) for tp in numpy_complex]) result = dataset.get_parameter_data() np.testing.assert_array_equal(result["x"]["x"], expected_result) From 83a44fff2fe03d94d1ff236e7425046dba24111a Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:28:54 +0200 Subject: [PATCH 11/70] Narrow the numpy int and float type tuples numpy_ints and numpy_floats were tuples of bare type, so the element type carried no information and registering sqlite adapters for them could not be checked. Narrowing them surfaced that _adapt_float only declared float, even though it is registered for the numpy float types as well. Annotate it like _adapt_complex next to it, which already accepts its numpy counterpart. The two changes are in one commit because the adapter signature is only wrong once the tuples are narrowed. --- docs/changes/newsfragments/8441.underthehood.5 | 5 +++++ src/qcodes/dataset/sqlite/database.py | 2 +- src/qcodes/utils/types.py | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 docs/changes/newsfragments/8441.underthehood.5 diff --git a/docs/changes/newsfragments/8441.underthehood.5 b/docs/changes/newsfragments/8441.underthehood.5 new file mode 100644 index 000000000000..3317ec3768c5 --- /dev/null +++ b/docs/changes/newsfragments/8441.underthehood.5 @@ -0,0 +1,5 @@ +``numpy_ints`` and ``numpy_floats`` in ``qcodes.utils.types`` are now annotated +as tuples of ``type[np.integer]`` and ``type[np.floating]`` rather than of bare +``type``. As a consequence ``_adapt_float``, which is registered as a sqlite +adapter for the numpy float types as well as for ``float``, now declares that it +accepts ``np.floating`` too. Its behaviour is unchanged. diff --git a/src/qcodes/dataset/sqlite/database.py b/src/qcodes/dataset/sqlite/database.py index cb39c378c09d..32d4ffc204d9 100644 --- a/src/qcodes/dataset/sqlite/database.py +++ b/src/qcodes/dataset/sqlite/database.py @@ -105,7 +105,7 @@ def _convert_numeric(value: bytes) -> float | int | str: return numeric_int -def _adapt_float(fl: float) -> float | str: +def _adapt_float(fl: float | np.floating) -> float | str: # For a single value, math.isnan is 10 times faster than np.isnan # Overall, saving floats with numeric format is 2 times faster with math.isnan if math.isnan(fl): diff --git a/src/qcodes/utils/types.py b/src/qcodes/utils/types.py index 01fe50e3a2eb..dc82f1692674 100644 --- a/src/qcodes/utils/types.py +++ b/src/qcodes/utils/types.py @@ -44,7 +44,7 @@ Default integer types. The size may be platform dependent. """ -numpy_ints: tuple[type, ...] = ( +numpy_ints: tuple[type[np.integer], ...] = ( numpy_concrete_ints + numpy_c_ints + numpy_non_concrete_ints_instantiable ) """ @@ -61,7 +61,7 @@ Floating point types that matches C types. """ -numpy_floats: tuple[type, ...] = numpy_concrete_floats + numpy_c_floats +numpy_floats: tuple[type[np.floating], ...] = numpy_concrete_floats + numpy_c_floats """ All numpy float types """ From 571f2c29dd12adce4bae4318ebab9789115268bf Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:30:32 +0200 Subject: [PATCH 12/70] Suppress ty on the ParamSpec._from_dict override ParamSpec._from_dict narrows the parameter to ParamSpecDict, which carries the extra depends_on and inferred_from fields that the base ParamSpecBaseDict does not. That is a deliberate Liskov violation which already carried a mypy suppression, so extend it to ty. --- src/qcodes/dataset/descriptions/param_spec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qcodes/dataset/descriptions/param_spec.py b/src/qcodes/dataset/descriptions/param_spec.py index 0be4fff4b70f..f4431839c4cc 100644 --- a/src/qcodes/dataset/descriptions/param_spec.py +++ b/src/qcodes/dataset/descriptions/param_spec.py @@ -181,7 +181,7 @@ def base_version(self) -> _ParamSpecBase: ) @classmethod - def _from_dict(cls, ser: ParamSpecDict) -> ParamSpec: # type: ignore[override] + def _from_dict(cls, ser: ParamSpecDict) -> ParamSpec: # type: ignore[override] # ty: ignore[invalid-method-override] """ Create a ParamSpec instance of the current version from a dictionary representation of ParamSpec of some version From 56d9585baa7442d64ffd6ce170f6c844f9e0cc96 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:30:55 +0200 Subject: [PATCH 13/70] Suppress ty on the IPToVisa base class conflict IPToVisa deliberately injects VisaInstrument ahead of IPInstrument in the MRO so that an IPInstrument can be driven by the pyvisa-sim backend, as the class docstring explains. The two bases declare set_address incompatibly, which already carried a mypy suppression, so extend it to ty. --- src/qcodes/instrument/ip_to_visa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qcodes/instrument/ip_to_visa.py b/src/qcodes/instrument/ip_to_visa.py index 5f66698dd24e..9a31954636c5 100644 --- a/src/qcodes/instrument/ip_to_visa.py +++ b/src/qcodes/instrument/ip_to_visa.py @@ -24,7 +24,7 @@ # Such a driver is just a two-line class definition. -class IPToVisa(VisaInstrument, IPInstrument): # type: ignore[misc] +class IPToVisa(VisaInstrument, IPInstrument): # type: ignore[misc] # ty: ignore[invalid-method-override] """ Class to inject an VisaInstrument like behaviour in an IPInstrument that we'd like to use as a VISAInstrument with the From 6105f8fd3a5bb94e843071dc5cee9f7a828b099c Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:31:17 +0200 Subject: [PATCH 14/70] Suppress ty on the Alazar get_idn override The Alazar boards report a CPLD version as an int, so get_idn widens the value type of the returned dict. The existing TODO records that this is inconsistent with the base class, and the override already carried a mypy suppression, so extend it to ty. --- src/qcodes/instrument_drivers/AlazarTech/ATS.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qcodes/instrument_drivers/AlazarTech/ATS.py b/src/qcodes/instrument_drivers/AlazarTech/ATS.py index a1e64776c0b8..89519af5e9a7 100644 --- a/src/qcodes/instrument_drivers/AlazarTech/ATS.py +++ b/src/qcodes/instrument_drivers/AlazarTech/ATS.py @@ -153,7 +153,7 @@ def __init__( self.buffer_list: list[Buffer] = [] - def get_idn(self) -> dict[str, str | int | None]: # type: ignore[override] + def get_idn(self) -> dict[str, str | int | None]: # type: ignore[override] # ty: ignore[invalid-method-override] # TODO return type is inconsistent with the super class. We should consider # if ints and floats are allowed as values in the dict """ From d706f5a1ea70b65c9db030712ab984fc95024160 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:32:25 +0200 Subject: [PATCH 15/70] Match the parameter name of the AWG5014 __getattr__ The override named its parameter name while DelegateAttributes.__getattr__ names it key, so the two differ for a caller passing it by keyword. Python only ever calls __getattr__ positionally, so this is a real but harmless Liskov violation and is simpler to fix than to suppress. --- docs/changes/newsfragments/8441.underthehood.6 | 5 +++++ src/qcodes/instrument_drivers/tektronix/AWG5014.py | 10 +++++----- 2 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 docs/changes/newsfragments/8441.underthehood.6 diff --git a/docs/changes/newsfragments/8441.underthehood.6 b/docs/changes/newsfragments/8441.underthehood.6 new file mode 100644 index 000000000000..10becb126c81 --- /dev/null +++ b/docs/changes/newsfragments/8441.underthehood.6 @@ -0,0 +1,5 @@ +The ``__getattr__`` that provides backwards-compatible access to the old flat +parameter names on the Tektronix AWG5014 now names its parameter ``key``, +matching ``DelegateAttributes.__getattr__`` which it overrides and delegates to. +Python only ever calls ``__getattr__`` positionally, so this has no effect at +runtime. diff --git a/src/qcodes/instrument_drivers/tektronix/AWG5014.py b/src/qcodes/instrument_drivers/tektronix/AWG5014.py index 68878fe76e68..fcc57b6c76cf 100644 --- a/src/qcodes/instrument_drivers/tektronix/AWG5014.py +++ b/src/qcodes/instrument_drivers/tektronix/AWG5014.py @@ -608,7 +608,7 @@ def __init__( r"^ch(?P[1-4])_(?:(?Pm[12])_)?(?P.+)$" ) - def __getattr__(self, name: str) -> Any: + def __getattr__(self, key: str) -> Any: """ Provide backwards-compatible access to the old flat parameter names like ``ch1_amp``, ``ch1_m1_high``, etc. @@ -616,7 +616,7 @@ def __getattr__(self, name: str) -> Any: These now live on channel / marker submodules but are still reachable via the old names with a deprecation warning. """ - m = self._LEGACY_CHANNEL_RE.match(name) + m = self._LEGACY_CHANNEL_RE.match(key) if m is not None: ch_num = int(m.group("ch")) marker = m.group("marker") @@ -632,7 +632,7 @@ def __getattr__(self, name: str) -> Any: if hasattr(mrk, new_param): new_name = f"ch{ch_num}.{marker}.{new_param}" warnings.warn( - f"Accessing '{name}' is deprecated. " + f"Accessing '{key}' is deprecated. " f"Use '{new_name}' instead.", category=QCoDeSDeprecationWarning, stacklevel=2, @@ -641,12 +641,12 @@ def __getattr__(self, name: str) -> Any: elif hasattr(ch, param): new_name = f"ch{ch_num}.{param}" warnings.warn( - f"Accessing '{name}' is deprecated. Use '{new_name}' instead.", + f"Accessing '{key}' is deprecated. Use '{new_name}' instead.", category=QCoDeSDeprecationWarning, stacklevel=2, ) return getattr(ch, param) - return super().__getattr__(name) + return super().__getattr__(key) # Convenience parser def newlinestripper(self, string: str) -> str: From 20a2554dc03febee2dbf6c17b2c8ab56553412cd Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:32:51 +0200 Subject: [PATCH 16/70] Suppress ty on Parameter.increment increment only works for parameters whose data type supports addition, which the generic data type variable does not express, as the comment above it already records. Extend the existing mypy suppression to ty. --- src/qcodes/parameters/parameter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qcodes/parameters/parameter.py b/src/qcodes/parameters/parameter.py index 564da8898520..c8acb6b4831c 100644 --- a/src/qcodes/parameters/parameter.py +++ b/src/qcodes/parameters/parameter.py @@ -554,7 +554,7 @@ def increment(self, value: ParameterDataTypeVar) -> None: """ # this method only works with parameters that support addition # however we don't currently enforce that via typing - self.set(self.get() + value) # type: ignore[operator] + self.set(self.get() + value) # type: ignore[operator] # ty: ignore[unsupported-operator] def sweep( self, From c796722d4d3f4b44ee4a8515f2a0bfd75b1450b8 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:33:14 +0200 Subject: [PATCH 17/70] Suppress ty on the Lakeshore CHANNEL_CLASS default Assigning the class that matches the default of the covariant channel type variable is rejected by mypy and pyright already, and ty agrees. Extend the existing suppression and note that all three flag it. --- src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py b/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py index 3e3c6c7bcbe0..2e2441d3a23c 100644 --- a/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py +++ b/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py @@ -769,9 +769,9 @@ class LakeshoreBase(VisaInstrument, Generic[ChanType_co]): # Define this in the model-specific class in case you want to use a # different class for sensor channels # type error. It's not clear to me why assigning a value that matches the - # default of the TypeVar is an error but both mypy and pyright - # flags it here. - CHANNEL_CLASS: type[ChanType_co] = LakeshoreBaseSensorChannel # type: ignore[assignment] + # default of the TypeVar is an error but mypy, pyright and ty all + # flag it here. + CHANNEL_CLASS: type[ChanType_co] = LakeshoreBaseSensorChannel # type: ignore[assignment] # ty: ignore[invalid-assignment] # This dict has channel name in the driver as keys, and channel "name" that # is used in instrument commands as values. For example, if channel called From b0ca3b8556b6ea6e26ecc29fa0d3cede2cd82152 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:33:37 +0200 Subject: [PATCH 18/70] Suppress ty on the cache update monkeypatch The on_cache_change mixin wraps the _update_with method of the cache of the parameter it is mixed into, so that it can detect changes. Patching a method on another object is inherently dynamic and already carried a mypy suppression, so extend it to ty. --- .../extensions/parameters/parameter_mixin_on_cache_change.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qcodes/extensions/parameters/parameter_mixin_on_cache_change.py b/src/qcodes/extensions/parameters/parameter_mixin_on_cache_change.py index edd9ca9cbc88..fdeafd7c6701 100644 --- a/src/qcodes/extensions/parameters/parameter_mixin_on_cache_change.py +++ b/src/qcodes/extensions/parameters/parameter_mixin_on_cache_change.py @@ -143,7 +143,7 @@ def wrapped_cache_update( raw_value_new=raw_value_new, ) - parameter.cache._update_with = wrapped_cache_update # type: ignore[method-assign] + parameter.cache._update_with = wrapped_cache_update # type: ignore[method-assign] # ty: ignore[invalid-assignment] def _handle_on_cache_change( self, *, value_old: Any, value_new: Any, raw_value_old: Any, raw_value_new: Any From ac3454c99d49a123f718d203799bd7ed20ebc757 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:35:03 +0200 Subject: [PATCH 19/70] Suppress ty on the run overview extra columns The keys of the extra columns are supplied by the caller at runtime, so they cannot be part of the closed RunOverviewDict definition, as the comment above already records. Extend the existing mypy suppression to ty. --- src/qcodes/dataset/sqlite/db_overview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qcodes/dataset/sqlite/db_overview.py b/src/qcodes/dataset/sqlite/db_overview.py index 088bdae9ed3f..d74eeb99680f 100644 --- a/src/qcodes/dataset/sqlite/db_overview.py +++ b/src/qcodes/dataset/sqlite/db_overview.py @@ -286,7 +286,7 @@ def get_db_overview( # The keys of ``extra`` are only known at runtime (they are the # user-supplied ``extra_columns``), so they cannot be part of # the closed ``RunOverviewDict`` definition. - entry.update(extra) # type: ignore[typeddict-item] + entry.update(extra) # type: ignore[typeddict-item] # ty: ignore[invalid-argument-type] overview[run_id] = entry From 10651196485aad0ccf51b94b56753e4a4d7c1620 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:35:55 +0200 Subject: [PATCH 20/70] Type the AutoLoadableChannelList multichan_paramclass The parameter was annotated as a bare type while ChannelList, which it forwards to, declares type[MultiChannelInstrumentParameter]. The docstring already states that it must be a subclass of that, so say so in the annotation. --- src/qcodes/instrument/channel.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/qcodes/instrument/channel.py b/src/qcodes/instrument/channel.py index 13e47178125c..4616916576ea 100644 --- a/src/qcodes/instrument/channel.py +++ b/src/qcodes/instrument/channel.py @@ -1215,7 +1215,9 @@ def __init__( chan_type: type[TAUTORELOADCHANNEL], chan_list: Sequence[TAUTORELOADCHANNEL] | None = None, snapshotable: bool = True, - multichan_paramclass: type = MultiChannelInstrumentParameter, + multichan_paramclass: type[MultiChannelInstrumentParameter] = ( + MultiChannelInstrumentParameter + ), **kwargs: Any, ) -> None: super().__init__( From 2ebae247fe63f3bd15586c81d96b9efc951f52b5 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:36:25 +0200 Subject: [PATCH 21/70] Suppress ty on the ChannelList setitem narrowing Narrowing the value with isinstance does not tell either checker that it is the element type of the list, because the element type is a TypeVar bound to InstrumentModule. Extend the existing mypy suppression to ty and move the explanatory comment above the line it applies to. --- src/qcodes/instrument/channel.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/qcodes/instrument/channel.py b/src/qcodes/instrument/channel.py index 4616916576ea..7c8a2c8846b3 100644 --- a/src/qcodes/instrument/channel.py +++ b/src/qcodes/instrument/channel.py @@ -741,9 +741,10 @@ def __setitem__( # asserts added to work around https://github.com/python/mypy/issues/7858 if isinstance(index, int): assert isinstance(value, InstrumentModule) - self._channels[index] = value # type: ignore[assignment] - # mypy does not know that InstrumentModuleType is a TypeVar bound to - # InstrumentModule so complains here + # neither mypy nor ty knows that InstrumentModuleType is a TypeVar + # bound to InstrumentModule, so narrowing value with the isinstance + # above does not give them the element type of the list + self._channels[index] = value # type: ignore[assignment] # ty: ignore[invalid-assignment] else: assert not isinstance(value, InstrumentModule) self._channels[index] = value From b02b2735874fd072709139919daeaa8ab44d5503 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:37:12 +0200 Subject: [PATCH 22/70] Annotate the second Command exec mapping The two exec mappings are built in mutually exclusive branches but shared a name, and only the first carried an annotation. ty takes the inferred type of the second, whose keys are plain bool tuples, so looking up a key that may be the literal "multi" was an error. Give the second mapping its own name and the same annotation. --- src/qcodes/parameters/command.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/qcodes/parameters/command.py b/src/qcodes/parameters/command.py index d98de94adf76..9f107ba06ab7 100644 --- a/src/qcodes/parameters/command.py +++ b/src/qcodes/parameters/command.py @@ -124,7 +124,10 @@ def __init__( elif is_function(cmd, arg_count): assert cmd is not None self._cmd = cmd - exec_mapping = { + cmd_exec_mapping: dict[ + tuple[bool | Literal["multi"], bool], + Callable[..., Output | ParsedOutput], + ] = { # (parse_input, parse_output) (False, False): cmd, (False, True): self.call_cmd_parsed_out, (True, False): self.call_cmd_parsed_in, @@ -132,7 +135,7 @@ def __init__( ("multi", False): self.call_cmd_parsed_in2, ("multi", True): self.call_cmd_parsed_in2_out, } - self.exec_function = exec_mapping[(parse_input, parse_output)] + self.exec_function = cmd_exec_mapping[(parse_input, parse_output)] elif cmd is None: if no_cmd_function is not None: From ae68fd0977b9a21d0f609c533f65293548504187 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 08:09:30 +0200 Subject: [PATCH 23/70] Register the float sqlite adapter separately from numpy floats Narrowing numpy_floats to a tuple of type[np.floating] made mypy join the element type of (float, *numpy_floats) to object, which is not a valid argument to register_adapter. ty and pyright both kept the union. Register float on its own so the loop element type stays a numpy float for all three checkers. --- src/qcodes/dataset/sqlite/database.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/qcodes/dataset/sqlite/database.py b/src/qcodes/dataset/sqlite/database.py index 32d4ffc204d9..5ccb3e6c49b0 100644 --- a/src/qcodes/dataset/sqlite/database.py +++ b/src/qcodes/dataset/sqlite/database.py @@ -138,7 +138,10 @@ def _register_numpy_sqlite_adapters_and_converters() -> None: sqlite3.register_converter("numeric", _convert_numeric) - for numpy_float in (float, *numpy_floats): + # registered separately from the numpy floats below, so that the element + # type of the loop stays a numpy float rather than widening to object + sqlite3.register_adapter(float, _adapt_float) + for numpy_float in numpy_floats: sqlite3.register_adapter(numpy_float, _adapt_float) for complex_type in complex_types: From d92ce352e826d028ef1dd9fdd7c7b2545acd0f71 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 19:35:27 +0200 Subject: [PATCH 24/70] Extend the get_ramp_values suppression to ty get_ramp_values works in numbers while the value being set has the generic parameter data type, which the comment above already records and which mypy also reports. ty reports it too, so extend the existing suppression. --- src/qcodes/parameters/parameter_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qcodes/parameters/parameter_base.py b/src/qcodes/parameters/parameter_base.py index 5d6342b04aa9..6f6b348d42d9 100644 --- a/src/qcodes/parameters/parameter_base.py +++ b/src/qcodes/parameters/parameter_base.py @@ -1023,7 +1023,7 @@ def set_wrapper(value: ParameterDataTypeVar, **kwargs: Any) -> None: # a list containing only `value`. # The steps are deliberately untyped: ``get_ramp_values`` works # in terms of numbers rather than the parameter's data type. - steps: Sequence[Any] = self.get_ramp_values(value, step=self.step) # type: ignore[arg-type] + steps: Sequence[Any] = self.get_ramp_values(value, step=self.step) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] for val_step in steps: # even if the final value is valid we may be generating From dcb0e2e34dfe1ca8bb79b94912f6e2b2482de52a Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 21:08:32 +0200 Subject: [PATCH 25/70] Annotate the json exporter templates The templates are heterogeneous dict literals, so the inferred value type was a union of str and the nested dicts. Callers fill the template in by indexing into it, which meant every such assignment was an error because the str member of the union is not subscriptable. Annotate them as dict[str, Any], matching how export_data_as_json_linear and export_data_as_json_heatmap already type the state. This clears 18 of the 20 diagnostics in the subscriber json exporter notebook. --- docs/changes/newsfragments/8441.underthehood.7 | 5 +++++ src/qcodes/dataset/json_exporter.py | 8 ++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 docs/changes/newsfragments/8441.underthehood.7 diff --git a/docs/changes/newsfragments/8441.underthehood.7 b/docs/changes/newsfragments/8441.underthehood.7 new file mode 100644 index 000000000000..e519a10b0793 --- /dev/null +++ b/docs/changes/newsfragments/8441.underthehood.7 @@ -0,0 +1,5 @@ +``json_template_linear`` and ``json_template_heatmap`` in +``qcodes.dataset.json_exporter`` are now annotated as ``dict[str, Any]``. They +are templates for a JSON document, so their values are deliberately +heterogeneous, and without the annotation the inferred value type made indexing +into them an error for callers filling the template in. diff --git a/src/qcodes/dataset/json_exporter.py b/src/qcodes/dataset/json_exporter.py index dcf4ac3fef19..6c60aa79e4d1 100644 --- a/src/qcodes/dataset/json_exporter.py +++ b/src/qcodes/dataset/json_exporter.py @@ -8,13 +8,17 @@ if TYPE_CHECKING: from collections.abc import Mapping -json_template_linear = { +# These are templates for a JSON document, so the values are deliberately +# heterogeneous and consumers index arbitrarily deep into them. Annotating the +# value type as ``Any`` matches how ``export_data_as_json_*`` below already +# types the state they are copied into. +json_template_linear: dict[str, Any] = { "type": "linear", "x": {"data": [], "name": "", "full_name": "", "is_setpoint": True, "unit": ""}, "y": {"data": [], "name": "", "full_name": "", "is_setpoint": False, "unit": ""}, } -json_template_heatmap = { +json_template_heatmap: dict[str, Any] = { "type": "heatmap", "x": {"data": [], "name": "", "full_name": "", "is_setpoint": True, "unit": ""}, "y": {"data": [], "name": "", "full_name": "", "is_setpoint": True, "unit": ""}, From 655ce7bf2630bdf1f4a78509c5950eafc3b9dae6 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 21:09:48 +0200 Subject: [PATCH 26/70] Allow subscribe callbacks to take callback_kwargs subscribe declared its callback as taking exactly three arguments, which contradicts its own callback_kwargs argument: those are bound onto the callback with functools.partial, so a callback using them takes more. Any documented use of callback_kwargs was therefore a type error. Type it as Callable[..., None], which is what _Subscriber, the thing subscribe forwards to, already uses. --- docs/changes/newsfragments/8441.improved.1 | 6 ++++++ src/qcodes/dataset/data_set.py | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 docs/changes/newsfragments/8441.improved.1 diff --git a/docs/changes/newsfragments/8441.improved.1 b/docs/changes/newsfragments/8441.improved.1 new file mode 100644 index 000000000000..e0c385c8e161 --- /dev/null +++ b/docs/changes/newsfragments/8441.improved.1 @@ -0,0 +1,6 @@ +The ``callback`` argument of :meth:`.DataSet.subscribe` is now typed as +``Callable[..., None]``. The previous annotation described a callback taking +exactly three arguments, which contradicted ``callback_kwargs``: those are bound +onto the callback with ``functools.partial``, so a callback using them takes +further arguments. ``_Subscriber``, which ``subscribe`` forwards to, already +typed it this way. diff --git a/src/qcodes/dataset/data_set.py b/src/qcodes/dataset/data_set.py index 2d74bdb95c1a..ccb2e32ce2af 100644 --- a/src/qcodes/dataset/data_set.py +++ b/src/qcodes/dataset/data_set.py @@ -1234,7 +1234,11 @@ def write_data_to_text_file( def subscribe( self, - callback: Callable[[Any, int, Any | None], None], + # ``Callable[..., None]`` rather than a three argument callable because + # ``callback_kwargs`` below is bound onto the callback with + # ``functools.partial``, so it may take further keyword arguments. This + # matches how ``_Subscriber`` types the same callback. + callback: Callable[..., None], min_wait: int = 0, min_count: int = 1, state: Any | None = None, From 7c06f8d7a4756600a5fd48c090c4561c9dfb6d7d Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 28 Oct 2026 06:55:44 +0100 Subject: [PATCH 27/70] Suppress ty on the 34980A safety interlock demo The notebook sets _is_locked on a module to demonstrate the safety interlock. That attribute is defined on the 34934A driver rather than on the switch matrix submodule base class that module is typed as, so suppress the unresolved attribute. --- ...with Keysight 34980A Switch Mainframe and Modules.ipynb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/examples/driver_examples/Qcodes example with Keysight 34980A Switch Mainframe and Modules.ipynb b/docs/examples/driver_examples/Qcodes example with Keysight 34980A Switch Mainframe and Modules.ipynb index 3726074aa3ee..b1a64ba32a6a 100644 --- a/docs/examples/driver_examples/Qcodes example with Keysight 34980A Switch Mainframe and Modules.ipynb +++ b/docs/examples/driver_examples/Qcodes example with Keysight 34980A Switch Mainframe and Modules.ipynb @@ -428,9 +428,10 @@ "metadata": {}, "outputs": [], "source": [ - "switch_matrix.module[\n", - " 2\n", - "]._is_locked = True # DO NOT perform this action in real situation" + "# DO NOT perform this action in a real situation. ``_is_locked`` is defined\n", + "# on the 34934A driver rather than on the shared submodule base class that\n", + "# ``module`` is typed as, hence the suppression.\n", + "switch_matrix.module[2]._is_locked = True # ty: ignore[unresolved-attribute]" ] }, { From 49febecf4973d5652d6df9d0e7ca0c7faf0b676a Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 21:18:50 +0200 Subject: [PATCH 28/70] Correct the return type of parse_awg_file The docstring states that the returned tuple matches the call signature of make_send_and_load_awg_file, but the declared type did not, so the documented round trip of parsing a file and sending it back was a type error throughout. The waveform and marker entries were declared as lists of dicts, but _parser3 appends parsed_wfmdict["wfm"], which _parser2 types as an ndarray. The loop counts and sequencing values were declared as possibly str when the parser only ever puts ints in them. Confirmed both by reading _parser2 and by running the parsers over a synthetic waveform. --- docs/changes/newsfragments/8441.improved.3 | 6 ++++++ .../tektronix/AWGFileParser.py | 18 +++++++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 docs/changes/newsfragments/8441.improved.3 diff --git a/docs/changes/newsfragments/8441.improved.3 b/docs/changes/newsfragments/8441.improved.3 new file mode 100644 index 000000000000..0437c9e88e28 --- /dev/null +++ b/docs/changes/newsfragments/8441.improved.3 @@ -0,0 +1,6 @@ +The return type of :func:`.parse_awg_file` has been corrected. The waveform and +marker entries were declared as lists of dicts, but the parser returns the arrays +from inside those dicts, and the loop counts and sequencing values were declared +as possibly ``str`` when they are always ``int``. The type now matches the call +signature of :meth:`.TektronixAWG5014.make_send_and_load_awg_file`, which the +docstring already promised and which is how the function is meant to be used. diff --git a/src/qcodes/instrument_drivers/tektronix/AWGFileParser.py b/src/qcodes/instrument_drivers/tektronix/AWGFileParser.py index 9c7ed36b4c0e..da64e7919eb6 100644 --- a/src/qcodes/instrument_drivers/tektronix/AWGFileParser.py +++ b/src/qcodes/instrument_drivers/tektronix/AWGFileParser.py @@ -295,14 +295,18 @@ "WAIT_VALUE": {1: "First", 2: "Last"}, } +# The tuple returned by ``_parser3``, and therefore by ``parse_awg_file``. It +# deliberately matches the call signature of +# ``TektronixAWG5014.make_send_and_load_awg_file``, so that the output of the +# parser can be passed straight back in. _parser3_output = tuple[ - list[list[dict[Any, Any]]], - list[list[dict[Any, Any]]], - list[list[dict[Any, Any]]], - list[str | int], - list[str | int], - list[str | int], - list[str | int], + list[list[npt.NDArray]], + list[list[npt.NDArray]], + list[list[npt.NDArray]], + list[int], + list[int], + list[int], + list[int], list[int], ] From 2379ef06459c4568aaa7350432638d25f69b83c2 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 21:20:13 +0200 Subject: [PATCH 29/70] Do not assume every parameter has a label in the AWG5014C notebook instrument.parameters is a dict of ParameterBase, which does not carry a label. Parameter and ArrayParameter do, but MultiParameter has labels instead, so the listing would raise for an instrument holding one. Read it with getattr and a default, and say why in the notebook. --- .../Qcodes example with Tektronix AWG5014C.ipynb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/examples/driver_examples/Qcodes example with Tektronix AWG5014C.ipynb b/docs/examples/driver_examples/Qcodes example with Tektronix AWG5014C.ipynb index abe47966ccc7..494e9a4697a2 100644 --- a/docs/examples/driver_examples/Qcodes example with Tektronix AWG5014C.ipynb +++ b/docs/examples/driver_examples/Qcodes example with Tektronix AWG5014C.ipynb @@ -100,19 +100,23 @@ "metadata": {}, "outputs": [], "source": [ + "# ``instrument.parameters`` is a dict of ``ParameterBase``, and not every\n", + "# parameter type carries a ``label``: ``MultiParameter`` has ``labels`` instead.\n", + "# Fall back to an empty string so this works for any parameter.\n", + "\n", "# Top-level parameters\n", "for name in sorted(awg1.parameters):\n", - " print(name, \": \", awg1.parameters[name].label)\n", + " print(name, \": \", getattr(awg1.parameters[name], \"label\", \"\"))\n", "\n", "# Channel parameters (e.g. ch1)\n", "print(\"\\nChannel 1 parameters:\")\n", "for name in sorted(awg1.ch1.parameters):\n", - " print(f\" ch1.{name}: \", awg1.ch1.parameters[name].label)\n", + " print(f\" ch1.{name}: \", getattr(awg1.ch1.parameters[name], \"label\", \"\"))\n", "\n", "# Marker parameters (e.g. ch1.m1)\n", "print(\"\\nChannel 1 Marker 1 parameters:\")\n", "for name in sorted(awg1.ch1.m1.parameters):\n", - " print(f\" ch1.m1.{name}: \", awg1.ch1.m1.parameters[name].label)" + " print(f\" ch1.m1.{name}: \", getattr(awg1.ch1.m1.parameters[name], \"label\", \"\"))" ] }, { From 105ba36ebb269ec194195b33d474fa285808cf73 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 07:45:00 +0200 Subject: [PATCH 30/70] Require qcodes_loop 0.2.3 The legacy dataset examples in docs import qcodes_loop, and 0.2.3 is the first release whose annotations let those notebooks type check. --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2158bbb45d47..b42da0b27f3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ Changelog = "https://microsoft.github.io/Qcodes/changes/index.html" zurichinstruments = ["zhinst-qcodes>=0.3"] minicircuits_usb_spdt = ["pythonnet>3.0.4"] minicircuits_rudat = ["pywinusb>=0.4.2"] -loop = ["qcodes_loop>=0.1.2"] +loop = ["qcodes_loop>=0.2.3"] test = [ "coverage[toml]>=7.10.5", "hypothesis>=6.130.0", @@ -86,7 +86,7 @@ test = [ "types-tabulate>=0.1.0", "types-tqdm>=4.64.6", "types_pywin32>=305.0.0.7", - "qcodes_loop>=0.1.1", + "qcodes_loop>=0.2.3", "zhinst.qcodes>=0.5; python_version < '3.15'", # typecheck zhinst driver alias. zhinst-core has no Python 3.15 wheels yet "libcst>=1.2.0", # refactor tests ] From cb433021fd7ebe3a4a6e65c0f1785bb63e61ea04 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:00:21 +0200 Subject: [PATCH 31/70] Check the example notebooks with ty ty understands Jupyter notebooks, which mypy and pyright do not, so adding docs to the checked paths gives coverage of the examples that we have no other way to get. Also ignore unresolved imports in the plottr notebook, since plottr is a separate package that the notebook demonstrates integrating with rather than a dependency of qcodes. Note that this leaves ty reporting on the notebooks until the remaining findings are worked through. --- pyproject.toml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b42da0b27f3a..2a2dec1898b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,6 +89,7 @@ test = [ "qcodes_loop>=0.2.3", "zhinst.qcodes>=0.5; python_version < '3.15'", # typecheck zhinst driver alias. zhinst-core has no Python 3.15 wheels yet "libcst>=1.2.0", # refactor tests + "intersphinx_registry>=0.2603.16" # typecheck docs/conf.py ] docs = [ "autodocsumm>=0.2.9", @@ -479,8 +480,9 @@ build_py = "versioningit.cmdclass.build_py" python-platform = "all" [tool.ty.src] -# mirrors the include and ignore settings of pyright above -include = ["src", "tests"] +# unlike pyright above, ty also understands Jupyter notebooks, so the example +# notebooks in docs are checked too. That is coverage we get from ty alone. +include = ["src", "tests", "docs"] exclude = [ "src/qcodes/instrument_drivers/Harvard/Decadac.py", ] @@ -504,6 +506,15 @@ include = ["src/qcodes/instrument_drivers/Minicircuits/_minicircuits_usb_spdt.py [tool.ty.overrides.rules] unresolved-attribute = "ignore" +# plottr is a separate package that this notebook demonstrates integrating with, +# it is not a dependency of qcodes +[[tool.ty.overrides]] +include = [ + "docs/examples/plotting/How-to-use-Plottr-with-QCoDeS-for-live-plotting.ipynb", +] +[tool.ty.overrides.rules] +unresolved-import = "ignore" + [tool.towncrier] package = "qcodes" name = "QCoDeS" From 46d83d6a63504e6fcf7a05b3862f8c2b08c2ee60 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:21:09 +0200 Subject: [PATCH 32/70] Use the enum keys for the B1500 module dicts by_kind and by_channel are keyed by ModuleKind and ChNr. Those are a StrEnum and an IntEnum, so a plain string or int is the same key at runtime, but the dicts are typed as taking the enums. Use constants.ModuleKind.SMU for the by_kind lookup, which is what the markdown just above it points at. The by_channel cell deliberately shows both the enum and the plain int and asserts they select the same module, so keep that and record why the second form is not typed. --- ...es example with Keysight B1500 Parameter Analyzer.ipynb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/examples/driver_examples/Qcodes example with Keysight B1500 Parameter Analyzer.ipynb b/docs/examples/driver_examples/Qcodes example with Keysight B1500 Parameter Analyzer.ipynb index fffbf3d12229..90da3e16552f 100644 --- a/docs/examples/driver_examples/Qcodes example with Keysight B1500 Parameter Analyzer.ipynb +++ b/docs/examples/driver_examples/Qcodes example with Keysight B1500 Parameter Analyzer.ipynb @@ -288,7 +288,7 @@ "metadata": {}, "outputs": [], "source": [ - "b1500.by_kind[\"SMU\"]" + "b1500.by_kind[constants.ModuleKind.SMU]" ] }, { @@ -331,8 +331,9 @@ "# Selecting a module by channel number using the Enum\n", "m1 = b1500.by_channel[constants.ChNr.SLOT_01_CH1]\n", "\n", - "# Without enum\n", - "m2 = b1500.by_channel[1]\n", + "# Without enum. ChNr is an IntEnum, so a plain int is the same key at\n", + "# runtime, but the dict is typed as taking ChNr.\n", + "m2 = b1500.by_channel[1] # ty: ignore[invalid-argument-type]\n", "\n", "# And we assert that we selected the same module:\n", "assert m1 is m2" From 5fa2cf2044ebcbaa195d36e6dd06f3bc286ba6d6 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:21:38 +0200 Subject: [PATCH 33/70] Enable the channels before B1500 phase compensation The cell called run_iv_staircase_sweep.measurement_status(), which does not exist: measurement_status is a property of the SMU spot measurement parameters, while IVSweepMeasurement only gets status_summary from StatusMixin. The cell therefore raised AttributeError. It also did not do what the text around it says. The markdown before it asks for all channel outputs to be enabled before performing phase compensation, and the markdown after it continues with the second prerequisite, so call enable_channels instead. The old line looks copied from the status_summary cell earlier in the notebook. --- docs/changes/newsfragments/8441.improved.4 | 6 ++++++ ...des example with Keysight B1500 Parameter Analyzer.ipynb | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 docs/changes/newsfragments/8441.improved.4 diff --git a/docs/changes/newsfragments/8441.improved.4 b/docs/changes/newsfragments/8441.improved.4 new file mode 100644 index 000000000000..b124c324651d --- /dev/null +++ b/docs/changes/newsfragments/8441.improved.4 @@ -0,0 +1,6 @@ +The Keysight B1500 example notebook called +``b1500.run_iv_staircase_sweep.measurement_status()`` in the phase compensation +section. ``IVSweepMeasurement`` has no such method, so the cell raised +``AttributeError``. The surrounding text asks for all channel outputs to be +enabled before performing phase compensation, so the cell now calls +``b1500.enable_channels()``. diff --git a/docs/examples/driver_examples/Qcodes example with Keysight B1500 Parameter Analyzer.ipynb b/docs/examples/driver_examples/Qcodes example with Keysight B1500 Parameter Analyzer.ipynb index 90da3e16552f..e5ee5a34304d 100644 --- a/docs/examples/driver_examples/Qcodes example with Keysight B1500 Parameter Analyzer.ipynb +++ b/docs/examples/driver_examples/Qcodes example with Keysight B1500 Parameter Analyzer.ipynb @@ -1119,7 +1119,8 @@ "metadata": {}, "outputs": [], "source": [ - "b1500.run_iv_staircase_sweep.measurement_status()" + "# enable all channel outputs\n", + "b1500.enable_channels()" ] }, { From 149b8151ab12a2f8774be55cbd26b0700614dfd9 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:25:30 +0200 Subject: [PATCH 34/70] Declare the dynamic attributes of the E4980A measurement pair The class exposes its two measured values as attributes named after the names of the measurement function, so capacitance exists for CPD and inductance for LPD. The class docstring documents this, but no checker can know the names, so the documented usage was an error everywhere it appeared. Declare a __getattr__ under TYPE_CHECKING. It is not defined at runtime, so accessing an attribute the current measurement function does not provide still raises the usual AttributeError, which the notebook prints in a cell demonstrating exactly that. --- docs/changes/newsfragments/8441.improved.5 | 7 +++++++ .../instrument_drivers/Keysight/keysight_e4980a.py | 10 ++++++++++ 2 files changed, 17 insertions(+) create mode 100644 docs/changes/newsfragments/8441.improved.5 diff --git a/docs/changes/newsfragments/8441.improved.5 b/docs/changes/newsfragments/8441.improved.5 new file mode 100644 index 000000000000..3727910a2a5c --- /dev/null +++ b/docs/changes/newsfragments/8441.improved.5 @@ -0,0 +1,7 @@ +``KeysightE4980AMeasurementPair`` now declares a ``__getattr__`` for type +checkers. The two measured values are exposed as attributes named after the +``names`` of the measurement function, for example ``capacitance`` for ``CPD`` +and ``inductance`` for ``LPD``, so which attributes exist is only known at +runtime. The declaration lets this documented usage be written in typed code. It +is not defined at runtime, so accessing an attribute that the current +measurement function does not provide still raises the usual ``AttributeError``. diff --git a/src/qcodes/instrument_drivers/Keysight/keysight_e4980a.py b/src/qcodes/instrument_drivers/Keysight/keysight_e4980a.py index 19793ca568a6..73f03601df8e 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysight_e4980a.py +++ b/src/qcodes/instrument_drivers/Keysight/keysight_e4980a.py @@ -50,6 +50,16 @@ class KeysightE4980AMeasurementPair(MultiParameter): value: tuple[float, float] = (0.0, 0.0) + if TYPE_CHECKING: + # The two measured values are exposed as attributes named after the + # ``names`` of the measurement function, so which attributes exist is + # only known at runtime. Declaring this for type checkers lets the + # documented usage, such as ``measurement.capacitance``, be written in + # typed code. It is not defined at runtime, so accessing an attribute + # that the current measurement function does not provide still raises + # the usual ``AttributeError``. + def __getattr__(self, name: str) -> float: ... + def __init__( self, name: str, names: "Sequence[str]", units: "Sequence[str]", **kwargs: Any ): From fbc5048cb192d8220b19a082b027a158a64efd21 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:33:51 +0200 Subject: [PATCH 35/70] Pack the SR86x example waveforms in lists makeSEQXFile documents its wfms argument as the waveform arrays packed in lists, per channel and then per element. The notebook wrapped them in two further numpy arrays instead, which is not a Sequence of Sequences. Use lists, which is also clearer since the outer two levels are channel and element containers rather than numeric data. Verified that the method sees the same arrays either way, so the generated file is unchanged. --- docs/changes/newsfragments/8441.improved.6 | 4 ++++ ...e with Stanford SR86x with buffered readout.ipynb | 12 ++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 docs/changes/newsfragments/8441.improved.6 diff --git a/docs/changes/newsfragments/8441.improved.6 b/docs/changes/newsfragments/8441.improved.6 new file mode 100644 index 000000000000..786b261bb836 --- /dev/null +++ b/docs/changes/newsfragments/8441.improved.6 @@ -0,0 +1,4 @@ +The Stanford SR86x buffered readout example notebook now packs the waveforms for +:meth:`.TektronixAWG70000Base.makeSEQXFile` in lists rather than wrapping them in +further numpy arrays, which is the shape the method documents. The two forms +behave the same at runtime. diff --git a/docs/examples/driver_examples/Qcodes example with Stanford SR86x with buffered readout.ipynb b/docs/examples/driver_examples/Qcodes example with Stanford SR86x with buffered readout.ipynb index 4adf9855f8f2..1f8aae138e10 100644 --- a/docs/examples/driver_examples/Qcodes example with Stanford SR86x with buffered readout.ipynb +++ b/docs/examples/driver_examples/Qcodes example with Stanford SR86x with buffered readout.ipynb @@ -683,8 +683,10 @@ "# (3000 samples for 3000 S/s sample rate)\n", "waveform_ch1[1, :-1500] = 1 # falling from 1 to 0 (a.u.),\n", "# at 0.5s after the start of the waveform\n", - "elements = numpy.array([waveform_ch1]) # we only have one element in the sequence\n", - "waveforms = numpy.array([elements]) # we will use only 1 channel\n", + "# makeSEQXFile takes the waveform arrays packed in lists, per channel and\n", + "# then per element, rather than in a further numpy array\n", + "elements = [waveform_ch1] # we only have one element in the sequence\n", + "waveforms = [elements] # we will use only 1 channel\n", "\n", "# Create a sequence file from the \"waveform\" array\n", "seq_name = \"single_trigger_marker_1\"\n", @@ -929,8 +931,10 @@ " n_trigger_pulses,\n", ") # falling from 1 to 0 (a.u.) every 0.01s after the start of the waveform\n", "\n", - "elements = numpy.array([waveform_ch1]) # we only have one element in the sequence\n", - "waveforms = numpy.array([elements]) # we will use only 1 channel\n", + "# makeSEQXFile takes the waveform arrays packed in lists, per channel and\n", + "# then per element, rather than in a further numpy array\n", + "elements = [waveform_ch1] # we only have one element in the sequence\n", + "waveforms = [elements] # we will use only 1 channel\n", "\n", "# Create a sequence file from the \"waveform\" array\n", "seq_name = \"single_trigger_marker_1\"\n", From a360b377c524ef6d792514247d4cadd54226afcf Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:38:11 +0200 Subject: [PATCH 36/70] Accept any iterable of paths in the B220X switch matrix connect_paths, disconnect_paths and to_channel_list only iterate the paths once and never index them, so requiring a Sequence was stricter than the implementation. That made the example notebook, which passes a set of paths, a type error even though it works. Take an Iterable instead. Checked that a list, tuple, set and generator all produce a valid channel list. The order of the resulting list follows the iteration order of the argument, which does not matter for opening or closing a group of paths. --- docs/changes/newsfragments/8441.improved.7 | 6 ++++++ src/qcodes/instrument_drivers/Keysight/keysight_b220x.py | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 docs/changes/newsfragments/8441.improved.7 diff --git a/docs/changes/newsfragments/8441.improved.7 b/docs/changes/newsfragments/8441.improved.7 new file mode 100644 index 000000000000..ffc16e01e346 --- /dev/null +++ b/docs/changes/newsfragments/8441.improved.7 @@ -0,0 +1,6 @@ +``connect_paths``, ``disconnect_paths`` and ``to_channel_list`` on the Keysight +B220X switch matrix drivers now accept any iterable of paths rather than only a +``Sequence``. They iterate the paths once and do not index them, so passing a +set, as the example notebook does, is fine. Note that the order in which the +paths appear in the channel list then follows the iteration order of the +argument. diff --git a/src/qcodes/instrument_drivers/Keysight/keysight_b220x.py b/src/qcodes/instrument_drivers/Keysight/keysight_b220x.py index a235bafd5c9c..9e0e2ed33b17 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysight_b220x.py +++ b/src/qcodes/instrument_drivers/Keysight/keysight_b220x.py @@ -7,7 +7,7 @@ from qcodes.validators import Enum, Ints, Lists, MultiType if TYPE_CHECKING: - from collections.abc import Callable, Sequence + from collections.abc import Callable, Iterable from typing import Concatenate, Unpack from qcodes.parameters import Parameter @@ -251,12 +251,12 @@ def connect(self, input_ch: int, output_ch: int) -> None: self.write(f":CLOS (@{self._card:01d}{input_ch:02d}{output_ch:02d})") @post_execution_status_poll - def connect_paths(self, paths: "Sequence[tuple[int, int]]") -> None: + def connect_paths(self, paths: "Iterable[tuple[int, int]]") -> None: channel_list_str = self.to_channel_list(paths) self.write(f":CLOS {channel_list_str}") @post_execution_status_poll - def disconnect_paths(self, paths: "Sequence[tuple[int, int]]") -> None: + def disconnect_paths(self, paths: "Iterable[tuple[int, int]]") -> None: channel_list_str = self.to_channel_list(paths) self.write(f":OPEN {channel_list_str}") @@ -424,7 +424,7 @@ def parse_channel_list(channel_list: str) -> set[tuple[int, int]]: for match in re.finditer(pattern, channel_list) } - def to_channel_list(self, paths: "Sequence[tuple[int, int]]") -> str: + def to_channel_list(self, paths: "Iterable[tuple[int, int]]") -> str: chan = [f"{self._card:01d}{i:02d}{o:02d}" for i, o in paths] channel_list = f"(@{','.join(chan)})" return channel_list From 509c4d497fbe85713fbc99f5302045718958bbe4 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:40:55 +0200 Subject: [PATCH 37/70] Accept any collection of paths in the 34980A switch matrix The path arguments were typed as list, which rejects even a tuple. Take a Collection instead, so a set works here as it now does on the B220X. Collection rather than Iterable because these methods walk the paths twice, once to validate each one and once to build the channel list, so a one shot iterator would be exhausted before the list was built. The 34934A override of to_channel_list is widened with the base, since an override may not accept less than what it overrides. --- docs/changes/newsfragments/8441.improved.8 | 6 ++++++ .../instrument_drivers/Keysight/keysight_34934a.py | 4 ++-- .../Keysight/keysight_34980a_submodules.py | 11 ++++++----- 3 files changed, 14 insertions(+), 7 deletions(-) create mode 100644 docs/changes/newsfragments/8441.improved.8 diff --git a/docs/changes/newsfragments/8441.improved.8 b/docs/changes/newsfragments/8441.improved.8 new file mode 100644 index 000000000000..60fc859b5b8c --- /dev/null +++ b/docs/changes/newsfragments/8441.improved.8 @@ -0,0 +1,6 @@ +The path arguments of ``connect_paths``, ``disconnect_paths``, ``are_closed``, +``are_open`` and ``to_channel_list`` on the Keysight 34980A switch matrix +submodules are now typed as a ``Collection`` rather than a ``list``, so a set or +a tuple of paths is accepted as well. A ``Collection`` rather than an +``Iterable`` because these methods walk the paths twice, once to validate them +and once to build the channel list, which a one shot iterator would not survive. diff --git a/src/qcodes/instrument_drivers/Keysight/keysight_34934a.py b/src/qcodes/instrument_drivers/Keysight/keysight_34934a.py index bd21ae442eb6..33a3c1f4de70 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysight_34934a.py +++ b/src/qcodes/instrument_drivers/Keysight/keysight_34934a.py @@ -6,7 +6,7 @@ from .keysight_34980a_submodules import Keysight34980ASwitchMatrixSubModule if TYPE_CHECKING: - from collections.abc import Callable + from collections.abc import Callable, Collection from typing import Unpack from qcodes.instrument import ( @@ -105,7 +105,7 @@ def _set_relay_protection_mode(self, mode: str) -> None: self.write(f"SYSTem:MODule:ROW:PROTection {self.slot}, {mode}") def to_channel_list( - self, paths: list[tuple[int, int]], wiring_config: str | None = "" + self, paths: "Collection[tuple[int, int]]", wiring_config: str | None = "" ) -> str: """ Convert the (row, column) pair to a 4-digit channel number 'sxxx', where diff --git a/src/qcodes/instrument_drivers/Keysight/keysight_34980a_submodules.py b/src/qcodes/instrument_drivers/Keysight/keysight_34980a_submodules.py index 67c44060a613..ae9ad9b0d37a 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysight_34980a_submodules.py +++ b/src/qcodes/instrument_drivers/Keysight/keysight_34980a_submodules.py @@ -3,6 +3,7 @@ from qcodes.instrument import InstrumentBaseKWArgs, InstrumentChannel if TYPE_CHECKING: + from collections.abc import Collection from typing import Unpack from .keysight_34980a import Keysight34980A @@ -43,7 +44,7 @@ def validate_value(self, row: int, column: int) -> None: raise NotImplementedError("Please subclass this") def to_channel_list( - self, paths: list[tuple[int, int]], wiring_config: str | None = None + self, paths: "Collection[tuple[int, int]]", wiring_config: str | None = None ) -> str: """ Convert the (row, column) pair to a 4-digit channel number 'sxxx', where @@ -125,7 +126,7 @@ def disconnect(self, row: int, column: int) -> None: channel = self.to_channel_list([(row, column)]) self.write(f"ROUT:OPEN {channel}") - def connect_paths(self, paths: list[tuple[int, int]]) -> None: + def connect_paths(self, paths: "Collection[tuple[int, int]]") -> None: """ To connect/close the specified channels. @@ -138,7 +139,7 @@ def connect_paths(self, paths: list[tuple[int, int]]) -> None: channel_list_str = self.to_channel_list(paths) self.write(f"ROUTe:CLOSe {channel_list_str}") - def disconnect_paths(self, paths: list[tuple[int, int]]) -> None: + def disconnect_paths(self, paths: "Collection[tuple[int, int]]") -> None: """ To disconnect/open the specified channels. @@ -151,7 +152,7 @@ def disconnect_paths(self, paths: list[tuple[int, int]]) -> None: channel_list_str = self.to_channel_list(paths) self.write(f"ROUTe:OPEN {channel_list_str}") - def are_closed(self, paths: list[tuple[int, int]]) -> list[bool]: + def are_closed(self, paths: "Collection[tuple[int, int]]") -> list[bool]: """ To check if a list of channels is closed/connected @@ -170,7 +171,7 @@ def are_closed(self, paths: list[tuple[int, int]]) -> list[bool]: messages = self.ask(f"ROUTe:CLOSe? {channel_list_str}") return [bool(int(message)) for message in messages.split(",")] - def are_open(self, paths: list[tuple[int, int]]) -> list[bool]: + def are_open(self, paths: "Collection[tuple[int, int]]") -> list[bool]: """ To check if a list of channels is open/disconnected From 58ca548ecf9b20e0e7fb67a125ef89b5c2a372fa Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:57:53 +0200 Subject: [PATCH 38/70] Do not take the length of Line2D.get_ydata in the Lakeshore examples get_ydata is typed as returning ArrayLike, which includes Buffer and so is not necessarily sized, making len() on it a type error. Keep the appended array in a local and use that for both the y data and the length of the x axis. This also avoids reading the data back out of the line on every iteration, and gives the same lengths, which was checked against matplotlib. The same helper appears in the Lakeshore 325 notebook, so both are updated together. --- docs/changes/newsfragments/8441.improved.9 | 5 +++++ .../driver_examples/Qcodes example with Lakeshore 325.ipynb | 5 +++-- ...mple with Lakeshore 336 or 372 - Bluefors T control.ipynb | 5 +++-- 3 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 docs/changes/newsfragments/8441.improved.9 diff --git a/docs/changes/newsfragments/8441.improved.9 b/docs/changes/newsfragments/8441.improved.9 new file mode 100644 index 000000000000..7d3920c0da1d --- /dev/null +++ b/docs/changes/newsfragments/8441.improved.9 @@ -0,0 +1,5 @@ +The live temperature plot helper in the two Lakeshore example notebooks keeps +the appended y data in a local variable instead of reading it back with +``Line2D.get_ydata``. The return of ``get_ydata`` is typed as ``ArrayLike``, +which is not necessarily sized, so taking its length was a type error. This also +avoids reading the data back from the line on every iteration. diff --git a/docs/examples/driver_examples/Qcodes example with Lakeshore 325.ipynb b/docs/examples/driver_examples/Qcodes example with Lakeshore 325.ipynb index 44a36f8baed8..042ed8d79c90 100644 --- a/docs/examples/driver_examples/Qcodes example with Lakeshore 325.ipynb +++ b/docs/examples/driver_examples/Qcodes example with Lakeshore 325.ipynb @@ -517,8 +517,9 @@ " text.value = f\"T = {channel_to_read.temperature()}\"\n", "\n", " # Add new point to the data that is being plotted\n", - " line.set_ydata(numpy.append(line.get_ydata(), channel_to_read.temperature()))\n", - " line.set_xdata(numpy.arange(0, len(line.get_ydata()), 1) * read_period)\n", + " ydata = numpy.append(line.get_ydata(), channel_to_read.temperature())\n", + " line.set_ydata(ydata)\n", + " line.set_xdata(numpy.arange(0, len(ydata), 1) * read_period)\n", "\n", " ax.relim() # Recalculate limits\n", " ax.autoscale_view(True, True, True) # Autoscale\n", diff --git a/docs/examples/driver_examples/Qcodes example with Lakeshore 336 or 372 - Bluefors T control.ipynb b/docs/examples/driver_examples/Qcodes example with Lakeshore 336 or 372 - Bluefors T control.ipynb index 8837ebb9e9fe..b8c566172e2a 100644 --- a/docs/examples/driver_examples/Qcodes example with Lakeshore 336 or 372 - Bluefors T control.ipynb +++ b/docs/examples/driver_examples/Qcodes example with Lakeshore 336 or 372 - Bluefors T control.ipynb @@ -508,8 +508,9 @@ " text.value = f\"T = {channel_to_read.temperature()}\"\n", "\n", " # Add new point to the data that is being plotted\n", - " line.set_ydata(numpy.append(line.get_ydata(), channel_to_read.temperature()))\n", - " line.set_xdata(numpy.arange(0, len(line.get_ydata()), 1) * read_period)\n", + " ydata = numpy.append(line.get_ydata(), channel_to_read.temperature())\n", + " line.set_ydata(ydata)\n", + " line.set_xdata(numpy.arange(0, len(ydata), 1) * read_period)\n", "\n", " ax.relim() # Recalculate limits\n", " ax.autoscale_view(True, True, True) # Autoscale\n", From 32e5367570181e61fc2900140f5a5ae1eaafb514 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 08:02:26 +0200 Subject: [PATCH 39/70] Handle the optional values in the offline plotting tutorial The colorbar returned for a 1D plot is None, so the entry taken from the returned list has to be checked before its label is set. Doing that with an assert also documents that the entries are optional. Saving used Axes.figure, which matplotlib types as Figure or SubFigure, and a SubFigure has no savefig. Ask for the root figure instead. --- docs/changes/newsfragments/8441.improved.10 | 3 +++ .../DataSet/Offline Plotting Tutorial.ipynb | 18 ++++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 docs/changes/newsfragments/8441.improved.10 diff --git a/docs/changes/newsfragments/8441.improved.10 b/docs/changes/newsfragments/8441.improved.10 new file mode 100644 index 000000000000..b2f6f471d3d8 --- /dev/null +++ b/docs/changes/newsfragments/8441.improved.10 @@ -0,0 +1,3 @@ +The offline plotting tutorial now checks the optional values it gets back from +:func:`.plot_dataset` before using them, and asks for the root figure when +saving. ``Axes.figure`` may be a ``SubFigure``, which has no ``savefig``. diff --git a/docs/examples/DataSet/Offline Plotting Tutorial.ipynb b/docs/examples/DataSet/Offline Plotting Tutorial.ipynb index dfddf0e41b06..164fafe2ed99 100644 --- a/docs/examples/DataSet/Offline Plotting Tutorial.ipynb +++ b/docs/examples/DataSet/Offline Plotting Tutorial.ipynb @@ -477,6 +477,8 @@ "outputs": [], "source": [ "colorbar = colorbars[0]\n", + "# 2D plots have a colorbar, 1D plots do not, so the entries are optional\n", + "assert colorbar is not None\n", "colorbar.set_label(\"Correct science label\")" ] }, @@ -939,9 +941,11 @@ "source": [ "%%time\n", "axeslist, _ = plot_dataset(dataset)\n", - "axeslist[0].figure.savefig(\n", - " Path.cwd().parent / \"example_output\" / f\"test_plot_dataset_{dataid}.pdf\"\n", - ")" + "# Axes.figure may be a SubFigure, which cannot be saved, so ask for the\n", + "# root figure\n", + "figure = axeslist[0].get_figure(root=True)\n", + "assert figure is not None\n", + "figure.savefig(Path.cwd().parent / \"example_output\" / f\"test_plot_dataset_{dataid}.pdf\")" ] }, { @@ -971,9 +975,11 @@ "source": [ "%%time\n", "axeslist, _ = plot_dataset(dataset, rasterized=False)\n", - "axeslist[0].figure.savefig(\n", - " Path.cwd().parent / \"example_output\" / f\"test_plot_dataset_{dataid}.pdf\"\n", - ")" + "# Axes.figure may be a SubFigure, which cannot be saved, so ask for the\n", + "# root figure\n", + "figure = axeslist[0].get_figure(root=True)\n", + "assert figure is not None\n", + "figure.savefig(Path.cwd().parent / \"example_output\" / f\"test_plot_dataset_{dataid}.pdf\")" ] } ], From 9474d3955345d8f33511541a03d05d9ef59d5bc6 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 08:07:51 +0200 Subject: [PATCH 40/70] Put snapshot_raw on the dataset protocol snapshot_raw is documented as the way to get the snapshot of a run as a JSON string, and the snapshot notebooks use it, but it was declared only on DataSet. DataSetInMem carried the same data under the private _snapshot_raw, and the protocol declared only that, so reading it from the dataset a measurement hands back did not type check. Declare it on the protocol and add the public property to DataSetInMem, mirroring DataSet. This also removes the suppression that test_snapshot.py needed for exactly this, along with its comment saying the property is not part of the protocol. --- docs/changes/newsfragments/8441.improved.11 | 5 +++++ src/qcodes/dataset/data_set_in_memory.py | 5 +++++ src/qcodes/dataset/data_set_protocol.py | 3 +++ tests/dataset/test_dataset_export.py | 7 ++----- tests/dataset/test_snapshot.py | 4 +--- 5 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 docs/changes/newsfragments/8441.improved.11 diff --git a/docs/changes/newsfragments/8441.improved.11 b/docs/changes/newsfragments/8441.improved.11 new file mode 100644 index 000000000000..080e53462aae --- /dev/null +++ b/docs/changes/newsfragments/8441.improved.11 @@ -0,0 +1,5 @@ +``snapshot_raw`` is now part of :class:`.DataSetProtocol` and is available on +:class:`.DataSetInMem` as well as on :class:`.DataSet`. It is documented as the +way to get the snapshot of a run as a JSON string, and is used as such in the +example notebooks, but it was only declared on one of the two dataset classes, +so reading it from a dataset returned by a measurement did not type check. diff --git a/src/qcodes/dataset/data_set_in_memory.py b/src/qcodes/dataset/data_set_in_memory.py index 241b789ba3bb..5fe228c189f2 100644 --- a/src/qcodes/dataset/data_set_in_memory.py +++ b/src/qcodes/dataset/data_set_in_memory.py @@ -595,6 +595,11 @@ def _snapshot_raw(self) -> str | None: """Snapshot of the run as a JSON-formatted string (or None).""" return self._snapshot_raw_data + @property + def snapshot_raw(self) -> str | None: + """Snapshot of the run as a JSON-formatted string (or None).""" + return self._snapshot_raw + def add_metadata(self, tag: str, metadata: Any) -> None: """ Adds metadata to the :class:`.DataSet`. diff --git a/src/qcodes/dataset/data_set_protocol.py b/src/qcodes/dataset/data_set_protocol.py index 13d1ced21f91..5628aab7c727 100644 --- a/src/qcodes/dataset/data_set_protocol.py +++ b/src/qcodes/dataset/data_set_protocol.py @@ -168,6 +168,9 @@ def add_snapshot(self, snapshot: str, overwrite: bool = False) -> None: ... @property def _snapshot_raw(self) -> str | None: ... + @property + def snapshot_raw(self) -> str | None: ... + def add_metadata(self, tag: str, metadata: Any) -> None: ... @property diff --git a/tests/dataset/test_dataset_export.py b/tests/dataset/test_dataset_export.py index 027be867622e..aad6be991ab1 100644 --- a/tests/dataset/test_dataset_export.py +++ b/tests/dataset/test_dataset_export.py @@ -1506,11 +1506,8 @@ def _assert_xarray_metadata_is_as_expected( assert xarray_ds.ds_name == qc_dataset.name assert xarray_ds.sample_name == qc_dataset.sample_name assert xarray_ds.exp_name == qc_dataset.exp_name - assert ( - xarray_ds.snapshot == qc_dataset.snapshot_raw - if qc_dataset.snapshot_raw is not None - else "null" - ) + if qc_dataset.snapshot_raw is not None: + assert xarray_ds.snapshot == qc_dataset.snapshot_raw assert xarray_ds.guid == qc_dataset.guid assert xarray_ds.run_timestamp == qc_dataset.run_timestamp() assert xarray_ds.completed_timestamp == qc_dataset.completed_timestamp() diff --git a/tests/dataset/test_snapshot.py b/tests/dataset/test_snapshot.py index d58a4077bb8a..57eb0c135331 100644 --- a/tests/dataset/test_snapshot.py +++ b/tests/dataset/test_snapshot.py @@ -77,9 +77,7 @@ def test_station_snapshot_during_measurement( assert expected_snapshot == snapshot_from_dataset # 2. Test `snapshot_raw` property - # this is not part of the DatasetProtocol interface - # but we test it anyway - assert json_snapshot_from_dataset == data_saver.dataset.snapshot_raw # type: ignore[attr-defined] + assert json_snapshot_from_dataset == data_saver.dataset.snapshot_raw # 3. Test `snapshot` property From 370da54f00f9b91c4fc50b6ba945300e99e6e6c1 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 08:08:26 +0200 Subject: [PATCH 41/70] Check the optional snapshots in the snapshots notebook A run only has a snapshot if one was recorded, so snapshot and snapshot_raw are both optional. The notebook indexed and passed them on without checking. Assert once where each is first read, which also tells the reader they are optional, and reuse the already checked value in the diff at the end rather than reading it from the dataset again. --- docs/changes/newsfragments/8441.improved.12 | 3 +++ docs/examples/DataSet/Working with snapshots.ipynb | 12 +++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 docs/changes/newsfragments/8441.improved.12 diff --git a/docs/changes/newsfragments/8441.improved.12 b/docs/changes/newsfragments/8441.improved.12 new file mode 100644 index 000000000000..04a44aad1a7c --- /dev/null +++ b/docs/changes/newsfragments/8441.improved.12 @@ -0,0 +1,3 @@ +The snapshot example notebook now checks that the snapshots it reads back from +the datasets are present before using them. A run only has a snapshot if one was +recorded, so both ``snapshot`` and ``snapshot_raw`` are optional. diff --git a/docs/examples/DataSet/Working with snapshots.ipynb b/docs/examples/DataSet/Working with snapshots.ipynb index 49d42c62d4e6..801126e0a1b4 100644 --- a/docs/examples/DataSet/Working with snapshots.ipynb +++ b/docs/examples/DataSet/Working with snapshots.ipynb @@ -593,7 +593,9 @@ "metadata": {}, "outputs": [], "source": [ - "snapshot_of_run = dataset.snapshot" + "snapshot_of_run = dataset.snapshot\n", + "# a run only has a snapshot if one was recorded, this one has\n", + "assert snapshot_of_run is not None" ] }, { @@ -602,7 +604,8 @@ "metadata": {}, "outputs": [], "source": [ - "snapshot_of_run_in_json_format = dataset.snapshot_raw" + "snapshot_of_run_in_json_format = dataset.snapshot_raw\n", + "assert snapshot_of_run_in_json_format is not None" ] }, { @@ -881,7 +884,10 @@ "metadata": {}, "outputs": [], "source": [ - "diff_param_values(dataset.snapshot, bad_dataset.snapshot).changed" + "snapshot_of_bad_run = bad_dataset.snapshot\n", + "assert snapshot_of_bad_run is not None\n", + "\n", + "diff_param_values(snapshot_of_run, snapshot_of_bad_run).changed" ] }, { From b0d25d1643fa610696fe7ac945e9f3b0ccd21d11 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 27 Aug 2026 16:09:29 +0200 Subject: [PATCH 42/70] Unpack dond results safely in the AMI430 example dond returns a single dataset rather than a tuple of them unless squeeze=False is passed, so unpacking the result in the AMI430 example does not type check. Pass squeeze=False and take the first dataset. --- .../driver_examples/Qcodes example with AMI430.ipynb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/examples/driver_examples/Qcodes example with AMI430.ipynb b/docs/examples/driver_examples/Qcodes example with AMI430.ipynb index 3705dc284386..750ca3d212d8 100644 --- a/docs/examples/driver_examples/Qcodes example with AMI430.ipynb +++ b/docs/examples/driver_examples/Qcodes example with AMI430.ipynb @@ -35,7 +35,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "tags": [] }, @@ -860,8 +860,10 @@ "time_p.reset_clock()\n", "\n", "while elapsed_time < 2:\n", - " ds, _, _ = dond(sweep_1, sweep_2, dmm.v1, additional_setpoints=(time_p,))\n", - " timed_datasets.append(ds)\n", + " dss, _, _ = dond(\n", + " sweep_1, sweep_2, dmm.v1, additional_setpoints=(time_p,), squeeze=False\n", + " )\n", + " timed_datasets.append(dss[0])\n", " time.sleep(0.5)\n", " elapsed_time = time_p.get()" ] From b27f9b5db8ae53cebfb0ccb9c0ded95c560965a0 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 27 Aug 2026 16:25:05 +0200 Subject: [PATCH 43/70] Ask dond for unsqueezed results in the docs dond returns a single dataset rather than a tuple of them unless squeeze=False is passed, so notebooks that unpack the result could not be type checked. Pass squeeze=False and index into the result instead. --- ...ext_manager_for_performing_measurements.ipynb | 16 ++++++++++++---- .../Parameter_defined_InterDependencies.ipynb | 10 +++++----- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/docs/examples/DataSet/Using_doNd_functions_in_comparison_to_Measurement_context_manager_for_performing_measurements.ipynb b/docs/examples/DataSet/Using_doNd_functions_in_comparison_to_Measurement_context_manager_for_performing_measurements.ipynb index fd2977868fcd..20d10cb774bd 100644 --- a/docs/examples/DataSet/Using_doNd_functions_in_comparison_to_Measurement_context_manager_for_performing_measurements.ipynb +++ b/docs/examples/DataSet/Using_doNd_functions_in_comparison_to_Measurement_context_manager_for_performing_measurements.ipynb @@ -1404,7 +1404,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -1451,7 +1451,15 @@ } ], "source": [ - "result = dond(sweep_1, sweep_2, [dmm.v1], [dmm.v2], do_plot=True, show_progress=True)" + "result = dond(\n", + " sweep_1,\n", + " sweep_2,\n", + " [dmm.v1],\n", + " [dmm.v2],\n", + " do_plot=True,\n", + " show_progress=True,\n", + " squeeze=False,\n", + ")" ] }, { @@ -1979,7 +1987,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv", + "display_name": "qcodes", "language": "python", "name": "python3" }, @@ -1993,7 +2001,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.7" + "version": "3.14.7" }, "toc": { "base_numbering": 1, diff --git a/docs/examples/Parameters/Parameter_defined_InterDependencies.ipynb b/docs/examples/Parameters/Parameter_defined_InterDependencies.ipynb index 3cd2ca6c7bc8..cb672c7fb204 100644 --- a/docs/examples/Parameters/Parameter_defined_InterDependencies.ipynb +++ b/docs/examples/Parameters/Parameter_defined_InterDependencies.ipynb @@ -224,7 +224,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "73823b84", "metadata": {}, "outputs": [ @@ -252,14 +252,14 @@ "source": [ "from qcodes.dataset import LinSweep, dond\n", "\n", - "ds, _, _ = dond(LinSweep(control, 0, 1, 11), meas_param)\n", - "ds.get_parameter_data()" + "ds, _, _ = dond(LinSweep(control, 0, 1, 11), meas_param, squeeze=False)\n", + "ds[0].get_parameter_data()" ] } ], "metadata": { "kernelspec": { - "display_name": "py311", + "display_name": "qcodes", "language": "python", "name": "python3" }, @@ -273,7 +273,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.8" + "version": "3.14.7" } }, "nbformat": 4, From e506ccf704adf4b116531fd9ef93e791c53d80ed Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 27 Aug 2026 16:32:20 +0200 Subject: [PATCH 44/70] Allow ParamSpecBase in get_parameter_data get_parameter_data only uses the name of each parameter, so it already accepts any ParamSpecBase at runtime, but it was annotated as taking a ParamSpec. Widen the annotation on the protocol and both implementations, so that the example notebook can pass ParamSpecBase objects. Also drop the cell in that notebook that displays dataset.parameters. The dataset there is a DataSetProtocol, which has no parameters attribute, and the cell before it already shows dataset.get_parameters(). --- .../DataSet/Accessing-data-in-DataSet.ipynb | 24 ++----------------- src/qcodes/dataset/data_set.py | 2 +- src/qcodes/dataset/data_set_in_memory.py | 4 ++-- src/qcodes/dataset/data_set_protocol.py | 6 ++--- 4 files changed, 8 insertions(+), 28 deletions(-) diff --git a/docs/examples/DataSet/Accessing-data-in-DataSet.ipynb b/docs/examples/DataSet/Accessing-data-in-DataSet.ipynb index e3909e71bdc0..15b33bcd9d94 100644 --- a/docs/examples/DataSet/Accessing-data-in-DataSet.ipynb +++ b/docs/examples/DataSet/Accessing-data-in-DataSet.ipynb @@ -710,26 +710,6 @@ "dataset.get_parameters()" ] }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'x,t,y,y2,q'" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "dataset.parameters" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -1047,7 +1027,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv", + "display_name": "qcodes", "language": "python", "name": "python3" }, @@ -1061,7 +1041,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.12" + "version": "3.14.7" }, "toc": { "base_numbering": 1, diff --git a/src/qcodes/dataset/data_set.py b/src/qcodes/dataset/data_set.py index ccb2e32ce2af..4dbc176b9775 100644 --- a/src/qcodes/dataset/data_set.py +++ b/src/qcodes/dataset/data_set.py @@ -899,7 +899,7 @@ def _ensure_dataset_written(self) -> None: def get_parameter_data( self, - *params: str | ParamSpec | ParameterBase, + *params: str | ParamSpecBase | ParameterBase, start: int | None = None, end: int | None = None, callback: Callable[[float], None] | None = None, diff --git a/src/qcodes/dataset/data_set_in_memory.py b/src/qcodes/dataset/data_set_in_memory.py index 5fe228c189f2..d42fac295d59 100644 --- a/src/qcodes/dataset/data_set_in_memory.py +++ b/src/qcodes/dataset/data_set_in_memory.py @@ -899,7 +899,7 @@ def to_pandas_dataframe( def get_parameter_data( self, - *params: str | ParamSpec | ParameterBase, + *params: str | ParamSpecBase | ParameterBase, start: int | None = None, end: int | None = None, callback: Callable[[float], None] | None = None, @@ -909,7 +909,7 @@ def get_parameter_data( @staticmethod def _warn_if_set( - *params: str | ParamSpec | ParameterBase, + *params: str | ParamSpecBase | ParameterBase, start: int | None = None, end: int | None, ) -> None: diff --git a/src/qcodes/dataset/data_set_protocol.py b/src/qcodes/dataset/data_set_protocol.py index 5628aab7c727..222a9af47d73 100644 --- a/src/qcodes/dataset/data_set_protocol.py +++ b/src/qcodes/dataset/data_set_protocol.py @@ -207,7 +207,7 @@ def cache(self) -> DataSetCache[DataSetProtocol]: ... def get_parameter_data( self, - *params: str | ParamSpec | ParameterBase, + *params: str | ParamSpecBase | ParameterBase, start: int | None = None, end: int | None = None, callback: Callable[[float], None] | None = None, @@ -470,11 +470,11 @@ def _add_metadata_to_netcdf_if_nc_exported(self, tag: str, data: Any) -> None: ) @staticmethod - def _validate_parameters(*params: str | ParamSpec | ParameterBase) -> list[str]: + def _validate_parameters(*params: str | ParamSpecBase | ParameterBase) -> list[str]: """ Validate that the provided parameters have a name and return those names as a list. - The Parameters may be a mix of strings, ParamSpecs or ordinary + The Parameters may be a mix of strings, ParamSpecsBase or ordinary QCoDeS parameters. """ From 459102174277cc0967932955f91a7b5c187062a1 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 27 Aug 2026 16:34:48 +0200 Subject: [PATCH 45/70] Check for a missing config in the config notebook qc.config.current_config is optional, so the notebook has to establish that a configuration is loaded before reading values out of it. --- docs/examples/basic_examples/Configuring_QCoDeS.ipynb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/examples/basic_examples/Configuring_QCoDeS.ipynb b/docs/examples/basic_examples/Configuring_QCoDeS.ipynb index 0a02fb04a7c8..b6f2229db17c 100644 --- a/docs/examples/basic_examples/Configuring_QCoDeS.ipynb +++ b/docs/examples/basic_examples/Configuring_QCoDeS.ipynb @@ -553,10 +553,12 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ + "if qc.config.current_config is None:\n", + " raise RuntimeError(\"No current QCoDeS configuration is set.\")\n", "qc.config.current_config.core.loglevel = \"INFO\"" ] }, @@ -585,7 +587,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -603,6 +605,8 @@ } ], "source": [ + "if qc.config.current_config is None:\n", + " raise RuntimeError(\"No current QCoDeS configuration is set.\")\n", "qc.config.current_config.core.loglevel = \"YOLO\"\n", "qc.config.validate()\n", "# NOTE that you how have a broken config!" From c57dfb6fecd9b11253f28e197089995cc16b365d Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 27 Aug 2026 16:37:12 +0200 Subject: [PATCH 46/70] Match the set_raw parameter name in the docs Overriding set_raw with a differently named argument is an invalid override, since callers are free to pass value as a keyword. --- docs/examples/Parameters/Parameters.ipynb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/examples/Parameters/Parameters.ipynb b/docs/examples/Parameters/Parameters.ipynb index f5d488a6c9f2..aff6c0527980 100644 --- a/docs/examples/Parameters/Parameters.ipynb +++ b/docs/examples/Parameters/Parameters.ipynb @@ -82,7 +82,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -103,8 +103,8 @@ " self._count += 1\n", " return self._count\n", "\n", - " def set_raw(self, val):\n", - " self._count = val\n", + " def set_raw(self, value):\n", + " self._count = value\n", " return self._count" ] }, From fec940261b9fb5a863617ce7f9b9720eec82874b Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 27 Aug 2026 16:48:42 +0200 Subject: [PATCH 47/70] Narrow root_instrument in the example parameters The parameters in these examples read the point count off the instrument they belong to, which only the concrete instrument class declares. Override root_instrument to return that class, so that the examples state what they already assume. --- .../DataSet/Threaded data acquisition.ipynb | 13 ++++++++++++- ...nts-defined-on-a-different-instrument.ipynb | 18 +++++++++++++++--- ...ple-Example-of-ParameterWithSetpoints.ipynb | 16 +++++++++++++++- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/docs/examples/DataSet/Threaded data acquisition.ipynb b/docs/examples/DataSet/Threaded data acquisition.ipynb index ae5e2c38cbde..0f41089fcd08 100644 --- a/docs/examples/DataSet/Threaded data acquisition.ipynb +++ b/docs/examples/DataSet/Threaded data acquisition.ipynb @@ -92,7 +92,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "2add302b", "metadata": { "ExecuteTime": { @@ -115,6 +115,17 @@ " time.sleep(0.1)\n", " return val\n", "\n", + " @property\n", + " def root_instrument(self) -> \"DummyInstrumentWithMeasurement\":\n", + " if self.instrument is None or not isinstance(\n", + " self.instrument.root_instrument, DummyInstrumentWithMeasurement\n", + " ):\n", + " raise ValueError(\n", + " \"SleepyDmmExponentialParameter must be bound to a DummyInstrumentWithMeasurement\"\n", + " )\n", + " instr = self.instrument.root_instrument\n", + " return instr\n", + "\n", " @staticmethod\n", " def _exponential_decay(a: float, b: float):\n", " x = 0\n", diff --git a/docs/examples/Parameters/Parameter-With-Setpoints-defined-on-a-different-instrument.ipynb b/docs/examples/Parameters/Parameter-With-Setpoints-defined-on-a-different-instrument.ipynb index c86165c1306d..8225df8c836b 100644 --- a/docs/examples/Parameters/Parameter-With-Setpoints-defined-on-a-different-instrument.ipynb +++ b/docs/examples/Parameters/Parameter-With-Setpoints-defined-on-a-different-instrument.ipynb @@ -81,7 +81,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -108,6 +108,18 @@ " npoints = self.root_instrument.sweep_n_points.get_latest()\n", " return np.random.default_rng().random(npoints)\n", "\n", + " @property\n", + " def root_instrument(self) -> \"DummyBufferedDMM\":\n", + " # we override this to enforce that the root instrument is a DummyBufferedDMM\n", + " # to ensure that we can get the value of the n_points parameter\n", + " if self._instrument is None:\n", + " raise ValueError(\"Instrument is not set for this parameter\")\n", + " root_instrument = self._instrument.root_instrument\n", + " if not isinstance(root_instrument, DummyBufferedDMM):\n", + " raise TypeError(\"root_instrument must be an instance of DummyBufferedDMM\")\n", + "\n", + " return root_instrument\n", + "\n", "\n", "class DummyBufferedDMM(Instrument):\n", " def __init__(self, name, **kwargs):\n", @@ -655,7 +667,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "qcodes", "language": "python", "name": "python3" }, @@ -669,7 +681,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.9" + "version": "3.14.7" }, "toc": { "base_numbering": 1, diff --git a/docs/examples/Parameters/Simple-Example-of-ParameterWithSetpoints.ipynb b/docs/examples/Parameters/Simple-Example-of-ParameterWithSetpoints.ipynb index 0358ae1f4c93..1db1eb9e5b09 100644 --- a/docs/examples/Parameters/Simple-Example-of-ParameterWithSetpoints.ipynb +++ b/docs/examples/Parameters/Simple-Example-of-ParameterWithSetpoints.ipynb @@ -83,7 +83,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -110,6 +110,20 @@ " npoints = self.root_instrument.n_points.get_latest()\n", " return np.random.default_rng().random(npoints)\n", "\n", + " @property\n", + " def root_instrument(self) -> \"DummySpectrumAnalyzer\":\n", + " # we override this to enforce that the root instrument is a DummySpectrumAnalyzer\n", + " # to ensure that we can get the value of the n_points parameter\n", + " if self._instrument is None:\n", + " raise ValueError(\"Instrument is not set for this parameter\")\n", + " root_instrument = self._instrument.root_instrument\n", + " if not isinstance(root_instrument, DummySpectrumAnalyzer):\n", + " raise TypeError(\n", + " \"root_instrument must be an instance of DummySpectrumAnalyzer\"\n", + " )\n", + "\n", + " return root_instrument\n", + "\n", "\n", "class DummySpectrumAnalyzer(Instrument):\n", " def __init__(self, name, **kwargs):\n", From 966af1d42e0603c2e76028a34a6edf6b66ff22a1 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 28 Aug 2026 21:51:03 +0200 Subject: [PATCH 48/70] Suppress ty on the InstrumentMeta patch in docs docs/conf.py replaces InstrumentMeta with ABCMeta, so that sphinx documents the signature of __init__ rather than __call__. ty rejects assigning a different class to the module attribute, which is the whole point of the patch. --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 0514625c4bd1..135398f4799e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -33,7 +33,7 @@ # this should happen as early as possible import qcodes.instrument.instrument_meta -qcodes.instrument.instrument_meta.InstrumentMeta = ABCMeta +qcodes.instrument.instrument_meta.InstrumentMeta = ABCMeta # ty: ignore[invalid-assignment] # we need to reload any module that has been imported and # makes use of this metaclass. The modules below are all imported # by importing qcodes.instrument so we need to reload them From f6171fd68047c85fc784fdbba6245650a82e95be Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 28 Aug 2026 21:52:26 +0200 Subject: [PATCH 49/70] Guard the DataSetInMem only API in the docs set_netcdf_location exists on DataSetInMem but not on the protocol that load_by_run_spec returns, so the exporting notebook narrows the dataset before calling it. The walkthrough notebook loses the two cells reading DataSet.started and DataSet.parameters, which are likewise not part of the protocol. --- .../DataSet/DataSet-class-walkthrough.ipynb | 51 +------------------ ...Exporting-data-to-other-file-formats.ipynb | 8 ++- 2 files changed, 9 insertions(+), 50 deletions(-) diff --git a/docs/examples/DataSet/DataSet-class-walkthrough.ipynb b/docs/examples/DataSet/DataSet-class-walkthrough.ipynb index 44e9fe09ef02..9c360867a7ef 100644 --- a/docs/examples/DataSet/DataSet-class-walkthrough.ipynb +++ b/docs/examples/DataSet/DataSet-class-walkthrough.ipynb @@ -235,7 +235,7 @@ }, { "data": { - "image/png": "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\n", + "image/png": "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", "text/plain": [ "
" ] @@ -247,7 +247,7 @@ }, { "data": { - "image/png": "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\n", + "image/png": "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", "text/plain": [ "
" ] @@ -866,26 +866,6 @@ "dataset.completed" ] }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 26, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "dataset.started" - ] - }, { "cell_type": "code", "execution_count": 27, @@ -1218,33 +1198,6 @@ "dataset.get_parameters()" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Lastly, `DataSet` has `parameters` that returns a string with comma-separated names of all the dataset parameters (will likely be deprecated soon):" - ] - }, - { - "cell_type": "code", - "execution_count": 39, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'x,t,y,y2'" - ] - }, - "execution_count": 39, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "dataset.parameters" - ] - }, { "cell_type": "markdown", "metadata": {}, diff --git a/docs/examples/DataSet/Exporting-data-to-other-file-formats.ipynb b/docs/examples/DataSet/Exporting-data-to-other-file-formats.ipynb index 8ea718a348e0..930555694e97 100644 --- a/docs/examples/DataSet/Exporting-data-to-other-file-formats.ipynb +++ b/docs/examples/DataSet/Exporting-data-to-other-file-formats.ipynb @@ -1116,7 +1116,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -1131,6 +1131,12 @@ } ], "source": [ + "from qcodes.dataset.data_set_in_memory import DataSetInMem\n", + "\n", + "# the set_netcdf_location only exists for DataSetInMem so we validate that we are indeed using that.\n", + "if not isinstance(reloaded_ds, DataSetInMem):\n", + " raise TypeError(\"reloaded_ds must be an instance of DataSetInMem\")\n", + "\n", "reloaded_ds.set_netcdf_location(new_file_path)\n", "reloaded_ds.export_info" ] From d91bb2101f7b093f80a82b2ed9cc51332089d8f6 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 28 Aug 2026 22:06:27 +0200 Subject: [PATCH 50/70] Allow a sequence of arrays as a measurement result A MultiParameter that measures more than one array returns a tuple of arrays. DataSaver.add_result has always unpacked such results, but ValuesType had no arm for them, so type checkers rejected the call. --- docs/changes/newsfragments/8441.improved.13 | 5 +++++ src/qcodes/dataset/data_set_protocol.py | 1 + 2 files changed, 6 insertions(+) create mode 100644 docs/changes/newsfragments/8441.improved.13 diff --git a/docs/changes/newsfragments/8441.improved.13 b/docs/changes/newsfragments/8441.improved.13 new file mode 100644 index 000000000000..3a3cbdae81b2 --- /dev/null +++ b/docs/changes/newsfragments/8441.improved.13 @@ -0,0 +1,5 @@ +The type alias ``ValuesType``, used by ``DataSaver.add_result`` and related +methods, now also accepts a sequence of numpy arrays. This is what a +``MultiParameter`` measuring more than one array returns, so passing such a +result to ``DataSaver.add_result`` no longer produces a type checking error. +The behavior at runtime is unchanged since this was already supported. diff --git a/src/qcodes/dataset/data_set_protocol.py b/src/qcodes/dataset/data_set_protocol.py index 222a9af47d73..ed96f751dac9 100644 --- a/src/qcodes/dataset/data_set_protocol.py +++ b/src/qcodes/dataset/data_set_protocol.py @@ -55,6 +55,7 @@ | npt.NDArray | Sequence[ScalarResTypes] | Sequence[Sequence[ScalarResTypes]] + | Sequence[npt.NDArray] ) type ResType = "tuple[ParameterBase | str, ValuesType]" type SetpointsType = "Sequence[str | ParameterBase]" From dc83c37a37a3182cae4fd28fa1f0e02522ec885f Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:10:44 +0200 Subject: [PATCH 51/70] Suppress ty errors where mypy is already suppressed ty does not understand the error codes in a mypy type: ignore comment, so every deliberately wrong call in the test suite is reported twice. Add a matching ty: ignore comment on those lines. This is the mechanical part of getting the tests to type check with ty and leaves only the diagnostics that are not already suppressed for mypy. --- .../test_measurement_context_manager.py | 8 ++--- tests/dataset/test_converters.py | 4 +-- tests/dataset/test_datasaver.py | 4 +-- tests/dataset/test_dataset_basic.py | 4 +-- tests/dataset/test_db_overview.py | 2 +- tests/dataset/test_dependencies.py | 12 +++---- tests/dataset/test_descriptions.py | 2 +- ...st_parameter_with_setpoints_has_control.py | 2 +- tests/dataset/test_paramspec.py | 2 +- tests/dataset/test_snapshot.py | 2 +- tests/dataset/test_sqlite_base.py | 2 +- tests/dataset/test_sqlite_connection.py | 4 +-- tests/dataset/test_string_data.py | 4 +-- tests/delegate/test_delegate_instrument.py | 2 +- .../b1500_driver_tests/test_b1517a_smu.py | 4 +-- tests/drivers/test_ami430_visa.py | 4 +-- tests/drivers/test_lakeshore_372.py | 10 +++--- tests/drivers/test_mercuryips.py | 2 +- tests/drivers/test_signal_hound_usb_sa124b.py | 2 +- tests/drivers/test_sr830.py | 4 +-- tests/drivers/test_tektronix_awg70000a.py | 2 +- tests/helpers/test_delegate_attribues.py | 10 +++--- tests/helpers/test_strip_attrs.py | 6 ++-- tests/parameter/test_array_parameter.py | 6 ++-- tests/parameter/test_delegate_parameter.py | 6 ++-- .../parameter/test_elapsed_time_parameter.py | 2 +- tests/parameter/test_get_latest.py | 6 ++-- tests/parameter/test_get_set_wrapping.py | 2 +- tests/parameter/test_group_parameter.py | 2 +- tests/parameter/test_issequenceof.py | 4 +-- tests/parameter/test_keyword_only_args.py | 28 ++++++++-------- tests/parameter/test_manual_parameter.py | 2 +- tests/parameter/test_multi_parameter.py | 4 +-- tests/parameter/test_on_off_mapping.py | 4 +-- tests/parameter/test_parameter_basics.py | 14 ++++---- tests/parameter/test_parameter_cache.py | 14 ++++---- .../test_parameter_context_manager.py | 2 +- tests/parameter/test_parameter_validation.py | 2 +- .../test_parameter_with_setpoints.py | 8 ++--- tests/parameter/test_scaled_parameter.py | 4 +-- tests/parameter/test_snapshot.py | 32 +++++++++---------- tests/test_channels.py | 8 ++--- tests/test_command.py | 10 +++--- tests/test_import.py | 18 +++++------ tests/test_instrument.py | 4 +-- tests/test_logger.py | 4 +-- tests/test_metadata.py | 4 +-- tests/test_station.py | 4 +-- tests/utils/test_class_strings.py | 2 +- tests/utils/test_isfunction.py | 2 +- tests/validators/test_arrays.py | 10 +++--- tests/validators/test_basic.py | 4 +-- tests/validators/test_bool.py | 2 +- tests/validators/test_callable.py | 2 +- tests/validators/test_complex.py | 2 +- tests/validators/test_dict.py | 2 +- tests/validators/test_enum.py | 2 +- tests/validators/test_ints.py | 14 ++++---- tests/validators/test_lists.py | 4 +-- tests/validators/test_multi_type.py | 6 ++-- tests/validators/test_multi_type_and.py | 2 +- tests/validators/test_multi_type_or.py | 2 +- tests/validators/test_multiples.py | 4 +-- tests/validators/test_numbers.py | 2 +- tests/validators/test_sequence.py | 2 +- tests/validators/test_string.py | 16 +++++----- 66 files changed, 186 insertions(+), 186 deletions(-) diff --git a/tests/dataset/measurement/test_measurement_context_manager.py b/tests/dataset/measurement/test_measurement_context_manager.py index d42fdaaa2cba..92481999e18c 100644 --- a/tests/dataset/measurement/test_measurement_context_manager.py +++ b/tests/dataset/measurement/test_measurement_context_manager.py @@ -95,10 +95,10 @@ def test_register_parameter_arg_types( meas.register_parameter(DMM.v1, setpoints="foo") with pytest.raises(TypeError): - meas.register_parameter(DMM.v1, basis=(DAC.ch1, 3)) # type: ignore[arg-type] + meas.register_parameter(DMM.v1, basis=(DAC.ch1, 3)) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises(TypeError): - meas.register_parameter(DMM.v1, setpoints=(DAC.ch1, 3)) # type: ignore[arg-type] + meas.register_parameter(DMM.v1, setpoints=(DAC.ch1, 3)) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_register_parameter_numbers(DAC: DummyInstrument, DMM: DummyInstrument) -> None: @@ -112,7 +112,7 @@ def test_register_parameter_numbers(DAC: DummyInstrument, DMM: DummyInstrument) for not_a_parameter in not_parameters: with pytest.raises(ValueError): - meas.register_parameter(not_a_parameter) # type: ignore[arg-type] + meas.register_parameter(not_a_parameter) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] my_param = DAC.ch1 meas.register_parameter(my_param) @@ -1015,7 +1015,7 @@ def test_datasaver_foul_input(bg_writing: bool) -> None: with meas.run(bg_writing) as datasaver: for ft in foul_stuff: with pytest.raises(ValueError): - datasaver.add_result(("foul", ft)) # type: ignore[arg-type] + datasaver.add_result(("foul", ft)) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] @settings(max_examples=10, deadline=None) diff --git a/tests/dataset/test_converters.py b/tests/dataset/test_converters.py index e970618d4824..eb3d2d524c98 100644 --- a/tests/dataset/test_converters.py +++ b/tests/dataset/test_converters.py @@ -198,11 +198,11 @@ def test_construct_current_rundescriber_from_fake_v4( version=4, shapes=None, ) - v4["foobar"] = {"foo": ["bar"]} # type: ignore[typeddict-unknown-key] + v4["foobar"] = {"foo": ["bar"]} # type: ignore[typeddict-unknown-key] # ty: ignore[invalid-key] rds1 = RunDescriber._from_dict(v4) rds_upgraded = from_dict_to_current(v4) v3 = v4.copy() - v3.pop("foobar") # type: ignore[typeddict-item] + v3.pop("foobar") # type: ignore[typeddict-item] # ty: ignore[invalid-key] v3["version"] = 3 assert rds1._to_dict() == v3 assert rds_upgraded._to_dict() == v3 diff --git a/tests/dataset/test_datasaver.py b/tests/dataset/test_datasaver.py index a552b1b7c23b..695dde6f17c2 100644 --- a/tests/dataset/test_datasaver.py +++ b/tests/dataset/test_datasaver.py @@ -177,7 +177,7 @@ def test_saving_numeric_values_as_text( data_saver.add_result((p.name, value)) finally: data_saver.dataset.mark_completed() - data_saver.dataset.conn.close() # type: ignore[attr-defined] + data_saver.dataset.conn.close() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] @pytest.mark.usefixtures("experiment") @@ -209,4 +209,4 @@ def test_duplicated_parameter_raises() -> None: data_saver.add_result((p.name, 1), (p.name, 1)) finally: data_saver.dataset.mark_completed() - data_saver.dataset.conn.close() # type: ignore[attr-defined] + data_saver.dataset.conn.close() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] diff --git a/tests/dataset/test_dataset_basic.py b/tests/dataset/test_dataset_basic.py index b446bc0fb1ac..350decb674e2 100644 --- a/tests/dataset/test_dataset_basic.py +++ b/tests/dataset/test_dataset_basic.py @@ -321,7 +321,7 @@ def test_load_by_id_for_none() -> None: with pytest.raises( ValueError, match=re.escape("run_id has to be a positive integer, not None.") ): - _ = load_by_id(None) # type: ignore[arg-type] + _ = load_by_id(None) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] @settings(deadline=None, max_examples=6) @@ -845,7 +845,7 @@ def test_parent_dataset_links_invalid_input() -> None: match = re.escape("Invalid input. Did not receive a list of Links") with pytest.raises(ValueError, match=match): - ds.parent_dataset_links = [ds.guid] # type: ignore[list-item] + ds.parent_dataset_links = [ds.guid] # type: ignore[list-item] # ty: ignore[invalid-assignment] match = re.escape( "Invalid input. All links must point to this dataset. " diff --git a/tests/dataset/test_db_overview.py b/tests/dataset/test_db_overview.py index 5e4fdaf63bf2..218f3c93b486 100644 --- a/tests/dataset/test_db_overview.py +++ b/tests/dataset/test_db_overview.py @@ -148,7 +148,7 @@ def test_get_db_overview_extra_columns(db_conn: AtomicConnection) -> None: # An existing ad-hoc metadata column is returned ... overview = get_db_overview(conn=db_conn, extra_columns=["my_tag"]) - assert overview[1]["my_tag"] == "hello" # type: ignore[typeddict-item] + assert overview[1]["my_tag"] == "hello" # type: ignore[typeddict-item] # ty: ignore[invalid-key] # ... while a non-existent column is silently skipped. overview = get_db_overview(conn=db_conn, extra_columns=["does_not_exist"]) diff --git a/tests/dataset/test_dependencies.py b/tests/dataset/test_dependencies.py index 8aee0c94575f..22a6ee3c1c3b 100644 --- a/tests/dataset/test_dependencies.py +++ b/tests/dataset/test_dependencies.py @@ -24,7 +24,7 @@ def test_wrong_input_raises() -> None: ["p1", ParamSpec("p2", paramtype="text")], ): with pytest.raises(ValueError): - InterDependencies(pspecs) # type: ignore[arg-type] + InterDependencies(pspecs) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_init( @@ -82,18 +82,18 @@ def test_init_validation_raises( for tree, cause in zip(invalid_trees, causes): with pytest.raises(ValueError, match="Invalid dependencies") as ei: - InterDependencies_(dependencies=tree, inferences={}) # type: ignore[arg-type] + InterDependencies_(dependencies=tree, inferences={}) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert error_caused_by(ei, cause=cause) for tree, cause in zip(invalid_trees, causes): with pytest.raises(ValueError, match="Invalid inferences") as ei: - InterDependencies_(dependencies={}, inferences=tree) # type: ignore[arg-type] + InterDependencies_(dependencies={}, inferences=tree) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert error_caused_by(ei, cause=cause) with pytest.raises(ValueError, match="Invalid standalones") as ei: - InterDependencies_(standalones=("ps1", "ps2")) # type: ignore[arg-type] + InterDependencies_(standalones=("ps1", "ps2")) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert error_caused_by(ei, cause="Standalones must be a sequence of ParamSpecs") @@ -105,8 +105,8 @@ def test_init_validation_raises( for inv in invalid_trees_2: with pytest.raises(ValueError, match="already exists"): InterDependencies_( - dependencies=inv["deps"], # type: ignore[arg-type] - inferences=inv["inffs"], # type: ignore[arg-type] + dependencies=inv["deps"], # type: ignore[arg-type] # ty: ignore[invalid-argument-type] + inferences=inv["inffs"], # type: ignore[arg-type] # ty: ignore[invalid-argument-type] ) diff --git a/tests/dataset/test_descriptions.py b/tests/dataset/test_descriptions.py index 44747961758d..2ebf8424e5ba 100644 --- a/tests/dataset/test_descriptions.py +++ b/tests/dataset/test_descriptions.py @@ -16,7 +16,7 @@ def test_wrong_input_type_raises() -> None: for interdeps in ("interdeps", ["p1", "p2"], 0): with pytest.raises(ValueError): - RunDescriber(interdeps=interdeps) # type: ignore[arg-type] + RunDescriber(interdeps=interdeps) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_equality( diff --git a/tests/dataset/test_parameter_with_setpoints_has_control.py b/tests/dataset/test_parameter_with_setpoints_has_control.py index 430a0b68fa91..6bb284085e93 100644 --- a/tests/dataset/test_parameter_with_setpoints_has_control.py +++ b/tests/dataset/test_parameter_with_setpoints_has_control.py @@ -33,7 +33,7 @@ def unpack_self( res.append((controlled, controlled())) return res - p = _ControlledSetpoints(name, **kwargs) # type: ignore[arg-type] + p = _ControlledSetpoints(name, **kwargs) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] p.has_control_of.add(controlled) return p diff --git a/tests/dataset/test_paramspec.py b/tests/dataset/test_paramspec.py index 7ad5d1a286e7..700e2718ff2c 100644 --- a/tests/dataset/test_paramspec.py +++ b/tests/dataset/test_paramspec.py @@ -129,7 +129,7 @@ def test_creation( invalid_types = ("np.array", "ndarray", "lala", "", Number, ndarray, 0, None) for inv_type in invalid_types: with pytest.raises(ValueError): - ParamSpec(name, inv_type) # type: ignore[arg-type] + ParamSpec(name, inv_type) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] if not inff1.isidentifier(): inff1 = "inff1" diff --git a/tests/dataset/test_snapshot.py b/tests/dataset/test_snapshot.py index 57eb0c135331..6d05d2ba54e0 100644 --- a/tests/dataset/test_snapshot.py +++ b/tests/dataset/test_snapshot.py @@ -67,7 +67,7 @@ def test_station_snapshot_during_measurement( # 1. Test `get_metadata('snapshot')` method # this is not part of the DatasetProtocol interface # but we test it anyway - json_snapshot_from_dataset = data_saver.dataset.get_metadata("snapshot") # type: ignore[attr-defined] + json_snapshot_from_dataset = data_saver.dataset.get_metadata("snapshot") # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] snapshot_from_dataset = json.loads(json_snapshot_from_dataset) expected_snapshot = { diff --git a/tests/dataset/test_sqlite_base.py b/tests/dataset/test_sqlite_base.py index 19af1c240bdb..3cfea0c9bd6e 100644 --- a/tests/dataset/test_sqlite_base.py +++ b/tests/dataset/test_sqlite_base.py @@ -236,7 +236,7 @@ def test_get_layout_id_with_invalid_parameter_type(dataset: "DataSet") -> None: with pytest.raises(ValueError, match="Wrong parameter type, must be ParamSpec"): mut_queries._get_layout_id( dataset.conn, - 42, # type: ignore[arg-type] + 42, # type: ignore[arg-type] # ty: ignore[invalid-argument-type] dataset.run_id, ) diff --git a/tests/dataset/test_sqlite_connection.py b/tests/dataset/test_sqlite_connection.py index e55d29f645d2..f808aab827b9 100644 --- a/tests/dataset/test_sqlite_connection.py +++ b/tests/dataset/test_sqlite_connection.py @@ -78,7 +78,7 @@ def test_atomic_raises_for_non_atomic_conn() -> None: with ( closing(sqlite3.connect(":memory:")) as sqlite_conn, pytest.raises(ValueError, match=match_str), - atomic(sqlite_conn), # type: ignore[arg-type] + atomic(sqlite_conn), # type: ignore[arg-type] # ty: ignore[invalid-argument-type] ): pass @@ -362,7 +362,7 @@ def test_atomic_transaction_on_sqlite3_connection_raises(tmp_path: "Path") -> No closing(sqlite3.connect(dbfile)) as conn, pytest.raises(ValueError, match=match_str), ): - atomic_transaction(conn, "whatever sql query") # type: ignore[arg-type] + atomic_transaction(conn, "whatever sql query") # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_connect() -> None: diff --git a/tests/dataset/test_string_data.py b/tests/dataset/test_string_data.py index 3dba7e800d25..8484dd02d033 100644 --- a/tests/dataset/test_string_data.py +++ b/tests/dataset/test_string_data.py @@ -144,7 +144,7 @@ def test_string_with_wrong_paramtype_via_datasaver() -> None: with pytest.raises(ValueError, match=msg): data_saver.add_result(("p", "some text")) finally: - data_saver.dataset.conn.close() # type: ignore[attr-defined] + data_saver.dataset.conn.close() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] @pytest.mark.usefixtures("experiment") @@ -200,7 +200,7 @@ def test_list_of_strings(experiment: "Experiment") -> None: try: np.testing.assert_array_equal(actual_data, expec_data) finally: - test_set.conn.close() # type: ignore[attr-defined] + test_set.conn.close() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] @settings(suppress_health_check=(HealthCheck.function_scoped_fixture,), deadline=None) diff --git a/tests/delegate/test_delegate_instrument.py b/tests/delegate/test_delegate_instrument.py index b221757afa15..3d856abb853a 100644 --- a/tests/delegate/test_delegate_instrument.py +++ b/tests/delegate/test_delegate_instrument.py @@ -83,5 +83,5 @@ def close_partially_constructed_instrument() -> None: DelegateInstrument( name=name, station=station, - parameters={"X": 42}, # type: ignore[dict-item] + parameters={"X": 42}, # type: ignore[dict-item] # ty: ignore[invalid-argument-type] ) diff --git a/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1517a_smu.py b/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1517a_smu.py index d2f36cae9694..ea4bedb6d5e7 100644 --- a/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1517a_smu.py +++ b/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1517a_smu.py @@ -34,10 +34,10 @@ def test_snapshot() -> None: # We need to use `InstrumentBase` (not a bare mock) in order for # `snapshot` methods call resolution to work out mainframe = InstrumentBase(name="mainframe") - mainframe.write = MagicMock() # type: ignore[attr-defined] + mainframe.write = MagicMock() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] slot_nr = 1 smu = KeysightB1517A( - parent=mainframe, # type: ignore[arg-type] + parent=mainframe, # type: ignore[arg-type] # ty: ignore[invalid-argument-type] name="B1517A", slot_nr=slot_nr, ) diff --git a/tests/drivers/test_ami430_visa.py b/tests/drivers/test_ami430_visa.py index 256ec5fef493..6b5e55f5fb45 100644 --- a/tests/drivers/test_ami430_visa.py +++ b/tests/drivers/test_ami430_visa.py @@ -419,7 +419,7 @@ def test_instantiation_from_badly_typed_argument( "AMI430_3D", mag_x.name, mag_y, - badly_typed_instrument_z_argument, # type: ignore[arg-type] + badly_typed_instrument_z_argument, # type: ignore[arg-type] # ty: ignore[invalid-argument-type] field_limit, ) @@ -2039,7 +2039,7 @@ def test_3d_driver_invalid_field_limit_type( mag_x, mag_y, mag_z = magnet_axes_instances request.addfinalizer(Instrument.close_all) with pytest.raises(ValueError, match="field limit should either be a number"): - AMIModel4303D("AMI430_3D", mag_x, mag_y, mag_z, None) # type: ignore[arg-type] + AMIModel4303D("AMI430_3D", mag_x, mag_y, mag_z, None) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_ramp_simultaneously(current_driver: AMIModel4303D) -> None: diff --git a/tests/drivers/test_lakeshore_372.py b/tests/drivers/test_lakeshore_372.py index 8a893f29784e..8a0f11a7e997 100644 --- a/tests/drivers/test_lakeshore_372.py +++ b/tests/drivers/test_lakeshore_372.py @@ -35,7 +35,7 @@ class MockVisaInstrument: def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.visa_log = get_instrument_logger(self, VISA_LOGGER) # type: ignore[arg-type] + self.visa_log = get_instrument_logger(self, VISA_LOGGER) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] # This base class mixin holds two dictionaries associated with the # pyvisa_instrument.write() @@ -73,7 +73,7 @@ def write_raw(self, cmd: str) -> None: self.visa_log.debug(f"Query: {cmd} for command {cmd_str} with args {args}") self.cmds[cmd_str](args) else: - super().write_raw(cmd) # type: ignore[misc] + super().write_raw(cmd) # type: ignore[misc] # ty: ignore[unresolved-attribute] def ask_raw(self, cmd: str) -> Any: query_parts = cmd.split(" ") @@ -87,12 +87,12 @@ def ask_raw(self, cmd: str) -> Any: self.visa_log.debug(f"Response: {response}") return response else: - return super().ask_raw(cmd) # type: ignore[misc] + return super().ask_raw(cmd) # type: ignore[misc] # ty: ignore[unresolved-attribute] def query[**P, T](name: str) -> Callable[[Callable[P, T]], Callable[P, T]]: def wrapper(func: Callable[P, T]) -> Callable[P, T]: - func.query_name = name.upper() # type: ignore[attr-defined] + func.query_name = name.upper() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] return func return wrapper @@ -100,7 +100,7 @@ def wrapper(func: Callable[P, T]) -> Callable[P, T]: def command[**P, T](name: str) -> Callable[[Callable[P, T]], Callable[P, T]]: def wrapper(func: Callable[P, T]) -> Callable[P, T]: - func.command_name = name.upper() # type: ignore[attr-defined] + func.command_name = name.upper() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] return func return wrapper diff --git a/tests/drivers/test_mercuryips.py b/tests/drivers/test_mercuryips.py index e63ecec31c30..45303998be43 100644 --- a/tests/drivers/test_mercuryips.py +++ b/tests/drivers/test_mercuryips.py @@ -96,7 +96,7 @@ def test_wrong_field_limit_raises() -> None: "mips", address="GPIB::1::INSTR", pyvisa_sim_file="MercuryiPS.yaml", - field_limits=0, # type: ignore[arg-type] + field_limits=0, # type: ignore[arg-type] # ty: ignore[invalid-argument-type] ) diff --git a/tests/drivers/test_signal_hound_usb_sa124b.py b/tests/drivers/test_signal_hound_usb_sa124b.py index 34ee5c44996f..f71985c66e44 100644 --- a/tests/drivers/test_signal_hound_usb_sa124b.py +++ b/tests/drivers/test_signal_hound_usb_sa124b.py @@ -75,7 +75,7 @@ def test_frequency_sweep_requires_signal_hound( ): FrequencySweep( "frequency_sweep", - instrument=not_a_signal_hound, # type: ignore[arg-type] + instrument=not_a_signal_hound, # type: ignore[arg-type] # ty: ignore[invalid-argument-type] sweep_len=10, start_freq=1e9, stepsize=1e6, diff --git a/tests/drivers/test_sr830.py b/tests/drivers/test_sr830.py index 31831a60c25a..680d2a23d86f 100644 --- a/tests/drivers/test_sr830.py +++ b/tests/drivers/test_sr830.py @@ -54,7 +54,7 @@ def test_channel_buffer_requires_sr830(not_an_sr830: DummyInstrument) -> None: ): ChannelBuffer( "ch1_databuffer", - instrument=not_an_sr830, # type: ignore[arg-type] + instrument=not_an_sr830, # type: ignore[arg-type] # ty: ignore[invalid-argument-type] channel=1, ) @@ -74,6 +74,6 @@ def test_buffer_parameters_reject_invalid_channel( with pytest.raises(ValueError, match=match): ChannelBuffer( f"ch{channel}_databuffer", - instrument=not_an_sr830, # type: ignore[arg-type] + instrument=not_an_sr830, # type: ignore[arg-type] # ty: ignore[invalid-argument-type] channel=channel, ) diff --git a/tests/drivers/test_tektronix_awg70000a.py b/tests/drivers/test_tektronix_awg70000a.py index 6248fa50eba5..5464b7ddb9ca 100644 --- a/tests/drivers/test_tektronix_awg70000a.py +++ b/tests/drivers/test_tektronix_awg70000a.py @@ -181,7 +181,7 @@ def test_seqxfilefromfs_failing(forged_sequence: dict[int, dict[str, Any]]) -> N forged_sequence, [1, 1, 1], seqname="dummyname", - channel_mapping={1: None, 3: None}, # type: ignore[dict-item] + channel_mapping={1: None, 3: None}, # type: ignore[dict-item] # ty: ignore[invalid-argument-type] ) # wrong channel mapping values diff --git a/tests/helpers/test_delegate_attribues.py b/tests/helpers/test_delegate_attribues.py index 69d54084cf10..300c4871054e 100644 --- a/tests/helpers/test_delegate_attribues.py +++ b/tests/helpers/test_delegate_attribues.py @@ -20,7 +20,7 @@ class ToDict(DelegateAttributes): assert td.apples == "green" d = {"apples": "red", "oranges": "orange"} - td.d = d # type: ignore[attr-defined] + td.d = d # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] # you can get the whole dict still assert td.d == d @@ -52,7 +52,7 @@ class ToDicts(DelegateAttributes): td = ToDicts() e = {"cats": 12, "dogs": 3} - td.e = e # type: ignore[attr-defined] + td.e = e # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] # you can still access the second one when the first doesn't exist with pytest.raises(AttributeError): @@ -61,7 +61,7 @@ class ToDicts(DelegateAttributes): assert td.cats == 12 # the first beats out the second - td.d = {"cats": 42, "chickens": 1000} # type: ignore[attr-defined] + td.d = {"cats": 42, "chickens": 1000} # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert td.cats == 42 # but you can still access things only in the second @@ -89,14 +89,14 @@ class ToObject(DelegateAttributes): _ = to_obj.recipient assert to_obj.gray == "#888" - to_obj.recipient = recipient # type: ignore[attr-defined] + to_obj.recipient = recipient # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] # now you can access recipient through to_obj assert to_obj.black == "#000" # to_obj overrides but you can still access other recipient attributes # "soft" black - to_obj.black = "#444" # type: ignore[attr-defined] + to_obj.black = "#444" # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert to_obj.black == "#444" assert to_obj.white == "#fff" diff --git a/tests/helpers/test_strip_attrs.py b/tests/helpers/test_strip_attrs.py index e032ed292f14..f8bf9fcff523 100644 --- a/tests/helpers/test_strip_attrs.py +++ b/tests/helpers/test_strip_attrs.py @@ -23,7 +23,7 @@ def __delitem__(self, item: Any) -> NoReturn: def test_normal() -> None: a = A() a.x = 15 - a.z = 25 # type: ignore[attr-defined] + a.z = 25 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] strip_attrs(a) @@ -38,13 +38,13 @@ def test_pathological() -> None: a = A() a.__dict__ = BadKeysDict() - a.fruit = "mango" # type: ignore[attr-defined] + a.fruit = "mango" # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] with pytest.raises(RuntimeError): a.__dict__.keys() strip_attrs(a) # no error, but the attribute is still there - assert a.fruit == "mango" # type: ignore[attr-defined] + assert a.fruit == "mango" # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] a = A() a.__dict__ = NoDelDict() diff --git a/tests/parameter/test_array_parameter.py b/tests/parameter/test_array_parameter.py index 710e94344fca..096e2e5cd1c1 100644 --- a/tests/parameter/test_array_parameter.py +++ b/tests/parameter/test_array_parameter.py @@ -167,13 +167,13 @@ def test_full_name() -> None: # this is not allowed since instrument # here is not actually an instrument # but useful for testing - p._instrument = instrument # type: ignore[assignment] + p._instrument = instrument # type: ignore[assignment] # ty: ignore[invalid-assignment] assert str(p) == "fred" assert p.setpoint_full_names == ("barney",) # and then an instrument that really has a name p = SimpleArrayParam([6, 7], "wilma", shape=(2,), setpoint_names=("betty",)) - p._instrument = named_instrument # type: ignore[assignment] + p._instrument = named_instrument # type: ignore[assignment] # ty: ignore[invalid-assignment] assert str(p) == "astro_wilma" assert p.setpoint_full_names == ("astro_betty",) @@ -181,7 +181,7 @@ def test_full_name() -> None: p = SimpleArrayParam( [[6, 7, 8], [1, 2, 3]], "wilma", shape=(3, 2), setpoint_names=("betty", None) ) - p._instrument = named_instrument # type: ignore[assignment] + p._instrument = named_instrument # type: ignore[assignment] # ty: ignore[invalid-assignment] assert p.setpoint_full_names == ("astro_betty", None) diff --git a/tests/parameter/test_delegate_parameter.py b/tests/parameter/test_delegate_parameter.py index 0fe8245d87aa..423850393480 100644 --- a/tests/parameter/test_delegate_parameter.py +++ b/tests/parameter/test_delegate_parameter.py @@ -84,7 +84,7 @@ def get_cmd() -> Any: get_cmd=get_cmd, ) param = cast("ObservableParam", p) - param.get_instr_val = get_cmd # type: ignore[method-assign] + param.get_instr_val = get_cmd # type: ignore[method-assign] # ty: ignore[invalid-assignment] return param yield make_parameter @@ -142,7 +142,7 @@ def test_get_set_raises(simple_param: Parameter) -> None: """ for kwargs in ({"set_cmd": None}, {"get_cmd": None}): with pytest.raises(KeyError) as e: - DelegateParameter("test_delegate_parameter", source=simple_param, **kwargs) # type: ignore[arg-type] + DelegateParameter("test_delegate_parameter", source=simple_param, **kwargs) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert str(e.value).startswith("'It is not allowed to set") @@ -320,7 +320,7 @@ def test_delegate_parameter_get_and_snapshot_with_none_source() -> None: assert delegate_param.get() is None assert delegate_param.snapshot()["value"] is None - parameter = delegate_param.cache._parameter # type: ignore[attr-defined] + parameter = delegate_param.cache._parameter # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert parameter.source.cache is none_param.cache delegate_param.source = source_param assert delegate_param.get() == 1 diff --git a/tests/parameter/test_elapsed_time_parameter.py b/tests/parameter/test_elapsed_time_parameter.py index 3add38e4515f..f5dc73174f3d 100644 --- a/tests/parameter/test_elapsed_time_parameter.py +++ b/tests/parameter/test_elapsed_time_parameter.py @@ -50,4 +50,4 @@ def test_elapsed_time_parameter_forbidden_kwargs() -> None: for fb_kwarg in forbidden_kwargs: match = f'Can not set "{fb_kwarg}" for an ElapsedTimeParameter' with pytest.raises(ValueError, match=match): - ElapsedTimeParameter("time", **{fb_kwarg: None}) # type: ignore[arg-type] + ElapsedTimeParameter("time", **{fb_kwarg: None}) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] diff --git a/tests/parameter/test_get_latest.py b/tests/parameter/test_get_latest.py index 5d9fb36c89f2..ad5c655254ff 100644 --- a/tests/parameter/test_get_latest.py +++ b/tests/parameter/test_get_latest.py @@ -64,8 +64,8 @@ def test_get_latest_unknown() -> None: local_parameter = BetterGettableParam("test_param", set_cmd=None, get_cmd=None) # fake a parameter that has a value but never been get/set to mock # an instrument. - local_parameter.cache._value = value # type: ignore[attr-defined] - local_parameter.cache._raw_value = value # type: ignore[attr-defined] + local_parameter.cache._value = value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] + local_parameter.cache._raw_value = value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert local_parameter.get_latest.get_timestamp() is None before_get = datetime.now(UTC) assert local_parameter._get_count == 0 @@ -169,7 +169,7 @@ def test_no_get_max_val_age() -> None: class LocalParameter(ParameterBase): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.set_raw = lambda x: x # type: ignore[method-assign] + self.set_raw = lambda x: x # type: ignore[method-assign] # ty: ignore[invalid-assignment] self.set = self._wrap_set(self.set_raw) localparameter = LocalParameter("test_param", instrument=None, max_val_age=1) diff --git a/tests/parameter/test_get_set_wrapping.py b/tests/parameter/test_get_set_wrapping.py index 83220d3a7d6a..def375cb45f6 100644 --- a/tests/parameter/test_get_set_wrapping.py +++ b/tests/parameter/test_get_set_wrapping.py @@ -69,7 +69,7 @@ def test_gettable_settable_attributes_with_get_set_raw( """Test that parameters that have get_raw,set_raw are listed as gettable/settable and reverse.""" - class GetSetParam(baseclass): # type: ignore[valid-type,misc] + class GetSetParam(baseclass): # type: ignore[valid-type,misc] # ty: ignore[unsupported-base] def __init__( self, *args: Any, initial_value: Any = None, **kwargs: Any ) -> None: diff --git a/tests/parameter/test_group_parameter.py b/tests/parameter/test_group_parameter.py index d6152dbc7fb5..b3a1f9c95417 100644 --- a/tests/parameter/test_group_parameter.py +++ b/tests/parameter/test_group_parameter.py @@ -112,7 +112,7 @@ def test_raise_on_get_set_cmd() -> None: kwarg = {arg: ""} with pytest.raises(ValueError) as e: - GroupParameter(name="a", **kwarg) # type: ignore[arg-type] + GroupParameter(name="a", **kwarg) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert ( str(e.value) == "A GroupParameter does not use 'set_cmd' or 'get_cmd' kwarg" diff --git a/tests/parameter/test_issequenceof.py b/tests/parameter/test_issequenceof.py index 2a16ac36e455..03bfa95c67e6 100644 --- a/tests/parameter/test_issequenceof.py +++ b/tests/parameter/test_issequenceof.py @@ -39,9 +39,9 @@ def test_simple_bad(args: tuple[Any, ...]) -> None: def test_examples_raises() -> None: with pytest.raises(TypeError): - is_sequence_of([1], 1) # type: ignore[arg-type] + is_sequence_of([1], 1) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises(TypeError): - is_sequence_of([1], (1, 2)) # type: ignore[arg-type] + is_sequence_of([1], (1, 2)) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] good_depth = [ diff --git a/tests/parameter/test_keyword_only_args.py b/tests/parameter/test_keyword_only_args.py index 3ae82f64cdb2..3927d1962b6c 100644 --- a/tests/parameter/test_keyword_only_args.py +++ b/tests/parameter/test_keyword_only_args.py @@ -42,7 +42,7 @@ def test_keyword_args_work(self) -> None: def test_positional_args_rejected(self) -> None: with pytest.raises(TypeError): - _ConcreteParameterBase("test", None) # type: ignore[misc] + _ConcreteParameterBase("test", None) # type: ignore[misc] # ty: ignore[too-many-positional-arguments] class TestParameterKeywordOnly: @@ -55,7 +55,7 @@ def test_keyword_args_work(self) -> None: def test_positional_args_rejected(self) -> None: with pytest.raises(TypeError): - Parameter("test", None, set_cmd=None) # type: ignore[misc] + Parameter("test", None, set_cmd=None) # type: ignore[misc] # ty: ignore[too-many-positional-arguments] # Minimal concrete subclass of ArrayParameter for testing @@ -74,11 +74,11 @@ def test_keyword_args_work(self) -> None: def test_positional_args_rejected(self) -> None: with pytest.raises(TypeError): - _ConcreteArrayParameter("test", (3,)) # type: ignore[misc] + _ConcreteArrayParameter("test", (3,)) # type: ignore[misc] # ty: ignore[missing-argument, too-many-positional-arguments] def test_missing_shape_raises(self) -> None: with pytest.raises(TypeError): - _ConcreteArrayParameter("test") # type: ignore[misc] + _ConcreteArrayParameter("test") # type: ignore[misc] # ty: ignore[missing-argument] class TestGroupParameterKeywordOnly: @@ -90,7 +90,7 @@ def test_keyword_args_work(self) -> None: def test_positional_args_rejected(self) -> None: with pytest.raises(TypeError): - GroupParameter("test", None) # type: ignore[misc] + GroupParameter("test", None) # type: ignore[misc] # ty: ignore[too-many-positional-arguments] def _make_delegate_group() -> DelegateGroup: @@ -112,11 +112,11 @@ def test_keyword_args_work(self) -> None: def test_positional_args_rejected(self) -> None: grp = _make_delegate_group() with pytest.raises(TypeError): - GroupedParameter("test", grp) # type: ignore[misc] + GroupedParameter("test", grp) # type: ignore[misc] # ty: ignore[missing-argument, too-many-positional-arguments] def test_missing_group_raises(self) -> None: with pytest.raises(TypeError): - GroupedParameter("test") # type: ignore[misc] + GroupedParameter("test") # type: ignore[misc] # ty: ignore[missing-argument] # Minimal concrete subclass of MultiParameter for testing @@ -135,15 +135,15 @@ def test_keyword_args_work(self) -> None: def test_positional_args_rejected(self) -> None: with pytest.raises(TypeError): - _ConcreteMultiParameter("test", ("a",), ((),)) # type: ignore[misc] + _ConcreteMultiParameter("test", ("a",), ((),)) # type: ignore[misc] # ty: ignore[missing-argument, too-many-positional-arguments] def test_missing_names_raises(self) -> None: with pytest.raises(TypeError): - _ConcreteMultiParameter("test", shapes=((),)) # type: ignore[misc] + _ConcreteMultiParameter("test", shapes=((),)) # type: ignore[misc] # ty: ignore[missing-argument] def test_missing_shapes_raises(self) -> None: with pytest.raises(TypeError): - _ConcreteMultiParameter("test", names=("a",)) # type: ignore[misc] + _ConcreteMultiParameter("test", names=("a",)) # type: ignore[misc] # ty: ignore[missing-argument] class TestMultiChannelInstrumentParameterKeywordOnly: @@ -167,13 +167,13 @@ def test_positional_args_rejected(self) -> None: def test_missing_channels_raises(self) -> None: with pytest.raises(TypeError): - MultiChannelInstrumentParameter( # type: ignore[call-arg] + MultiChannelInstrumentParameter( # type: ignore[call-arg] # ty: ignore[missing-argument] param_name="x", name="test", names=("a",), shapes=((),) ) def test_missing_param_name_raises(self) -> None: with pytest.raises(TypeError): - MultiChannelInstrumentParameter( # type: ignore[call-arg] + MultiChannelInstrumentParameter( # type: ignore[call-arg] # ty: ignore[missing-argument] channels=[], name="test", names=("a",), shapes=((),) ) @@ -188,7 +188,7 @@ def test_keyword_args_work(self) -> None: def test_positional_args_rejected(self) -> None: with pytest.raises(TypeError): - ElapsedTimeParameter("test", "My label") # type: ignore[misc] + ElapsedTimeParameter("test", "My label") # type: ignore[misc] # ty: ignore[too-many-positional-arguments] class TestInstrumentRefParameterKeywordOnly: @@ -201,4 +201,4 @@ def test_keyword_args_work(self) -> None: def test_positional_args_rejected(self) -> None: with pytest.raises(TypeError): - InstrumentRefParameter("test", None) # type: ignore[misc] + InstrumentRefParameter("test", None) # type: ignore[misc] # ty: ignore[too-many-positional-arguments] diff --git a/tests/parameter/test_manual_parameter.py b/tests/parameter/test_manual_parameter.py index f1e21c505435..c26d3da586aa 100644 --- a/tests/parameter/test_manual_parameter.py +++ b/tests/parameter/test_manual_parameter.py @@ -27,4 +27,4 @@ def test_manual_parameter_forbidden_kwargs() -> None: for fk in forbidden_kwargs: match = f'It is not allowed to set "{fk}" for a ManualParameter' with pytest.raises(ValueError, match=match): - ManualParameter("test", **{fk: None}) # type: ignore[arg-type] + ManualParameter("test", **{fk: None}) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] diff --git a/tests/parameter/test_multi_parameter.py b/tests/parameter/test_multi_parameter.py index 453e319872f5..502623e136c3 100644 --- a/tests/parameter/test_multi_parameter.py +++ b/tests/parameter/test_multi_parameter.py @@ -211,7 +211,7 @@ def test_full_name_s() -> None: shapes=shapes, setpoint_names=setpoint_names, ) - p._instrument = instrument # type: ignore[assignment] + p._instrument = instrument # type: ignore[assignment] # ty: ignore[invalid-assignment] assert str(p) == name assert p.full_names == names assert p.setpoint_full_names == ( @@ -228,7 +228,7 @@ def test_full_name_s() -> None: shapes=shapes, setpoint_names=setpoint_names, ) - p._instrument = named_instrument # type: ignore[assignment] + p._instrument = named_instrument # type: ignore[assignment] # ty: ignore[invalid-assignment] assert str(p) == "astro_mixed_dimensions" assert p.full_names == ("astro_0D", "astro_1D", "astro_2D") diff --git a/tests/parameter/test_on_off_mapping.py b/tests/parameter/test_on_off_mapping.py index 2bb4df4f8ede..a9839576cbe7 100644 --- a/tests/parameter/test_on_off_mapping.py +++ b/tests/parameter/test_on_off_mapping.py @@ -37,14 +37,14 @@ def test_create_on_off_val_mapping_for( # this does not type check. However, hash(1) == hash(True) # so 1/0 behaves like True and False at runtime - assert val_mapping[1] is on_val # type: ignore[index] + assert val_mapping[1] is on_val # type: ignore[index] # ty: ignore[invalid-argument-type] assert val_mapping[True] is on_val assert val_mapping["1"] is on_val assert val_mapping["ON"] is on_val assert val_mapping["On"] is on_val assert val_mapping["on"] is on_val - assert val_mapping[0] is off_val # type: ignore[index] + assert val_mapping[0] is off_val # type: ignore[index] # ty: ignore[invalid-argument-type] assert val_mapping[False] is off_val assert val_mapping["0"] is off_val assert val_mapping["OFF"] is off_val diff --git a/tests/parameter/test_parameter_basics.py b/tests/parameter/test_parameter_basics.py index 18a5446e1370..aa2356f049e3 100644 --- a/tests/parameter/test_parameter_basics.py +++ b/tests/parameter/test_parameter_basics.py @@ -15,7 +15,7 @@ def test_no_name() -> None: with pytest.raises(TypeError): - Parameter() # type: ignore[call-arg] + Parameter() # type: ignore[call-arg] # ty: ignore[missing-argument] def test_default_attributes() -> None: @@ -155,12 +155,12 @@ def test_str_representation() -> None: # three cases where only name gets used for full_name for instrument in blank_instruments: p = Parameter(name="fred") - p._instrument = instrument # type: ignore[assignment] + p._instrument = instrument # type: ignore[assignment] # ty: ignore[invalid-assignment] assert str(p) == "fred" # and finally an instrument that really has a name p = Parameter(name="wilma") - p._instrument = named_instrument # type: ignore[assignment] + p._instrument = named_instrument # type: ignore[assignment] # ty: ignore[invalid-assignment] assert str(p) == "astro_wilma" @@ -196,14 +196,14 @@ def test_parameter_call() -> None: assert p() == 1 with pytest.raises(TypeError, match="takes 1 positional argument but 2 were given"): - p(1, 2) # type: ignore[call-overload] + p(1, 2) # type: ignore[call-overload] # ty: ignore[no-matching-overload] p(value=2) assert p() == 2 with pytest.raises(TypeError, match="got multiple values for argument"): - p(2, value=2) # type: ignore[call-overload] + p(2, value=2) # type: ignore[call-overload] # ty: ignore[no-matching-overload] def test_parameter_set_extra_kwargs() -> None: @@ -243,13 +243,13 @@ def test_unknown_args_to_baseparameter_raises() -> None: _ = ParameterBase( name="Foo", instrument=None, - snapshotable=False, # type: ignore[call-arg] + snapshotable=False, # type: ignore[call-arg] # ty: ignore[unknown-argument] ) def test_underlying_instrument_for_virtual_parameter() -> None: p = GettableParam("base_param", vals=vals.Numbers()) - p._instrument = named_instrument # type: ignore[assignment] + p._instrument = named_instrument # type: ignore[assignment] # ty: ignore[invalid-assignment] vp = VirtualParameter("test_param", param=p) assert vp.underlying_instrument is named_instrument # type: ignore[comparison-overlap] diff --git a/tests/parameter/test_parameter_cache.py b/tests/parameter/test_parameter_cache.py index 68d5954be052..8628481e6ed9 100644 --- a/tests/parameter/test_parameter_cache.py +++ b/tests/parameter/test_parameter_cache.py @@ -100,8 +100,8 @@ def test_get_cache_unknown() -> None: local_parameter = BetterGettableParam("test_param", set_cmd=None, get_cmd=None) # fake a parameter that has a value but never been get/set to mock # an instrument. - local_parameter.cache._value = value # type: ignore[attr-defined] - local_parameter.cache._raw_value = value # type: ignore[attr-defined] + local_parameter.cache._value = value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] + local_parameter.cache._raw_value = value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert local_parameter.cache.timestamp is None before_get = datetime.now(UTC) assert local_parameter._get_count == 0 @@ -258,7 +258,7 @@ def test_no_get_max_val_age_runtime_error( class LocalParameter(ParameterBase): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.set_raw = lambda x: x # type: ignore[method-assign] + self.set_raw = lambda x: x # type: ignore[method-assign] # ty: ignore[invalid-assignment] self.set = self._wrap_set(self.set_raw) local_parameter = LocalParameter("test_param", instrument=None, max_val_age=1) @@ -298,13 +298,13 @@ def test_no_get_timestamp_none_runtime_error( def test_latest_dictionary_gets_updated_upon_set_of_memory_parameter() -> None: p = Parameter("p", set_cmd=None, get_cmd=None) - assert p.cache._value is None # type: ignore[attr-defined] + assert p.cache._value is None # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert p.cache.raw_value is None assert p.cache.timestamp is None p(42) - assert p.cache._value == 42 # type: ignore[attr-defined] + assert p.cache._value == 42 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert p.cache.raw_value == 42 assert p.cache.timestamp is not None @@ -350,7 +350,7 @@ def test_set_latest_works_for_plain_memory_parameter( assert p.raw_value == raw_value # Assert latest value and raw_value via private attributes for strictness - assert p.cache._value == value # type: ignore[attr-defined] + assert p.cache._value == value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert p.cache.raw_value == raw_value # Now let's get the value of the parameter to ensure that the value that @@ -371,7 +371,7 @@ def test_set_latest_works_for_plain_memory_parameter( assert p.raw_value == raw_value # Assert latest value and raw_value via private attributes for strictness - assert p.cache._value == value # type: ignore[attr-defined] + assert p.cache._value == value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert p.cache.raw_value == raw_value diff --git a/tests/parameter/test_parameter_context_manager.py b/tests/parameter/test_parameter_context_manager.py index f4cc7fa6f1b1..38b0c12230c6 100644 --- a/tests/parameter/test_parameter_context_manager.py +++ b/tests/parameter/test_parameter_context_manager.py @@ -137,7 +137,7 @@ def set_instr_value(value: Any) -> None: ) # pre-conditions - assert p.cache._value is None # type: ignore[attr-defined] + assert p.cache._value is None # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert p.cache.raw_value is None assert p.cache.timestamp is None assert set_counter == 0 diff --git a/tests/parameter/test_parameter_validation.py b/tests/parameter/test_parameter_validation.py index c25a69afc2cf..56058e6a6b43 100644 --- a/tests/parameter/test_parameter_validation.py +++ b/tests/parameter/test_parameter_validation.py @@ -39,7 +39,7 @@ def test_number_of_validations_for_set_cache() -> None: def test_bad_validator() -> None: with pytest.raises(TypeError): - Parameter("p", vals=[1, 2, 3]) # type:ignore[arg-type] + Parameter("p", vals=[1, 2, 3]) # type:ignore[arg-type] # ty: ignore[invalid-argument-type] def test_setting_int_with_float() -> None: diff --git a/tests/parameter/test_parameter_with_setpoints.py b/tests/parameter/test_parameter_with_setpoints.py index c16fe018d43c..1166dc833e0b 100644 --- a/tests/parameter/test_parameter_with_setpoints.py +++ b/tests/parameter/test_parameter_with_setpoints.py @@ -112,7 +112,7 @@ def test_setpoints_non_parameter_raises() -> None: param_with_setpoints_1 = ParameterWithSetpoints( "param_1", get_cmd=lambda: _rng.random(n_points_1()), - setpoints=(lambda x: x,), # type: ignore[arg-type] + setpoints=(lambda x: x,), # type: ignore[arg-type] # ty: ignore[invalid-argument-type] vals=vals.Arrays(shape=(n_points_1,)), ) @@ -123,7 +123,7 @@ def test_setpoints_non_parameter_raises() -> None: ) with pytest.raises(TypeError, match=err_msg): - param_with_setpoints_1.setpoints = (lambda x: x,) # type: ignore[assignment] + param_with_setpoints_1.setpoints = (lambda x: x,) # type: ignore[assignment] # ty: ignore[invalid-assignment] def test_validation_inconsistent_shape() -> None: @@ -331,7 +331,7 @@ def test_validation_one_dim_missing() -> None: "param_6", get_cmd=lambda: _rng.random(n_points_1()), setpoints=(setpoints_1,), - vals=vals.Arrays(shape=(n_points_1, None)), # type: ignore[arg-type] + vals=vals.Arrays(shape=(n_points_1, None)), # type: ignore[arg-type] # ty: ignore[invalid-argument-type] ) expected_err_msg = ( r"One or more dimensions have unknown shape " @@ -358,7 +358,7 @@ def test_validation_one_sp_dim_missing() -> None: setpoints_1 = Parameter( "setpoints_1", get_cmd=lambda: _rng.random(n_points_1()), - vals=vals.Arrays(shape=(n_points_1, None)), # type: ignore[arg-type] + vals=vals.Arrays(shape=(n_points_1, None)), # type: ignore[arg-type] # ty: ignore[invalid-argument-type] ) param_sp_without_shape = ParameterWithSetpoints( "param_6", diff --git a/tests/parameter/test_scaled_parameter.py b/tests/parameter/test_scaled_parameter.py index fb2357ce4063..9b5d480abd8a 100644 --- a/tests/parameter/test_scaled_parameter.py +++ b/tests/parameter/test_scaled_parameter.py @@ -24,7 +24,7 @@ def _make_instrument() -> "Generator[DummyInstrument, None, None]": get_cmd=None, set_cmd=None, ) - instrument.scaler = ScaledParameter( # type: ignore[attr-defined] + instrument.scaler = ScaledParameter( # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] instrument.target_parameter, division=1 ) yield instrument @@ -37,7 +37,7 @@ def test_constructor(instrument: DummyInstrument) -> None: # Require a wrapped parameter with pytest.raises(TypeError): - ScaledParameter() # type: ignore[call-arg] + ScaledParameter() # type: ignore[call-arg] # ty: ignore[missing-argument] # Require a scaling factor with pytest.raises(ValueError): diff --git a/tests/parameter/test_snapshot.py b/tests/parameter/test_snapshot.py index 790977573231..573de8e5b4da 100644 --- a/tests/parameter/test_snapshot.py +++ b/tests/parameter/test_snapshot.py @@ -56,13 +56,13 @@ def wrapped_func(*args: P.args, **kwargs: P.kwargs) -> T: call_count += 1 return get_func(*args, **kwargs) - wrapped_func.call_count = lambda: call_count # type: ignore[attr-defined] + wrapped_func.call_count = lambda: call_count # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] return wrapped_func p.get = wrap_in_call_counter(p.get) # pre-condition - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] else: # pre-condition assert not hasattr(p, "get") @@ -159,7 +159,7 @@ def test_snapshot_timestamp_for_valid_cache_depends_on_cache_update( # Hack cache's timestamp to simplify this test timestamp = p.cache.timestamp assert timestamp is not None - p.cache._timestamp = timestamp - timedelta(days=31) # type: ignore[attr-defined] + p.cache._timestamp = timestamp - timedelta(days=31) # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] tu = datetime.now(UTC) assert p.cache.timestamp is not None @@ -240,7 +240,7 @@ def test_snapshot_when_snapshot_value_is_false( assert "raw_value" not in s if get_cmd is not False: - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def test_snapshot_value_is_true_by_default( @@ -295,7 +295,7 @@ def test_snapshot_when_snapshot_get_is_false( assert s["raw_value"] is None if get_cmd is not False: - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def test_snapshot_of_non_gettable_parameter_mirrors_cache( @@ -346,15 +346,15 @@ def test_snapshot_of_gettable_parameter_depends_on_update( if should_get: assert s["value"] == 65 assert s["raw_value"] == 69 - assert p.get.call_count() == 1 # type: ignore[attr-defined] + assert p.get.call_count() == 1 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] elif cache_is_valid: assert s["value"] == 42 assert s["raw_value"] == 46 - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] else: assert s["value"] is None assert s["raw_value"] is None - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def test_snapshot_value() -> None: @@ -389,7 +389,7 @@ def test_normalize_snapshot_update_maps_to_canonical_values() -> None: def test_normalize_snapshot_update_rejects_unknown_string() -> None: with pytest.raises(ValueError, match="Invalid value for snapshot"): - normalize_snapshot_update("bogus") # type: ignore[arg-type] + normalize_snapshot_update("bogus") # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_snapshot_update_all_always_calls_get() -> None: @@ -401,7 +401,7 @@ def test_snapshot_update_all_always_calls_get() -> None: ) s = p.snapshot(update="All") assert s["value"] == 69 - assert p.get.call_count() == 1 # type: ignore[attr-defined] + assert p.get.call_count() == 1 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def test_snapshot_update_never_never_calls_get() -> None: @@ -413,7 +413,7 @@ def test_snapshot_update_never_never_calls_get() -> None: ) s = p.snapshot(update="Never") assert s["value"] is None - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def test_snapshot_update_only_invalid_calls_get_when_cache_invalid() -> None: @@ -425,7 +425,7 @@ def test_snapshot_update_only_invalid_calls_get_when_cache_invalid() -> None: ) s = p.snapshot(update="Only_invalid") assert s["value"] == 69 - assert p.get.call_count() == 1 # type: ignore[attr-defined] + assert p.get.call_count() == 1 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def test_snapshot_update_only_invalid_skips_get_when_cache_valid() -> None: @@ -438,7 +438,7 @@ def test_snapshot_update_only_invalid_skips_get_when_cache_valid() -> None: s = p.snapshot(update="Only_invalid") # the cached (set) value is used, ``get`` is not called assert s["value"] == 42 - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] @pytest.mark.parametrize( @@ -471,8 +471,8 @@ def test_snapshot_update_string_matches_legacy_value( assert s_legacy["value"] == s_string["value"] assert s_legacy["raw_value"] == s_string["raw_value"] assert ( - p_legacy.get.call_count() # type: ignore[attr-defined] - == p_string.get.call_count() # type: ignore[attr-defined] + p_legacy.get.call_count() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] + == p_string.get.call_count() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] ) @@ -484,4 +484,4 @@ def test_snapshot_rejects_unknown_update_value() -> None: cache_is_valid=True, ) with pytest.raises(ValueError, match="Invalid value for snapshot"): - p.snapshot(update="bogus") # type: ignore[arg-type] + p.snapshot(update="bogus") # type: ignore[arg-type] # ty: ignore[no-matching-overload] diff --git a/tests/test_channels.py b/tests/test_channels.py index 0537146357a9..041ebcf20435 100644 --- a/tests/test_channels.py +++ b/tests/test_channels.py @@ -157,7 +157,7 @@ def test_invalid_channel_type_raises(empty_instrument: Instrument) -> None: ChannelList( parent=empty_instrument, name="empty", - chan_type=int, # type: ignore[type-var] + chan_type=int, # type: ignore[type-var] # ty: ignore[invalid-argument-type] ) @@ -167,7 +167,7 @@ def test_invalid_multichan_type_raises(empty_instrument: Instrument) -> None: parent=empty_instrument, name="empty", chan_type=DummyChannel, - multichan_paramclass=int, # type: ignore[arg-type] + multichan_paramclass=int, # type: ignore[arg-type] # ty: ignore[invalid-argument-type] ) @@ -736,7 +736,7 @@ def test_channel_tuple_call_method_called_as_expected( assert result is None for channel in dci.channels: # type inference ignores that this has been mocked - channel.turn_on.assert_called_with("bar") # type: ignore[union-attr] + channel.turn_on.assert_called_with("bar") # type: ignore[union-attr] # ty: ignore[unresolved-attribute] def test_channel_tuple_names(dci: DummyChannelInstrument) -> None: @@ -810,7 +810,7 @@ def test_multi_function_with_callable_method( result = multi_func("bar") assert result is None for channel in dci.channels: - channel.turn_on.assert_called_with("bar") # type: ignore[union-attr] + channel.turn_on.assert_called_with("bar") # type: ignore[union-attr] # ty: ignore[unresolved-attribute] def test_multi_function_invalid_name_raises(dci: DummyChannelInstrument) -> None: diff --git a/tests/test_command.py b/tests/test_command.py index 7b9dbc34bfa4..4f662685258f 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -11,25 +11,25 @@ class CustomError(Exception): def test_bad_calls() -> None: with pytest.raises(TypeError): - Command() # type: ignore[call-arg] + Command() # type: ignore[call-arg] # ty: ignore[missing-argument] with pytest.raises(TypeError): - Command(cmd="") # type: ignore[call-arg] + Command(cmd="") # type: ignore[call-arg] # ty: ignore[missing-argument] with pytest.raises(TypeError): - Command(0, "", output_parser=lambda: 1) # type: ignore[arg-type, misc] + Command(0, "", output_parser=lambda: 1) # type: ignore[arg-type, misc] # ty: ignore[invalid-argument-type] with pytest.raises(TypeError): Command(1, "", input_parser=lambda: 1) with pytest.raises(TypeError): - Command(0, cmd="", exec_str="not a function") # type: ignore[arg-type] + Command(0, cmd="", exec_str="not a function") # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises(TypeError): Command( 0, cmd=lambda: 1, - no_cmd_function="not a function", # type: ignore[arg-type] + no_cmd_function="not a function", # type: ignore[arg-type] # ty: ignore[invalid-argument-type] ) diff --git a/tests/test_import.py b/tests/test_import.py index 73d312b0720f..b40392163fb6 100644 --- a/tests/test_import.py +++ b/tests/test_import.py @@ -32,7 +32,7 @@ def test_top_level_parameter_shorthand_deprecated() -> None: with pytest.warns( QCoDeSDeprecationWarning, match=r"top level.*'qcodes\.parameters'" ): - obj = qcodes.Parameter # type: ignore[attr-defined] + obj = qcodes.Parameter # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert obj is qcodes.parameters.Parameter @@ -40,13 +40,13 @@ def test_top_level_combine_shorthand_deprecated() -> None: with pytest.warns( QCoDeSDeprecationWarning, match=r"top level.*'qcodes\.parameters'" ): - obj = qcodes.combine # type: ignore[attr-defined] + obj = qcodes.combine # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert obj is qcodes.parameters.combine def test_top_level_measurement_shorthand_deprecated() -> None: with pytest.warns(QCoDeSDeprecationWarning, match=r"top level.*'qcodes\.dataset'"): - obj = qcodes.Measurement # type: ignore[attr-defined] + obj = qcodes.Measurement # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert obj is qcodes.dataset.Measurement @@ -54,19 +54,19 @@ def test_top_level_instrument_shorthand_deprecated() -> None: with pytest.warns( QCoDeSDeprecationWarning, match=r"top level.*'qcodes\.instrument'" ): - obj = qcodes.Instrument # type: ignore[attr-defined] + obj = qcodes.Instrument # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert obj is qcodes.instrument.Instrument def test_top_level_monitor_shorthand_deprecated() -> None: with pytest.warns(QCoDeSDeprecationWarning, match=r"top level.*'qcodes\.monitor'"): - obj = qcodes.Monitor # type: ignore[attr-defined] + obj = qcodes.Monitor # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert obj is qcodes.monitor.Monitor def test_top_level_station_shorthand_deprecated() -> None: with pytest.warns(QCoDeSDeprecationWarning, match=r"top level.*'qcodes\.station'"): - obj = qcodes.Station # type: ignore[attr-defined] + obj = qcodes.Station # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert obj is qcodes.station.Station @@ -85,7 +85,7 @@ def test_top_level_submodule_access_not_deprecated() -> None: def test_top_level_unknown_attribute_raises() -> None: with pytest.raises(AttributeError, match="definitely_not_a_qcodes_attribute"): - _ = qcodes.definitely_not_a_qcodes_attribute # type: ignore[attr-defined] + _ = qcodes.definitely_not_a_qcodes_attribute # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def test_instrument_parameter_reexport_deprecated() -> None: @@ -94,7 +94,7 @@ def test_instrument_parameter_reexport_deprecated() -> None: with pytest.warns( QCoDeSDeprecationWarning, match=r"'ManualParameter'.*'qcodes\.parameters'" ): - obj = qcodes.instrument.ManualParameter # type: ignore[attr-defined] + obj = qcodes.instrument.ManualParameter # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert obj is qcodes.parameters.ManualParameter @@ -127,4 +127,4 @@ def test_instrument_parameter_reexport_deprecated_all(name: str) -> None: def test_instrument_unknown_attribute_raises() -> None: with pytest.raises(AttributeError, match="definitely_not_a_qcodes_attribute"): - _ = qcodes.instrument.definitely_not_a_qcodes_attribute # type: ignore[attr-defined] + _ = qcodes.instrument.definitely_not_a_qcodes_attribute # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] diff --git a/tests/test_instrument.py b/tests/test_instrument.py index 0d2c1248c2f4..4c39daaac62d 100644 --- a/tests/test_instrument.py +++ b/tests/test_instrument.py @@ -450,7 +450,7 @@ def test_other_exception() -> None: # in order to raise an unexpected exception, and make sure it is # passed through the call stack, let's pass an empty dict instead # of a string with instrument name - _ = find_or_create_instrument(DummyInstrument, {}) # type: ignore[arg-type] + _ = find_or_create_instrument(DummyInstrument, {}) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] @pytest.mark.usefixtures("close_before_and_after") @@ -564,7 +564,7 @@ def test_close_all_no_log_by_default(caplog: pytest.LogCaptureFixture) -> None: def test_close_all_only_accepts_keyword_arguments() -> None: """The ``close_all`` options are keyword-only.""" with pytest.raises(TypeError): - Instrument.close_all(True) # type: ignore[misc] + Instrument.close_all(True) # type: ignore[misc] # ty: ignore[too-many-positional-arguments] class _RegistryCollectingGarbageWhenIterated(WeakValueDictionary[str, Instrument]): diff --git a/tests/test_logger.py b/tests/test_logger.py index dfa00e32dbca..81d3c3c3b1ea 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -372,7 +372,7 @@ def test_installation_info_logging() -> None: def test_get_level_name_with_invalid_type() -> None: """Only str and int can be converted to a logging level name.""" with pytest.raises(RuntimeError, match="get_level_name: Cannot to convert level"): - logger.get_level_name(1.5) # type: ignore[arg-type] + logger.get_level_name(1.5) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_get_level_code_with_invalid_type() -> None: @@ -661,7 +661,7 @@ def test_submodule_logger_is_not_replaced_without_instrument_scope() -> None: def test_nested_submodule_logger_is_child_of_its_parent_module() -> None: inst = ScopedDummyChannelInstrument("nested_modules") channel = inst.submodules["A"] - nested = InstrumentModule(channel, "nested") # type: ignore[arg-type] + nested = InstrumentModule(channel, "nested") # type: ignore[arg-type] # ty: ignore[invalid-argument-type] channel.add_submodule("nested", nested) # type: ignore[union-attr] assert nested.name_parts == ["nested_modules", "ChanA", "nested"] diff --git a/tests/test_metadata.py b/tests/test_metadata.py index b608d7bd2b05..2071cfc98343 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -25,7 +25,7 @@ def snapshot_base( class HasSnapshot(Metadatable): # Users shouldn't do this... but we'll test its behavior # for completeness - def snapshot(self, update: bool | None = False) -> Snapshot: # type: ignore[misc] + def snapshot(self, update: bool | None = False) -> Snapshot: # type: ignore[misc] # ty: ignore[invalid-method-override, override-of-final-method] return {"fruit": "kiwi"} @@ -64,7 +64,7 @@ def test_load() -> None: def test_init() -> None: with pytest.raises(TypeError): - Metadatable(metadata={"2": 3}, not_metadata={4: 5}) # type: ignore[call-arg] + Metadatable(metadata={"2": 3}, not_metadata={4: 5}) # type: ignore[call-arg] # ty: ignore[unknown-argument] m = Metadatable(metadata={"2": 3}) assert m.metadata == {"2": 3} diff --git a/tests/test_station.py b/tests/test_station.py index 0c633b325f08..8ae4590889ea 100644 --- a/tests/test_station.py +++ b/tests/test_station.py @@ -118,13 +118,13 @@ def test_add_component_with_no_name() -> None: """ bob = {"name", "bob"} station = Station() - station.add_component(bob) # type: ignore[arg-type] + station.add_component(bob) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert ["component0"] == list(station.components.keys()) assert bob == station.components["component0"] # type: ignore[comparison-overlap] jay = {"name", "jay"} - station.add_component(jay) # type: ignore[arg-type] + station.add_component(jay) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert ["component0", "component1"] == list(station.components.keys()) assert jay == station.components["component1"] # type: ignore[comparison-overlap] diff --git a/tests/utils/test_class_strings.py b/tests/utils/test_class_strings.py index 4badd3ca4d28..d059d3aa5e21 100644 --- a/tests/utils/test_class_strings.py +++ b/tests/utils/test_class_strings.py @@ -12,5 +12,5 @@ def test_full_class() -> None: def test_named_repr() -> None: j = json.JSONEncoder() id_ = id(j) - j.name = "Peppa" # type: ignore[attr-defined] + j.name = "Peppa" # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert named_repr(j) == f"" diff --git a/tests/utils/test_isfunction.py b/tests/utils/test_isfunction.py index 4088c7986195..9af685caf73e 100644 --- a/tests/utils/test_isfunction.py +++ b/tests/utils/test_isfunction.py @@ -36,7 +36,7 @@ def f2(a: object, b: object) -> NoReturn: # make sure we only accept valid arg_count with pytest.raises(TypeError): - is_function(f0, "lots") # type: ignore[arg-type] + is_function(f0, "lots") # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises(TypeError): is_function(f0, -1) diff --git a/tests/validators/test_arrays.py b/tests/validators/test_arrays.py index 02a0549f6709..607b612fe95c 100644 --- a/tests/validators/test_arrays.py +++ b/tests/validators/test_arrays.py @@ -18,7 +18,7 @@ def test_type() -> None: m = Arrays(min_value=0.0, max_value=3.2, shape=(2, 2)) for v in ["somestring", 4, 2, [[2, 0], [1, 2]]]: with pytest.raises(TypeError): - m.validate(v) # type: ignore[arg-type] + m.validate(v) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_complex_min_max_raises() -> None: @@ -31,14 +31,14 @@ def test_complex_min_max_raises() -> None: r" It is \(1\+1j\) of type " r"", ): - Arrays(min_value=1 + 1j) # type: ignore[arg-type] + Arrays(min_value=1 + 1j) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises( TypeError, match=r"max_value must be a real number. " r"It is \(1\+1j\) of type " r"", ): - Arrays(max_value=1 + 1j) # type: ignore[arg-type] + Arrays(max_value=1 + 1j) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises( TypeError, match=r"Setting min_value or max_value is " @@ -225,9 +225,9 @@ def test_valid_values() -> None: def test_shape_non_sequence_raises() -> None: with pytest.raises(ValueError): - _ = Arrays(shape=5) # type: ignore[arg-type] + _ = Arrays(shape=5) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises(ValueError): - _ = Arrays(shape=lambda: 10) # type: ignore[arg-type] + _ = Arrays(shape=lambda: 10) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_repr() -> None: diff --git a/tests/validators/test_basic.py b/tests/validators/test_basic.py index ee606a4205ac..763e3d88ded5 100644 --- a/tests/validators/test_basic.py +++ b/tests/validators/test_basic.py @@ -55,7 +55,7 @@ def test_real_anything() -> None: def test_failed_anything() -> None: with pytest.raises(TypeError): - Anything(1) # type: ignore[call-arg] + Anything(1) # type: ignore[call-arg] # ty: ignore[too-many-positional-arguments] with pytest.raises(TypeError): - Anything(values=[1, 2, 3]) # type: ignore[call-arg] + Anything(values=[1, 2, 3]) # type: ignore[call-arg] # ty: ignore[unknown-argument] diff --git a/tests/validators/test_bool.py b/tests/validators/test_bool.py index f41da91ee6aa..3ec9739f615e 100644 --- a/tests/validators/test_bool.py +++ b/tests/validators/test_bool.py @@ -54,7 +54,7 @@ def test_bool() -> None: for vv in NOTBOOLS: with pytest.raises(TypeError): - b.validate(vv) # type: ignore[arg-type] + b.validate(vv) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert repr(b) == "" diff --git a/tests/validators/test_callable.py b/tests/validators/test_callable.py index 02a75a59cb6d..8a43ae2234eb 100644 --- a/tests/validators/test_callable.py +++ b/tests/validators/test_callable.py @@ -12,7 +12,7 @@ def test_func() -> bool: c.validate(test_func) test_int = 5 with pytest.raises(TypeError): - c.validate(test_int) # type: ignore[arg-type] + c.validate(test_int) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_valid_values() -> None: diff --git a/tests/validators/test_complex.py b/tests/validators/test_complex.py index 2f0f9e45544d..ff1e4995108c 100644 --- a/tests/validators/test_complex.py +++ b/tests/validators/test_complex.py @@ -28,4 +28,4 @@ def test_complex_raises(val: float | str) -> None: n = ComplexNumbers() with pytest.raises(TypeError, match=r"is not complex;"): - n.validate(val) # type: ignore[arg-type] + n.validate(val) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] diff --git a/tests/validators/test_dict.py b/tests/validators/test_dict.py index 76746374ba76..e3cca0b67331 100644 --- a/tests/validators/test_dict.py +++ b/tests/validators/test_dict.py @@ -13,7 +13,7 @@ def test_dict() -> None: d.validate(my_dict) my_int = 5 with pytest.raises(TypeError): - d.validate(my_int) # type: ignore[arg-type] + d.validate(my_int) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_valid_values() -> None: diff --git a/tests/validators/test_enum.py b/tests/validators/test_enum.py index 72628bbc0436..9496f63789d0 100644 --- a/tests/validators/test_enum.py +++ b/tests/validators/test_enum.py @@ -37,7 +37,7 @@ def test_good() -> None: def test_bad() -> None: for enum in not_enums: with pytest.raises(TypeError): - vals.Enum(*enum) # type: ignore[arg-type] + vals.Enum(*enum) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_valid_values() -> None: diff --git a/tests/validators/test_ints.py b/tests/validators/test_ints.py index 92450f80baa4..22f2ec76dec0 100644 --- a/tests/validators/test_ints.py +++ b/tests/validators/test_ints.py @@ -91,7 +91,7 @@ def test_unlimited() -> None: for vv in not_ints: with pytest.raises(TypeError): - n.validate(vv) # type: ignore[arg-type] + n.validate(vv) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_min() -> None: @@ -109,7 +109,7 @@ def test_min_raises() -> None: n = Ints(min_value=ints[-1]) for v in not_ints: with pytest.raises(TypeError): - n.validate(v) # type: ignore[arg-type] + n.validate(v) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_max() -> None: @@ -127,7 +127,7 @@ def test_max_raises() -> None: n = Ints(max_value=ints[-1]) for v in not_ints: with pytest.raises(TypeError): - n.validate(v) # type: ignore[arg-type] + n.validate(v) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_range() -> None: @@ -142,7 +142,7 @@ def test_range() -> None: for vv in not_ints: with pytest.raises(TypeError): - n.validate(vv) # type: ignore[arg-type] + n.validate(vv) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert repr(n) == "" assert n.is_numeric @@ -150,17 +150,17 @@ def test_range() -> None: def test_failed_numbers() -> None: with pytest.raises(TypeError): - Ints(1, 2, 3) # type: ignore[call-arg] + Ints(1, 2, 3) # type: ignore[call-arg] # ty: ignore[too-many-positional-arguments] with pytest.raises(TypeError): Ints(1, 1) # min >= max for val in not_ints: with pytest.raises((TypeError, OverflowError)): - Ints(max_value=val) # type: ignore[arg-type] + Ints(max_value=val) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises((TypeError, OverflowError)): - Ints(min_value=val) # type: ignore[arg-type] + Ints(min_value=val) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_valid_values() -> None: diff --git a/tests/validators/test_lists.py b/tests/validators/test_lists.py index 92eb8311fa37..722e237facb5 100644 --- a/tests/validators/test_lists.py +++ b/tests/validators/test_lists.py @@ -15,7 +15,7 @@ def test_type() -> None: v2 = 234 with pytest.raises(TypeError): - list_validator.validate(v2) # type: ignore[arg-type] + list_validator.validate(v2) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_elt_vals() -> None: @@ -25,7 +25,7 @@ def test_elt_vals() -> None: v2 = [0, 1, 11] with pytest.raises(ValueError): - list_validator.validate(v2) # type: ignore[arg-type] + list_validator.validate(v2) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_valid_values() -> None: diff --git a/tests/validators/test_multi_type.py b/tests/validators/test_multi_type.py index 334f2a6922c4..a48a1f34673e 100644 --- a/tests/validators/test_multi_type.py +++ b/tests/validators/test_multi_type.py @@ -87,15 +87,15 @@ def test_bad() -> None: # combiner == 'OR' for args in [[], [1], [Strings(), True]]: with pytest.raises(TypeError): - MultiType(*args) # type: ignore[misc] + MultiType(*args) # type: ignore[misc] # ty: ignore[invalid-argument-type] # combiner == 'OR' for args in [[], [1], [Strings(), True]]: with pytest.raises(TypeError): - MultiType(*args, combiner="OR") # type: ignore[misc] + MultiType(*args, combiner="OR") # type: ignore[misc] # ty: ignore[invalid-argument-type] # combiner == 'AND' for args in [[], [1], [Strings(), True]]: with pytest.raises(TypeError): - MultiType(*args, combiner="AND") # type: ignore[misc] + MultiType(*args, combiner="AND") # type: ignore[misc] # ty: ignore[invalid-argument-type] def test_valid_values() -> None: diff --git a/tests/validators/test_multi_type_and.py b/tests/validators/test_multi_type_and.py index 8d323ea1453a..b943ed723344 100644 --- a/tests/validators/test_multi_type_and.py +++ b/tests/validators/test_multi_type_and.py @@ -44,4 +44,4 @@ def test_good() -> None: def test_bad() -> None: for args in ([], [1], [Strings(), True]): with pytest.raises(TypeError): - MultiTypeAnd(*args) # type: ignore[misc] + MultiTypeAnd(*args) # type: ignore[misc] # ty: ignore[invalid-argument-type] diff --git a/tests/validators/test_multi_type_or.py b/tests/validators/test_multi_type_or.py index 52ac0c1ced7d..c21c9b2b453f 100644 --- a/tests/validators/test_multi_type_or.py +++ b/tests/validators/test_multi_type_or.py @@ -38,7 +38,7 @@ def test_good() -> None: def test_bad() -> None: for args in ([], [1], [Strings(), True]): with pytest.raises(TypeError): - MultiTypeOr(*args) # type: ignore[misc] + MultiTypeOr(*args) # type: ignore[misc] # ty: ignore[invalid-argument-type] def test_valid_values() -> None: diff --git a/tests/validators/test_multiples.py b/tests/validators/test_multiples.py index cf3f894ca9ed..6a79f5b37056 100644 --- a/tests/validators/test_multiples.py +++ b/tests/validators/test_multiples.py @@ -108,11 +108,11 @@ def test_divisors() -> None: for vvv in not_multiples: with pytest.raises(TypeError): - n.validate(vvv) # type:ignore[arg-type] + n.validate(vvv) # type:ignore[arg-type] # ty: ignore[invalid-argument-type] for dd in not_divisors: with pytest.raises(TypeError): - n = Multiples(divisor=dd) # type:ignore[arg-type] + n = Multiples(divisor=dd) # type:ignore[arg-type] # ty: ignore[invalid-argument-type] n = Multiples(divisor=3, min_value=1, max_value=10) assert repr(n) == "" diff --git a/tests/validators/test_numbers.py b/tests/validators/test_numbers.py index 79a1acf1929f..0aca7efd874b 100644 --- a/tests/validators/test_numbers.py +++ b/tests/validators/test_numbers.py @@ -139,7 +139,7 @@ def test_range() -> None: def test_failed_numbers() -> None: with pytest.raises(TypeError): - Numbers(1, 2, 3) # type: ignore[call-arg] + Numbers(1, 2, 3) # type: ignore[call-arg] # ty: ignore[too-many-positional-arguments] with pytest.raises(TypeError): Numbers(1, 1) # min >= max diff --git a/tests/validators/test_sequence.py b/tests/validators/test_sequence.py index 269460565dd8..97d47092c567 100644 --- a/tests/validators/test_sequence.py +++ b/tests/validators/test_sequence.py @@ -12,7 +12,7 @@ def test_type() -> None: v2 = 234 with pytest.raises(TypeError): - sequence_validator.validate(v2) # type: ignore[arg-type] + sequence_validator.validate(v2) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_elt_vals() -> None: diff --git a/tests/validators/test_string.py b/tests/validators/test_string.py index f196afaf6f42..d7e0186110fd 100644 --- a/tests/validators/test_string.py +++ b/tests/validators/test_string.py @@ -49,7 +49,7 @@ def test_unlimited() -> None: for vv in not_strings: with pytest.raises(TypeError): - s.validate(vv) # type: ignore[arg-type] + s.validate(vv) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert repr(s) == "" @@ -66,7 +66,7 @@ def test_min() -> None: for vv in not_strings: with pytest.raises(TypeError): - s.validate(vv) # type: ignore[arg-type] + s.validate(vv) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] s = Strings(min_length=100) assert repr(s) == "=100>" @@ -85,7 +85,7 @@ def test_max() -> None: s = Strings(max_length=100) for vv in not_strings: with pytest.raises(TypeError): - s.validate(vv) # type: ignore[arg-type] + s.validate(vv) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert repr(s) == "" @@ -102,7 +102,7 @@ def test_range() -> None: for vv in not_strings: with pytest.raises(TypeError): - s.validate(vv) # type: ignore[arg-type] + s.validate(vv) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert repr(s) == "" @@ -112,7 +112,7 @@ def test_range() -> None: def test_failed_strings() -> None: with pytest.raises(TypeError): - Strings(1, 2, 3) # type: ignore[call-arg] + Strings(1, 2, 3) # type: ignore[call-arg] # ty: ignore[too-many-positional-arguments] with pytest.raises(TypeError): Strings(10, 9) @@ -121,14 +121,14 @@ def test_failed_strings() -> None: Strings(max_length=0) with pytest.raises(TypeError): - Strings(min_length=1e12) # type: ignore[arg-type] + Strings(min_length=1e12) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] for length in [-1, 3.5, "2", None]: with pytest.raises(TypeError): - Strings(max_length=length) # type: ignore[arg-type] + Strings(max_length=length) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises(TypeError): - Strings(min_length=length) # type: ignore[arg-type] + Strings(min_length=length) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_valid_values() -> None: From 433128192159c2aa43e354cd98ac88d4b875084e Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:16:01 +0200 Subject: [PATCH 52/70] Suppress ty where pyright is already suppressed These lines deliberately pass invalid arguments or subscripts, or mock attributes that are not declared on the annotated type, to check the runtime error handling. They are already suppressed for pyright. --- .../test_measurement_context_manager.py | 2 +- tests/dataset/test_dataset_export.py | 8 ++++---- tests/dataset/test_measurement_extensions.py | 6 +++--- tests/dataset/test_string_data.py | 2 +- .../b1500_driver_tests/test_b1500.py | 14 +++++++------- .../b1500_driver_tests/test_b1517a_smu.py | 6 +++--- .../test_sampling_measurement.py | 2 +- tests/parameter/test_keyword_only_args.py | 4 ++-- tests/parameter/test_snapshot.py | 2 +- .../sphinx_extension/test_parse_parameter_attr.py | 2 +- tests/test_channels.py | 12 ++++++------ tests/validators/test_enum.py | 2 +- tests/validators/test_literal.py | 4 ++-- 13 files changed, 33 insertions(+), 33 deletions(-) diff --git a/tests/dataset/measurement/test_measurement_context_manager.py b/tests/dataset/measurement/test_measurement_context_manager.py index 92481999e18c..51815dc9e649 100644 --- a/tests/dataset/measurement/test_measurement_context_manager.py +++ b/tests/dataset/measurement/test_measurement_context_manager.py @@ -310,7 +310,7 @@ def test_unregister_parameter(DAC: DummyInstrument, DMM: DummyInstrument) -> Non not_parameters = [DAC, DMM, 0.0, 1] for notparam in not_parameters: with pytest.raises(ValueError): - meas.unregister_parameter(notparam) # pyright: ignore[reportArgumentType] + meas.unregister_parameter(notparam) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] # unregistering something not registered should silently "succeed" meas.unregister_parameter("totes_not_registered") diff --git a/tests/dataset/test_dataset_export.py b/tests/dataset/test_dataset_export.py index aad6be991ab1..5c8fcf451e07 100644 --- a/tests/dataset/test_dataset_export.py +++ b/tests/dataset/test_dataset_export.py @@ -1874,7 +1874,7 @@ def test_multi_index_export_with_inferred_parameter( ) -> None: """Inferred parameters must export correctly when a MultiIndex dim is used.""" xds = mock_dataset_non_grid_inferred.to_xarray_dataset( - use_multi_index=use_multi_index # pyright: ignore[reportArgumentType] + use_multi_index=use_multi_index # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] ) assert xds.sizes == {"multi_index": 50} @@ -1898,13 +1898,13 @@ def test_non_unique_multi_index_export_with_inferred_parameter( def test_multi_index_wrong_option(mock_dataset_non_grid: DataSet) -> None: with pytest.raises(ValueError, match="Invalid value for use_multi_index"): - mock_dataset_non_grid.to_xarray_dataset(use_multi_index=True) # pyright: ignore[reportArgumentType] + mock_dataset_non_grid.to_xarray_dataset(use_multi_index=True) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] with pytest.raises(ValueError, match="Invalid value for use_multi_index"): - mock_dataset_non_grid.to_xarray_dataset(use_multi_index=False) # pyright: ignore[reportArgumentType] + mock_dataset_non_grid.to_xarray_dataset(use_multi_index=False) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] with pytest.raises(ValueError, match="Invalid value for use_multi_index"): - mock_dataset_non_grid.to_xarray_dataset(use_multi_index="perhaps") # pyright: ignore[reportArgumentType] + mock_dataset_non_grid.to_xarray_dataset(use_multi_index="perhaps") # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] def test_geneate_pandas_index() -> None: diff --git a/tests/dataset/test_measurement_extensions.py b/tests/dataset/test_measurement_extensions.py index ab84afca6cd5..a88f2caddc75 100644 --- a/tests/dataset/test_measurement_extensions.py +++ b/tests/dataset/test_measurement_extensions.py @@ -371,7 +371,7 @@ def test_dond_into_fails_with_together_sweeps( ): dond_into( datasaver, - TogetherSweep(sweep1, sweep2), # pyright: ignore [reportArgumentType] + TogetherSweep(sweep1, sweep2), # pyright: ignore [reportArgumentType] # ty: ignore[invalid-argument-type] meas1, ) @@ -394,8 +394,8 @@ def test_dond_into_fails_with_groups( dond_into( datasaver, sweep1, - [meas1], # pyright: ignore [reportArgumentType] - [meas2], # pyright: ignore [reportArgumentType] + [meas1], # pyright: ignore [reportArgumentType] # ty: ignore[invalid-argument-type] + [meas2], # pyright: ignore [reportArgumentType] # ty: ignore[invalid-argument-type] ) diff --git a/tests/dataset/test_string_data.py b/tests/dataset/test_string_data.py index 8484dd02d033..a5639f67fc0c 100644 --- a/tests/dataset/test_string_data.py +++ b/tests/dataset/test_string_data.py @@ -205,7 +205,7 @@ def test_list_of_strings(experiment: "Experiment") -> None: @settings(suppress_health_check=(HealthCheck.function_scoped_fixture,), deadline=None) @given( - p_values=hypnumpy.arrays( + p_values=hypnumpy.arrays( # ty: ignore[no-matching-overload] dtype=hst.sampled_from( ( hypnumpy.unicode_string_dtypes(), diff --git a/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1500.py b/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1500.py index 887e58c9711b..04d6c60ae2ec 100644 --- a/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1500.py +++ b/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1500.py @@ -69,21 +69,21 @@ def test_snapshot_does_not_raise_warnings(b1500: KeysightB1500) -> None: def test_submodule_access_by_class(b1500: KeysightB1500) -> None: assert b1500.smu1 in b1500.by_kind[constants.ModuleKind.SMU] # while it does not type check it is possible to look up by string - assert b1500.smu1 in b1500.by_kind["SMU"] # pyright: ignore[reportArgumentType] + assert b1500.smu1 in b1500.by_kind["SMU"] # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] assert b1500.smu1 in b1500.by_kind[constants.ModuleKind.SMU] - assert b1500.smu2 in b1500.by_kind["SMU"] # pyright: ignore[reportArgumentType] + assert b1500.smu2 in b1500.by_kind["SMU"] # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] assert b1500.cmu1 in b1500.by_kind[constants.ModuleKind.CMU] - assert b1500.cmu1 in b1500.by_kind["CMU"] # pyright: ignore[reportArgumentType] + assert b1500.cmu1 in b1500.by_kind["CMU"] # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] assert b1500.wgfmu1 in b1500.by_kind[constants.ModuleKind.WGFMU] - assert b1500.wgfmu1 in b1500.by_kind["WGFMU"] # pyright: ignore[reportArgumentType] + assert b1500.wgfmu1 in b1500.by_kind["WGFMU"] # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] def test_submodule_access_by_slot(b1500: KeysightB1500) -> None: assert b1500.smu1 is b1500.by_slot[SlotNr.SLOT01] assert b1500.smu2 is b1500.by_slot[SlotNr.SLOT02] # while it does not type check it is possible to look up by integer - assert b1500.cmu1 is b1500.by_slot[3] # pyright: ignore[reportArgumentType] - assert b1500.wgfmu1 is b1500.by_slot[6] # pyright: ignore[reportArgumentType] + assert b1500.cmu1 is b1500.by_slot[3] # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + assert b1500.wgfmu1 is b1500.by_slot[6] # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] def test_submodule_access_by_channel(b1500: KeysightB1500) -> None: @@ -92,7 +92,7 @@ def test_submodule_access_by_channel(b1500: KeysightB1500) -> None: assert b1500.cmu1 is b1500.by_channel[ChNr.SLOT_03_CH1] assert b1500.wgfmu1 is b1500.by_channel[ChNr.SLOT_06_CH1] # while it does not type check it is possible to look up by integer - assert b1500.wgfmu1 is b1500.by_channel[6] # pyright: ignore[reportArgumentType] + assert b1500.wgfmu1 is b1500.by_channel[6] # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] assert b1500.wgfmu1 is b1500.by_channel[ChNr.SLOT_06_CH2] diff --git a/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1517a_smu.py b/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1517a_smu.py index ea4bedb6d5e7..46601f8a1188 100644 --- a/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1517a_smu.py +++ b/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1517a_smu.py @@ -67,7 +67,7 @@ def test_v_measure_range_config_raises_type_error(smu: KeysightB1517A) -> None: msg = re.escape("Expected valid voltage measurement range, got 42.") with pytest.raises(TypeError, match=msg): - smu.v_measure_range_config(v_measure_range=42) # pyright: ignore[reportArgumentType] + smu.v_measure_range_config(v_measure_range=42) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] def test_v_measure_range_config_raises_invalid_range_error(smu: KeysightB1517A) -> None: @@ -105,7 +105,7 @@ def test_i_measure_range_config_raises_type_error(smu: KeysightB1517A) -> None: msg = re.escape("Expected valid current measurement range, got 99.") with pytest.raises(TypeError, match=msg): - smu.i_measure_range_config(i_measure_range=99) # pyright: ignore[reportArgumentType] + smu.i_measure_range_config(i_measure_range=99) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] def test_i_measure_range_config_raises_invalid_range_error(smu: KeysightB1517A) -> None: @@ -361,7 +361,7 @@ def test_set_average_samples_for_high_speed_adc(smu: KeysightB1517A) -> None: mainframe.reset_mock() # while it does not type check, it is possible to pass the enum value as int - smu.set_average_samples_for_high_speed_adc(131, 2) # pyright: ignore[reportArgumentType] + smu.set_average_samples_for_high_speed_adc(131, 2) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] mainframe.write.assert_called_once_with("AV 131,2") mainframe.reset_mock() diff --git a/tests/drivers/keysight_b1500/b1500_driver_tests/test_sampling_measurement.py b/tests/drivers/keysight_b1500/b1500_driver_tests/test_sampling_measurement.py index 0e734182659b..4c393c4e70ae 100644 --- a/tests/drivers/keysight_b1500/b1500_driver_tests/test_sampling_measurement.py +++ b/tests/drivers/keysight_b1500/b1500_driver_tests/test_sampling_measurement.py @@ -51,7 +51,7 @@ def return_predefined_data_on_xe(cmd: str) -> str: else: return original_ask(cmd) - smu_sm.root_instrument.ask = Mock(spec_set=smu.root_instrument.ask) # pyright: ignore[reportAttributeAccessIssue] + smu_sm.root_instrument.ask = Mock(spec_set=smu.root_instrument.ask) # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute] smu_sm.root_instrument.ask.side_effect = return_predefined_data_on_xe return smu_sm, status, channel, type_ diff --git a/tests/parameter/test_keyword_only_args.py b/tests/parameter/test_keyword_only_args.py index 3927d1962b6c..99c2bffc5528 100644 --- a/tests/parameter/test_keyword_only_args.py +++ b/tests/parameter/test_keyword_only_args.py @@ -157,8 +157,8 @@ def test_keyword_args_work(self) -> None: def test_positional_args_rejected(self) -> None: with pytest.raises(TypeError): - MultiChannelInstrumentParameter( - [], # pyright: ignore[reportCallIssue] + MultiChannelInstrumentParameter( # ty: ignore[missing-argument] + [], # pyright: ignore[reportCallIssue] # ty: ignore[too-many-positional-arguments] "x", name="test", names=("a",), diff --git a/tests/parameter/test_snapshot.py b/tests/parameter/test_snapshot.py index 573de8e5b4da..c5f531eb9de7 100644 --- a/tests/parameter/test_snapshot.py +++ b/tests/parameter/test_snapshot.py @@ -465,7 +465,7 @@ def test_snapshot_update_string_matches_legacy_value( # ``update=legacy`` intentionally uses the deprecated bool/None values to # confirm they still map to the new canonical behavior. - s_legacy = p_legacy.snapshot(update=legacy) # pyright: ignore[reportDeprecated] + s_legacy = p_legacy.snapshot(update=legacy) # pyright: ignore[reportDeprecated] # ty: ignore[deprecated] s_string = p_string.snapshot(update=string) assert s_legacy["value"] == s_string["value"] diff --git a/tests/sphinx_extension/test_parse_parameter_attr.py b/tests/sphinx_extension/test_parse_parameter_attr.py index c0331a715195..fc2dbc869026 100644 --- a/tests/sphinx_extension/test_parse_parameter_attr.py +++ b/tests/sphinx_extension/test_parse_parameter_attr.py @@ -104,7 +104,7 @@ def test_decorated_init_func() -> None: def test_decorated_class() -> None: - attr = qcodes_parameter_attr_getter(DummyDecoratedClassTestClass, "other_attr") # pyright: ignore[reportDeprecated] + attr = qcodes_parameter_attr_getter(DummyDecoratedClassTestClass, "other_attr") # pyright: ignore[reportDeprecated] # ty: ignore[deprecated] assert isinstance(attr, ParameterProxy) assert repr(attr) == '"InstanceAttribute"' diff --git a/tests/test_channels.py b/tests/test_channels.py index 041ebcf20435..6bc42dc35767 100644 --- a/tests/test_channels.py +++ b/tests/test_channels.py @@ -214,7 +214,7 @@ def test_append_channel_wrong_type_raises(dci_with_list: DCIWithList) -> None: channel = EmptyChannel(dci_with_list, "foo") with pytest.raises(TypeError, match="All items in a channel list must"): - dci_with_list.channels.append(channel) # pyright: ignore[reportArgumentType] + dci_with_list.channels.append(channel) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] assert len(dci_with_list.channels) == n_channels @@ -244,7 +244,7 @@ def test_extend_wrong_type_raises(dci_with_list: DCIWithList) -> None: TypeError, match=re.escape("All items in a channel list must be of the same type."), ): - dci_with_list.channels.extend(channels) # pyright: ignore[reportArgumentType] + dci_with_list.channels.extend(channels) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] def test_extend_locked_list_raises(dci_with_list: DCIWithList) -> None: @@ -252,7 +252,7 @@ def test_extend_locked_list_raises(dci_with_list: DCIWithList) -> None: names = ("foo", "bar", "foobar") channels = tuple(EmptyChannel(dci_with_list, "Chan" + name) for name in names) with pytest.raises(AttributeError, match="Cannot extend a locked channel list"): - dci_with_list.channels.extend(channels) # pyright: ignore[reportArgumentType] + dci_with_list.channels.extend(channels) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] def test_extend_then_remove(dci_with_list: DCIWithList) -> None: @@ -292,7 +292,7 @@ def test_insert_channel(dci_with_list: DCIWithList) -> None: def test_insert_channel_wrong_type_raises(dci_with_list: DCIWithList) -> None: with pytest.raises(TypeError, match="All items in a channel list"): - dci_with_list.channels.insert(1, EmptyChannel(parent=dci_with_list, name="foo")) # pyright: ignore[reportArgumentType] + dci_with_list.channels.insert(1, EmptyChannel(parent=dci_with_list, name="foo")) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] def test_channel_type_can_be_inferred( @@ -308,7 +308,7 @@ def test_add_none_channel_tuple_to_channel_tuple_raises( dci: DummyChannelInstrument, ) -> None: with pytest.raises(TypeError, match="Can't add objects of type"): - _ = dci.channels + [1] # pyright: ignore[reportOperatorIssue] # noqa: RUF005 + _ = dci.channels + [1] # pyright: ignore[reportOperatorIssue] # ty: ignore[unsupported-operator] # noqa: RUF005 def test_add_channel_tuples_of_different_types_raises( @@ -524,7 +524,7 @@ def test_access_channels_by_name_empty_raises(dci: DummyChannelInstrument) -> No def test_access_channel_by_name_empty_raises(dci: DummyChannelInstrument) -> None: with pytest.raises(TypeError, match="missing 1 required positional argument"): - dci.channels.get_channel_by_name() # pyright: ignore[reportCallIssue] + dci.channels.get_channel_by_name() # pyright: ignore[reportCallIssue] # ty: ignore[missing-argument] def test_delete_from_channel_list(dci_with_list: DCIWithList) -> None: diff --git a/tests/validators/test_enum.py b/tests/validators/test_enum.py index 9496f63789d0..1c0c5220e52b 100644 --- a/tests/validators/test_enum.py +++ b/tests/validators/test_enum.py @@ -25,7 +25,7 @@ def test_good() -> None: for v in [22, "bad data", [44, 55]]: with pytest.raises((ValueError, TypeError)): - e.validate(v) # pyright: ignore[reportArgumentType] + e.validate(v) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] assert repr(e) == f"" diff --git a/tests/validators/test_literal.py b/tests/validators/test_literal.py index cb625d1d1228..69bbfcd4b2c7 100644 --- a/tests/validators/test_literal.py +++ b/tests/validators/test_literal.py @@ -15,10 +15,10 @@ def test_literal_validator() -> None: a123_val.validate(1) with pytest.raises(ValueError, match="5 is not a member of "): - a123_val.validate(5, context="Outside range") # pyright: ignore[reportArgumentType] + a123_val.validate(5, context="Outside range") # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] with pytest.raises(ValueError, match="some_str is not a member of "): - a123_val.validate("some_str", context="Wrong type") # pyright: ignore[reportArgumentType] + a123_val.validate("some_str", context="Wrong type") # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] def test_literal_validator_repr() -> None: From d022ad76b0417759f40127c2084ecc519f5b487b Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:16:45 +0200 Subject: [PATCH 53/70] Annotate numpy_complex as numpy complex types only The tuple only contains numpy types but was annotated as also containing the builtin complex. Since complex in an annotation implicitly means int or float or complex, that made calls such as complex_type(1 + 2j) be checked against int and float too. --- docs/changes/newsfragments/8441.improved.14 | 5 +++++ src/qcodes/utils/types.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 docs/changes/newsfragments/8441.improved.14 diff --git a/docs/changes/newsfragments/8441.improved.14 b/docs/changes/newsfragments/8441.improved.14 new file mode 100644 index 000000000000..1093e7f909c9 --- /dev/null +++ b/docs/changes/newsfragments/8441.improved.14 @@ -0,0 +1,5 @@ +The annotation of ``qcodes.utils.types.numpy_complex`` now correctly +describes its content as a tuple of numpy complex types. It was previously +annotated as also containing the builtin ``complex``, which via the implicit +numeric tower made type checkers treat it as possibly containing ``int`` or +``float`` as well. diff --git a/src/qcodes/utils/types.py b/src/qcodes/utils/types.py index dc82f1692674..223adb261622 100644 --- a/src/qcodes/utils/types.py +++ b/src/qcodes/utils/types.py @@ -75,7 +75,7 @@ Complex types that matches C types. """ -numpy_complex: tuple[type[complex_type_union], ...] = ( +numpy_complex: tuple[type[np.complexfloating], ...] = ( numpy_concrete_complex + numpy_c_complex ) """ From f29f1dc6a2d22782c7891237f5c3ebbb246fe7db Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:17:12 +0200 Subject: [PATCH 54/70] Annotate the monitor ports as int Without an annotation ty infers the type of the module level constant from its default value, so reassigning WEBSOCKET_PORT to select another port, as the test suite does, is an error. --- docs/changes/newsfragments/8441.improved.15 | 3 +++ src/qcodes/monitor/monitor.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 docs/changes/newsfragments/8441.improved.15 diff --git a/docs/changes/newsfragments/8441.improved.15 b/docs/changes/newsfragments/8441.improved.15 new file mode 100644 index 000000000000..bec83574a152 --- /dev/null +++ b/docs/changes/newsfragments/8441.improved.15 @@ -0,0 +1,3 @@ +The module level ports used by ``qcodes.monitor`` are now annotated as +``int``. They are intended to be reassigned to select another port, which +type checkers rejected since the type was inferred from the default value. diff --git a/src/qcodes/monitor/monitor.py b/src/qcodes/monitor/monitor.py index eb9caa7e8830..3d8fccc0e1e4 100644 --- a/src/qcodes/monitor/monitor.py +++ b/src/qcodes/monitor/monitor.py @@ -44,8 +44,8 @@ from websockets.asyncio.server import ServerConnection -WEBSOCKET_PORT = 5678 -SERVER_PORT = 3000 +WEBSOCKET_PORT: int = 5678 +SERVER_PORT: int = 3000 log = logging.getLogger(__name__) From bc97f56292be4448ce0f325ab8f16cdd5584b9db Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:18:19 +0200 Subject: [PATCH 55/70] Annotate shape containers in tests The element type of an unannotated dict or list literal is inferred from its content, and containers are invariant, so a literal such as {name: (11, 11)} is not a dict[str, tuple[int, ...]]. Declare the type that the receiving function expects instead. --- tests/dataset/test_dataset_basic.py | 4 ++-- tests/dataset/test_measurement_extensions.py | 10 +++++----- tests/dataset/test_sqlite_base.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/dataset/test_dataset_basic.py b/tests/dataset/test_dataset_basic.py index 350decb674e2..6f8c924079a5 100644 --- a/tests/dataset/test_dataset_basic.py +++ b/tests/dataset/test_dataset_basic.py @@ -1017,7 +1017,7 @@ def test_get_array_parameter_data_no_nulls( expected_names = {} expected_names["val1"] = ["val1", "sp1", "sp2"] expected_names["val2"] = ["val2", "sp1"] - expected_shapes = {} + expected_shapes: dict[str, list[tuple[int, ...]]] = {} expected_values = {} if "array" in types: @@ -1056,7 +1056,7 @@ def test_get_array_parameter_data(array_dataset: DataSet) -> None: expected_shapes: dict[str, list[tuple[int, ...]]] = {} expected_len = 5 expected_shapes[par_name] = [(expected_len,), (expected_len,)] - expected_values = {} + expected_values: dict[str, list[np.ndarray]] = {} expected_values[par_name] = [ np.ones(expected_len) + 1, np.linspace(5, 9, expected_len), diff --git a/tests/dataset/test_measurement_extensions.py b/tests/dataset/test_measurement_extensions.py index a88f2caddc75..b21c63927311 100644 --- a/tests/dataset/test_measurement_extensions.py +++ b/tests/dataset/test_measurement_extensions.py @@ -546,7 +546,7 @@ def test_shapes_in_dataset_definition_with_scalar_params( _ = default_database_and_experiment set1, set2, _, meas1, meas2, _ = default_params - expected_shapes = { + expected_shapes: dict[str, tuple[int, ...]] = { meas1.register_name: (11, 11), meas2.register_name: (11, 11), } @@ -575,7 +575,7 @@ def test_shapes_in_dataset_definition_with_pws( _ = default_database_and_experiment pws1, set1 = pws_params - expected_shapes = { + expected_shapes: dict[str, tuple[int, ...]] = { pws1.register_name: (11, 11), } dataset_definition = [ @@ -625,7 +625,7 @@ def test_setup_measurement_instances_sets_shapes( _ = default_database_and_experiment set1, _, _, meas1, _, _ = default_params - expected_shapes = {meas1.register_name: (5,)} + expected_shapes: dict[str, tuple[int, ...]] = {meas1.register_name: (5,)} dataset_definitions = [ DataSetDefinition( name="test_shapes", @@ -667,8 +667,8 @@ def test_shapes_with_multiple_datasets( _ = default_database_and_experiment set1, set2, set3, meas1, _, meas3 = default_params - shapes_1 = {meas1.register_name: (11, 11)} - shapes_2 = {meas3.register_name: (11, 11)} + shapes_1: dict[str, tuple[int, ...]] = {meas1.register_name: (11, 11)} + shapes_2: dict[str, tuple[int, ...]] = {meas3.register_name: (11, 11)} dataset_definition = [ DataSetDefinition( name="dataset_1", diff --git a/tests/dataset/test_sqlite_base.py b/tests/dataset/test_sqlite_base.py index 3cfea0c9bd6e..d00b3aa5fada 100644 --- a/tests/dataset/test_sqlite_base.py +++ b/tests/dataset/test_sqlite_base.py @@ -64,7 +64,7 @@ def _make_simple_run_describer() -> "Generator[RunDescriber, None, None]": t = ParamSpecBase("t", "numeric") y = ParamSpecBase("y", "numeric") - paramtree = {y: (x, t)} + paramtree: dict[ParamSpecBase, tuple[ParamSpecBase, ...]] = {y: (x, t)} interdependencies = InterDependencies_(dependencies=paramtree) rundescriber = RunDescriber(interdependencies) From 7cd74ac591b4de441f407d8f40e8c624fe5c446e Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:18:59 +0200 Subject: [PATCH 56/70] Assert optional attributes are set in tests Parameter.step and DelegateParameter.source are optional and are read back through a property, so a type checker cannot know that the value assigned earlier in the test is still there. --- tests/parameter/test_delegate_parameter.py | 1 + tests/parameter/test_parameter_ramp.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/tests/parameter/test_delegate_parameter.py b/tests/parameter/test_delegate_parameter.py index 423850393480..dd65060cb48a 100644 --- a/tests/parameter/test_delegate_parameter.py +++ b/tests/parameter/test_delegate_parameter.py @@ -420,6 +420,7 @@ def test_delegate_parameter_with_changed_source_snapshot_matches_value( delegate_param.source = source_parameter calc_value = (value - offset) / scale assert delegate_param.cache.get(get_if_invalid=False) == calc_value + assert delegate_param.source is not None assert delegate_param.source.cache.get(get_if_invalid=False) == value snapshot = delegate_param.snapshot() # disregard timestamp that might be slightly different diff --git a/tests/parameter/test_parameter_ramp.py b/tests/parameter/test_parameter_ramp.py index 549f263c5009..bb6777895e55 100644 --- a/tests/parameter/test_parameter_ramp.py +++ b/tests/parameter/test_parameter_ramp.py @@ -62,6 +62,7 @@ def test_ramp_scaled(scale: int, value: float) -> None: assert p.raw_value == first_step * scale # then check the generated steps. They should not be scaled as the # scaling happens when setting them + assert p.step is not None expected_steps = np.linspace(first_step + p.step, second_step, 90) actual_steps = p.get_ramp_values(second_step, p.step) np.testing.assert_allclose(np.array(actual_steps), expected_steps) @@ -104,6 +105,7 @@ def test_ramp_parser(value: float) -> None: assert p.raw_value == -first_step # then check the generated steps. They should not be parsed as the # scaling happens when setting them + assert p.step is not None expected_steps = np.linspace((first_step + p.step), second_step, 90) actual_steps = p.get_ramp_values(second_step, p.step) np.testing.assert_allclose(np.array(actual_steps), expected_steps) @@ -142,6 +144,7 @@ def test_ramp_parsed_scaled(scale: int, value: float) -> None: # these are parsed in the set_wrapper np.testing.assert_allclose(np.array(p.set_values), expected_raw_steps) assert p.raw_value == -scale * first_step + assert p.step is not None expected_steps = np.linspace(first_step + p.step, second_step, 90) actual_steps = p.get_ramp_values(10, p.step) np.testing.assert_allclose(np.array(actual_steps), expected_steps) From 1d416569ab8f10f8233078eab018dbc1b514f31a Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:20:22 +0200 Subject: [PATCH 57/70] Suppress ty on TypeVars used as plain values ty requires a legacy TypeVar to be assigned to a variable matching its name, so it rejects the ones these tests store straight into a dict. They are values handed to _make_deprecated_typevars_getattr rather than type variables used to parametrize anything, and both mypy and pyright accept them. --- tests/utils/test_deprecate.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/utils/test_deprecate.py b/tests/utils/test_deprecate.py index 24b371f49622..454d396c1b51 100644 --- a/tests/utils/test_deprecate.py +++ b/tests/utils/test_deprecate.py @@ -16,9 +16,11 @@ @pytest.fixture def deprecated_getattr() -> _FixtureT: """Create a __getattr__ with two deprecated TypeVars.""" + # these TypeVars are values handed to the deprecation shim rather than + # type variables used to parametrize anything, which ty does not allow deprecated = { - "MyT": TypeVar("MyT"), - "MyK": TypeVar("MyK", bound=int), + "MyT": TypeVar("MyT"), # ty: ignore[invalid-legacy-type-variable] + "MyK": TypeVar("MyK", bound=int), # ty: ignore[invalid-legacy-type-variable] } getattr_fn = _make_deprecated_typevars_getattr("fake.module", deprecated) return deprecated, getattr_fn @@ -63,7 +65,7 @@ def test_repeated_access_returns_same_object( def test_fallback_is_called_for_unknown_names() -> None: - deprecated: dict[str, TypeVar] = {"X": TypeVar("X")} + deprecated: dict[str, TypeVar] = {"X": TypeVar("X")} # ty: ignore[invalid-legacy-type-variable] def fallback(name: str) -> str: return f"fallback:{name}" @@ -73,7 +75,7 @@ def fallback(name: str) -> str: def test_fallback_not_called_for_deprecated_names() -> None: - deprecated: dict[str, TypeVar] = {"X": TypeVar("X")} + deprecated: dict[str, TypeVar] = {"X": TypeVar("X")} # ty: ignore[invalid-legacy-type-variable] fallback_called = False def fallback(name: str) -> str: From 2634f3f1dfc28fb02784e5b8ae18f289a649bc8d Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:20:48 +0200 Subject: [PATCH 58/70] Clean up ignore comments in parameter override test One instrument in this test module deliberately assigns a parameter over a method, which is an error by design and now says so. The blanket ignore on the instrument that overrides a property is no longer needed by any of the three type checkers. --- tests/parameter/test_parameter_override.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/parameter/test_parameter_override.py b/tests/parameter/test_parameter_override.py index 3017b87c9136..c9dfc5d574f6 100644 --- a/tests/parameter/test_parameter_override.py +++ b/tests/parameter/test_parameter_override.py @@ -33,7 +33,7 @@ def __init__(self, name: str, **kwargs: "Unpack[InstrumentBaseKWArgs]") -> None: This instrument errors because it tries to override an attribute with a parameter. """ super().__init__(name, **kwargs) - self.voltage = self.add_parameter("voltage", set_cmd=None, get_cmd=None) + self.voltage = self.add_parameter("voltage", set_cmd=None, get_cmd=None) # ty: ignore[invalid-assignment] def voltage(self) -> int: return 0 From dfb533df1a61d95245544022cf91f60867448b70 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:21:37 +0200 Subject: [PATCH 59/70] Suppress ty on reading the name of a callable The wrapped callable is a function in practice but its declared type does not guarantee that. mypy and pyright both accept the attribute access. --- tests/common.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/common.py b/tests/common.py index 0f3b7aff5652..00ea6fc4f7e6 100644 --- a/tests/common.py +++ b/tests/common.py @@ -87,7 +87,9 @@ def profile[**P, T](func: Callable[P, T]) -> Callable[P, T]: """ def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: - profile_filename = func.__name__ + ".prof" + # a Callable is not necessarily a function object, so ty rejects + # reading its name here. mypy and pyright both allow it. + profile_filename = func.__name__ + ".prof" # ty: ignore[unresolved-attribute] profiler = cProfile.Profile() result = profiler.runcall(func, *args, **kwargs) profiler.dump_stats(profile_filename) From 789a5d51d867f5f4380622131b90e63055e5a216 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 21 Sep 2026 21:47:21 +0200 Subject: [PATCH 60/70] Fix the tests in the simulated instrument notebook The test cells imported the Weinschel8320 driver from qcodes, replacing the class of the same name that the notebook defines, which ty reports as an invalid assignment. Remove those imports so the tests use the driver from the notebook, and annotate the fixtures and tests. test_init_v2 requested a driver fixture that does not exist, so request weinschel_driver_2 instead. Also clear the stale output of the first cell. --- ...reating-Simulated-PyVISA-Instruments.ipynb | 39 +++++-------------- 1 file changed, 10 insertions(+), 29 deletions(-) diff --git a/docs/examples/writing_drivers/Creating-Simulated-PyVISA-Instruments.ipynb b/docs/examples/writing_drivers/Creating-Simulated-PyVISA-Instruments.ipynb index 03a841739330..47f12c5af6a8 100644 --- a/docs/examples/writing_drivers/Creating-Simulated-PyVISA-Instruments.ipynb +++ b/docs/examples/writing_drivers/Creating-Simulated-PyVISA-Instruments.ipynb @@ -48,23 +48,7 @@ "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\\221108-14924-qcodes.log\n" - ] - } - ], + "outputs": [], "source": [ "from typing import TYPE_CHECKING\n", "\n", @@ -74,6 +58,7 @@ "from qcodes.instrument.visa import VisaInstrument, VisaInstrumentKWArgs\n", "\n", "if TYPE_CHECKING:\n", + " from collections.abc import Generator\n", " from typing import Unpack\n", "\n", "\n", @@ -207,13 +192,11 @@ "source": [ "import pytest\n", "\n", - "from qcodes.instrument_drivers.weinschel import Weinschel8320\n", - "\n", "\n", "# The following decorator makes the driver\n", "# available to all the functions in this module\n", "@pytest.fixture(scope=\"function\", name=\"weinschel_driver_1\")\n", - "def _weinschel_driver_1():\n", + "def _weinschel_driver_1() -> \"Generator[Weinschel8320, None, None]\":\n", " wein_sim = Weinschel8320(\n", " \"wein_sim\",\n", " address=\"GPIB::1::65535::INSTR\",\n", @@ -224,7 +207,7 @@ " wein_sim.close()\n", "\n", "\n", - "def test_init_v1(weinschel_driver_1):\n", + "def test_init_v1(weinschel_driver_1: Weinschel8320) -> None:\n", " \"\"\"\n", " Test that simple initialisation works\n", " \"\"\"\n", @@ -344,13 +327,11 @@ "source": [ "import pytest\n", "\n", - "from qcodes.instrument_drivers.weinschel import Weinschel8320\n", - "\n", "\n", "# The following decorator makes the driver\n", "# available to all the functions in this module\n", "@pytest.fixture(scope=\"function\", name=\"weinschel_driver_2\")\n", - "def _weinschel_driver():\n", + "def _weinschel_driver() -> \"Generator[Weinschel8320, None, None]\":\n", " wein_sim = Weinschel8320(\n", " \"wein_sim\", address=\"GPIB::1::INSTR\", pyvisa_sim_file=\"Weinschel_8320.yaml\"\n", " )\n", @@ -359,7 +340,7 @@ " wein_sim.close()\n", "\n", "\n", - "def test_init_v2(driver):\n", + "def test_init_v2(weinschel_driver_2: Weinschel8320) -> None:\n", " \"\"\"\n", " Test that simple initialisation works\n", " \"\"\"\n", @@ -367,12 +348,12 @@ " # There is not that much to do, really.\n", " # We can check that the IDN string reads back correctly\n", "\n", - " idn_dict = driver.IDN()\n", + " idn_dict = weinschel_driver_2.IDN()\n", "\n", " assert idn_dict[\"vendor\"] == \"QCoDeS\"\n", "\n", "\n", - "def test_attenuation_validation(weinschel_driver_2):\n", + "def test_attenuation_validation(weinschel_driver_2: Weinschel8320) -> None:\n", " \"\"\"\n", " Test that incorrect values are rejected\n", " \"\"\"\n", @@ -409,7 +390,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "qcodes", "language": "python", "name": "python3" }, @@ -423,7 +404,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.3" + "version": "3.14.7" }, "nbsphinx": { "execute": "never" From c256b280213514bc1d358a122f64587e3413824b Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 21 Sep 2026 21:51:07 +0200 Subject: [PATCH 61/70] Annotate the IDN parameter as a dict IDN returns the dict produced by get_idn. Annotate it as Parameter[dict[str, str], Self]. Strictly the values may be None, but including that would require asserts at every use. The Weinschel 8320 test treated the result as a string. It only runs with an instance of the driver, so this went unnoticed. Check the vendor and model instead. --- src/qcodes/instrument/instrument.py | 7 ++++++- tests/drivers/test_weinchel.py | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/qcodes/instrument/instrument.py b/src/qcodes/instrument/instrument.py index fc3acfa90432..e09f06302dbf 100644 --- a/src/qcodes/instrument/instrument.py +++ b/src/qcodes/instrument/instrument.py @@ -17,6 +17,7 @@ from typing import Self, Unpack from qcodes.logger.instrument_logger import InstrumentLoggerAdapter + from qcodes.parameters.parameter import Parameter log = logging.getLogger(__name__) @@ -68,7 +69,11 @@ def __init__(self, name: str, **kwargs: Unpack[InstrumentBaseKWArgs]) -> None: super().__init__(name=name, **kwargs) - self.IDN = self.add_parameter("IDN", get_cmd=self.get_idn, vals=Anything()) + # the data type is strictly speaking dict[str, str | None] + # but we omit the | None since otherwise a lot of asserts would be required + self.IDN: Parameter[dict[str, str], Self] = self.add_parameter( + "IDN", get_cmd=self.get_idn, vals=Anything() + ) """ Standard IDN parameter, which queries the instrument for its ID """ diff --git a/tests/drivers/test_weinchel.py b/tests/drivers/test_weinchel.py index 48d687f47db3..b6ac9a790a71 100644 --- a/tests/drivers/test_weinchel.py +++ b/tests/drivers/test_weinchel.py @@ -13,7 +13,9 @@ class TestWeinschel8320(DriverTestCase[Weinschel8320]): def test_firmware_version(self) -> None: v = self.instrument.IDN.get() - self.assertTrue(v.startswith("API Weinschel, 8320,")) + self.assertIsInstance(v, dict) + self.assertTrue(v["vendor"] == "API Weinschel") + self.assertTrue(v["model"] == "8320") def test_attenuation(self) -> None: curr_val = self.instrument.attenuation.get() From fd5d68f85e4dbaed80869ed3bcad0437e0097155 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 25 Sep 2026 18:46:19 +0200 Subject: [PATCH 62/70] Compare the global set callback against None ty warns that testing a callable for truthiness is likely a missing call. Checking for None instead states what is meant and matches how ParameterBase itself guards the callback. --- tests/parameter/test_parameter_on_set_callback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/parameter/test_parameter_on_set_callback.py b/tests/parameter/test_parameter_on_set_callback.py index 89bc5a7f4aab..adf5be970455 100644 --- a/tests/parameter/test_parameter_on_set_callback.py +++ b/tests/parameter/test_parameter_on_set_callback.py @@ -275,7 +275,7 @@ def test_set_callback_for_instance( captured_instance_params = [] def callback(param: ParameterBase, val: Any) -> None: - if ParameterBase.global_on_set_callback: + if ParameterBase.global_on_set_callback is not None: ParameterBase.global_on_set_callback(param, val) captured_instance_params.append(val) From 517655d46bb16a60cdcf50e629b536527716b7b8 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 14 Oct 2026 07:29:03 +0200 Subject: [PATCH 63/70] Suppress ty on deliberately invalid test code ty reports the fixtures that overwrite get and set on a Parameter subclass as invalid overrides of a mutable attribute. ParameterBase rejects that at runtime, and asserting it is the whole point of the fixtures, so they are invalid on purpose. The invalid types assigned in test_parameter_mixin and the float passed to get_level_code are deliberate too and were already suppressed for mypy. --- tests/extensions/parameters/test_parameter_mixin.py | 4 ++-- tests/parameter/conftest.py | 8 ++++++-- tests/test_logger.py | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/extensions/parameters/test_parameter_mixin.py b/tests/extensions/parameters/test_parameter_mixin.py index 424397260b67..6b87855c50aa 100644 --- a/tests/extensions/parameters/test_parameter_mixin.py +++ b/tests/extensions/parameters/test_parameter_mixin.py @@ -90,7 +90,7 @@ def test_compatible_bases_must_be_list() -> None: with pytest.raises(TypeError): class IncorrectParameterMixin(ParameterMixin): - _COMPATIBLE_BASES = "notalist" # type: ignore[assignment] + _COMPATIBLE_BASES = "notalist" # type: ignore[assignment] # ty: ignore[invalid-assignment] def test_incompatible_bases_must_be_list() -> None: @@ -100,7 +100,7 @@ class IncorrectParameterMixin(ParameterMixin): _COMPATIBLE_BASES: ClassVar[list[type[ParameterBase]]] = [ CompatibleParameter ] - _INCOMPATIBLE_BASES = IncompatibleParameter # type: ignore[assignment] + _INCOMPATIBLE_BASES = IncompatibleParameter # type: ignore[assignment] # ty: ignore[invalid-assignment] ############################## diff --git a/tests/parameter/conftest.py b/tests/parameter/conftest.py index 49128488b22b..c1665e64e119 100644 --- a/tests/parameter/conftest.py +++ b/tests/parameter/conftest.py @@ -103,7 +103,9 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.set_count = 0 self.get_count = 0 - def get(self) -> int: + # overwriting get is not allowed and raises at runtime, which is what + # test_parameter_with_overwritten_get_raises checks + def get(self) -> int: # ty: ignore[invalid-mutable-override] self.get_count += 1 return self._value @@ -117,7 +119,9 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.set_count = 0 self.get_count = 0 - def set(self, value: Any) -> None: + # overwriting set is not allowed and raises at runtime, which is what + # test_parameter_with_overwritten_set_raises checks + def set(self, value: Any) -> None: # ty: ignore[invalid-mutable-override] self.set_count += 1 self._value = value diff --git a/tests/test_logger.py b/tests/test_logger.py index 81d3c3c3b1ea..45bfc62b82a3 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -378,7 +378,7 @@ def test_get_level_name_with_invalid_type() -> None: def test_get_level_code_with_invalid_type() -> None: """Only str and int can be converted to a logging level code.""" with pytest.raises(RuntimeError, match="get_level_code: Cannot to convert level"): - logger.get_level_code(1.5) # type: ignore[arg-type] + logger.get_level_code(1.5) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] class ScopedDummyInstrument(DummyInstrument): From 23e1c91d3d643a7cbcd9611fbfdaef98b0f30c8d Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 22 Oct 2026 20:57:58 +0200 Subject: [PATCH 64/70] Suppress ty errors on deliberately mistyped tests The type annotations added to the tests on main come with pyright and mypy suppressions on the lines that pass invalid types on purpose, mostly to check runtime error handling. Suppress the corresponding ty errors too. --- tests/dataset/dond/test_dond.py | 4 ++-- .../measurement/test_measurement_context_manager.py | 2 +- tests/dataset/test_dataset_basic.py | 6 +++--- tests/dataset/test_experiment_container.py | 4 ++-- .../test_parameter_mixin_interdependent.py | 6 +++--- .../test_parameter_mixin_on_cache_change.py | 2 +- tests/parameter/test_permissive_range.py | 2 +- tests/test_config.py | 12 ++++++------ tests/test_sweep_values.py | 4 ++-- 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/tests/dataset/dond/test_dond.py b/tests/dataset/dond/test_dond.py index dc12841c423d..162ad2535c2b 100644 --- a/tests/dataset/dond/test_dond.py +++ b/tests/dataset/dond/test_dond.py @@ -1092,8 +1092,8 @@ def test_dond_2d_multi_datasets_with_callable_output_data( exp_1 = dond( sweep_1, sweep_2, - [_string_callable, _param, _param_complex], # pyright: ignore[reportArgumentType] - [_string_callable, _param_2, _param_complex_2], # pyright: ignore[reportArgumentType] + [_string_callable, _param, _param_complex], # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + [_string_callable, _param_2, _param_complex_2], # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] ) datasets = exp_1[0] assert isinstance(datasets, tuple) diff --git a/tests/dataset/measurement/test_measurement_context_manager.py b/tests/dataset/measurement/test_measurement_context_manager.py index 51815dc9e649..b7e3df237e9b 100644 --- a/tests/dataset/measurement/test_measurement_context_manager.py +++ b/tests/dataset/measurement/test_measurement_context_manager.py @@ -414,7 +414,7 @@ def test_setting_write_period(wp: float | str) -> None: if isinstance(wp, str): with pytest.raises(ValueError): - meas.write_period = wp # type: ignore[assignment] + meas.write_period = wp # type: ignore[assignment] # ty: ignore[invalid-assignment] elif wp < 1e-3: with pytest.raises(ValueError): meas.write_period = wp diff --git a/tests/dataset/test_dataset_basic.py b/tests/dataset/test_dataset_basic.py index 6f8c924079a5..91e094468ce3 100644 --- a/tests/dataset/test_dataset_basic.py +++ b/tests/dataset/test_dataset_basic.py @@ -284,7 +284,7 @@ def test_create_dataset_from_non_existing_run_id( ValueError, match=f"Run with run_id {non_existing_run_id} does not exist in the database", ): - _ = DataSet(run_id=non_existing_run_id) # pyright: ignore[reportArgumentType] + _ = DataSet(run_id=non_existing_run_id) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] def test_create_dataset_pass_both_connection_and_path_to_db( @@ -573,7 +573,7 @@ def test_load_by_counter_for_nonexisting_counter( ) -> None: exp_id = 1 with pytest.raises(RuntimeError, match="Expected one row"): - _ = load_by_counter(exp_id, nonexisting_counter) # pyright: ignore[reportArgumentType] + _ = load_by_counter(exp_id, nonexisting_counter) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] @pytest.mark.usefixtures("empty_temp_db") @@ -582,7 +582,7 @@ def test_load_by_counter_for_nonexisting_experiment( nonexisting_exp_id: int | None, ) -> None: with pytest.raises(RuntimeError, match="Expected one row"): - _ = load_by_counter(nonexisting_exp_id, 1) # pyright: ignore[reportArgumentType] + _ = load_by_counter(nonexisting_exp_id, 1) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] @pytest.mark.usefixtures("empty_temp_db") diff --git a/tests/dataset/test_experiment_container.py b/tests/dataset/test_experiment_container.py index 720f47757a26..8e7126681eb7 100644 --- a/tests/dataset/test_experiment_container.py +++ b/tests/dataset/test_experiment_container.py @@ -169,7 +169,7 @@ def test_experiment_read_only_properties(experiment: "Experiment") -> None: @pytest.mark.parametrize("non_existing_id", (1, 0, -1, "number#42")) def test_create_experiment_from_non_existing_id(non_existing_id: int | str) -> None: with pytest.raises(ValueError, match="No such experiment in the database"): - _ = Experiment(exp_id=non_existing_id) # pyright: ignore[reportArgumentType] + _ = Experiment(exp_id=non_existing_id) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] @pytest.mark.usefixtures("empty_temp_db") @@ -183,7 +183,7 @@ def test_load_experiment_from_non_existing_id(non_existing_id: "int") -> None: @pytest.mark.parametrize("bad_id", (None, "number#42")) def test_load_experiment_from_bad_id(bad_id: str | None) -> None: with pytest.raises(ValueError, match="Experiment ID must be an integer"): - _ = load_experiment(bad_id) # pyright: ignore[reportArgumentType] + _ = load_experiment(bad_id) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] def test_format_string(empty_temp_db: "None") -> None: diff --git a/tests/extensions/parameters/test_parameter_mixin_interdependent.py b/tests/extensions/parameters/test_parameter_mixin_interdependent.py index 407a44f795d6..b8d4a304a69f 100644 --- a/tests/extensions/parameters/test_parameter_mixin_interdependent.py +++ b/tests/extensions/parameters/test_parameter_mixin_interdependent.py @@ -247,7 +247,7 @@ def test_typeerror_if_dependency_update_method_invalid( with pytest.raises( TypeError, match=re.escape("dependency_update_method must be callable or None.") ): - param.dependency_update_method = 123 # pyright: ignore[reportAttributeAccessIssue] + param.dependency_update_method = 123 # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[invalid-assignment] def test_typeerror_if_dependent_on_invalid(mock_instr: MockInstrument) -> None: @@ -261,10 +261,10 @@ def test_typeerror_if_dependent_on_invalid(mock_instr: MockInstrument) -> None: with pytest.raises( TypeError, match=re.escape("dependent_on must be a list of strings.") ): - param.dependent_on = "not_a_list" # pyright: ignore[reportAttributeAccessIssue] + param.dependent_on = "not_a_list" # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[invalid-assignment] # Assign a list with non-string with pytest.raises( TypeError, match=re.escape("dependent_on must be a list of strings.") ): - param.dependent_on = [123, None] # pyright: ignore[reportAttributeAccessIssue] + param.dependent_on = [123, None] # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[invalid-assignment] diff --git a/tests/extensions/parameters/test_parameter_mixin_on_cache_change.py b/tests/extensions/parameters/test_parameter_mixin_on_cache_change.py index 27d161307aca..cdd6c7dd7988 100644 --- a/tests/extensions/parameters/test_parameter_mixin_on_cache_change.py +++ b/tests/extensions/parameters/test_parameter_mixin_on_cache_change.py @@ -100,7 +100,7 @@ def test_error_on_non_callable_callback( docstring="A parameter for testing non-callable callbacks.", ) with pytest.raises(TypeError): - test_parameter.on_cache_change = invalid_callback # pyright: ignore[reportAttributeAccessIssue] + test_parameter.on_cache_change = invalid_callback # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[invalid-assignment] def test_no_callback_invocation_on_init_or_get( diff --git a/tests/parameter/test_permissive_range.py b/tests/parameter/test_permissive_range.py index c4e511b3666d..e639b1b831cf 100644 --- a/tests/parameter/test_permissive_range.py +++ b/tests/parameter/test_permissive_range.py @@ -25,7 +25,7 @@ @pytest.mark.parametrize("args", bad_args) def test_bad_calls(args: list[float | str | None]) -> None: with pytest.raises(TypeError): - permissive_range(*args) # pyright: ignore[reportArgumentType] + permissive_range(*args) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] @pytest.mark.parametrize("args,result", good_args) diff --git a/tests/test_config.py b/tests/test_config.py index f1cf2315411a..762d815a0539 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -170,9 +170,9 @@ def test_missing_config_file(config: Config) -> None: def test_default_config_files(config: Config, load_config: "MockType") -> None: load_config.side_effect = partial(side_effect, GOOD_CONFIG_MAP) # don't try to load custom schemas - config.schema_cwd_file_name = None # pyright: ignore[reportAttributeAccessIssue] - config.schema_home_file_name = None # pyright: ignore[reportAttributeAccessIssue] - config.schema_env_file_name = None # pyright: ignore[reportAttributeAccessIssue] + config.schema_cwd_file_name = None # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[invalid-assignment] + config.schema_home_file_name = None # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[invalid-assignment] + config.schema_env_file_name = None # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[invalid-assignment] config.defaults, _ = config.load_default() updated_config = config.update_config() assert updated_config == CONFIG @@ -181,9 +181,9 @@ def test_default_config_files(config: Config, load_config: "MockType") -> None: def test_bad_config_files(config: Config, load_config: "MockType") -> None: load_config.side_effect = partial(side_effect, BAD_CONFIG_MAP) # don't try to load custom schemas - config.schema_cwd_file_name = None # pyright: ignore[reportAttributeAccessIssue] - config.schema_home_file_name = None # pyright: ignore[reportAttributeAccessIssue] - config.schema_env_file_name = None # pyright: ignore[reportAttributeAccessIssue] + config.schema_cwd_file_name = None # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[invalid-assignment] + config.schema_home_file_name = None # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[invalid-assignment] + config.schema_env_file_name = None # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[invalid-assignment] config.defaults, _ = config.load_default() with pytest.raises(jsonschema.exceptions.ValidationError): config.update_config() diff --git a/tests/test_sweep_values.py b/tests/test_sweep_values.py index d01002af4499..de6b97c4498e 100644 --- a/tests/test_sweep_values.py +++ b/tests/test_sweep_values.py @@ -61,11 +61,11 @@ def test_errors(c0: Parameter, c1: Parameter, c2: Parameter) -> None: # improper use of extend with pytest.raises(TypeError): - c0[0.1].extend(5) # pyright: ignore[reportArgumentType] + c0[0.1].extend(5) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] # SweepValue object has no getter, even if the parameter does with pytest.raises(AttributeError): - _ = c0[0.1].get # pyright: ignore[reportAttributeAccessIssue] + _ = c0[0.1].get # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute] def test_valid(c0: Parameter) -> None: From 9de34924884a4afa877e0b2d25d9b4c333d19b15 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 22 Oct 2026 21:05:44 +0200 Subject: [PATCH 65/70] Remove casts made redundant by test annotations The test inputs these casts were narrowing are now annotated, so the values already have the type the casts asserted. --- tests/drivers/test_keithley_26xx.py | 4 +- .../test_parameter_mixin_interdependent.py | 108 ++++++++---------- .../test_parameter_mixin_on_cache_change.py | 23 ++-- 3 files changed, 56 insertions(+), 79 deletions(-) diff --git a/tests/drivers/test_keithley_26xx.py b/tests/drivers/test_keithley_26xx.py index 0ddfd44b6d1c..61b1d4ca3017 100644 --- a/tests/drivers/test_keithley_26xx.py +++ b/tests/drivers/test_keithley_26xx.py @@ -1,6 +1,6 @@ import logging from collections import Counter -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING from unittest.mock import patch import numpy as np @@ -217,8 +217,6 @@ def test_setting_measure_current_range_disables_autorange( def test_fast_sweep_parameters(smus: "tuple[Keithley2600Channel, ...]") -> None: for smu in smus: - smu = cast("Keithley2600Channel", smu) - start_val = 0.0001 stop_val = 0.001 npts = 50 diff --git a/tests/extensions/parameters/test_parameter_mixin_interdependent.py b/tests/extensions/parameters/test_parameter_mixin_interdependent.py index b8d4a304a69f..dea4b22eb918 100644 --- a/tests/extensions/parameters/test_parameter_mixin_interdependent.py +++ b/tests/extensions/parameters/test_parameter_mixin_interdependent.py @@ -1,5 +1,5 @@ import re -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any import pytest @@ -94,29 +94,23 @@ def test_dependency_update_invoked_on_change( callback: "Callable[[], None]", mock_instr: MockInstrument, ) -> None: - mock_instr.some_param = cast( - "InterdependentParameter", - mock_instr.add_parameter( - name="some_param", - parameter_class=InterdependentParameter, - set_cmd=lambda x: store.update({"some": x}), - get_cmd=lambda: store.get("some"), - docstring="Parameter some_param represents a primary parameter.", - ), + mock_instr.some_param = mock_instr.add_parameter( + name="some_param", + parameter_class=InterdependentParameter, + set_cmd=lambda x: store.update({"some": x}), + get_cmd=lambda: store.get("some"), + docstring="Parameter some_param represents a primary parameter.", ) """Parameter some_param represents a primary parameter.""" - mock_instr.managed_param = cast( - "InterdependentParameter", - mock_instr.add_parameter( - name="managed_param", - parameter_class=InterdependentParameter, - dependent_on=["some_param"], - dependency_update_method=callback, - set_cmd=lambda x: store.update({"managed": x}), - get_cmd=lambda: store.get("managed"), - docstring="Parameter managed_param depends on some_param.", - ), + mock_instr.managed_param = mock_instr.add_parameter( + name="managed_param", + parameter_class=InterdependentParameter, + dependent_on=["some_param"], + dependency_update_method=callback, + set_cmd=lambda x: store.update({"managed": x}), + get_cmd=lambda: store.get("managed"), + docstring="Parameter managed_param depends on some_param.", ) """Parameter managed_param depends on some_param.""" @@ -130,29 +124,23 @@ def test_adding_dependent_parameter_later( callback: "Callable[[], None]", mock_instr: MockInstrument, ) -> None: - mock_instr.some_param = cast( - "InterdependentParameter", - mock_instr.add_parameter( - name="some_param", - parameter_class=InterdependentParameter, - set_cmd=lambda x: store.update({"some": x}), - get_cmd=lambda: store.get("some"), - docstring="Parameter some_param represents a primary parameter.", - ), + mock_instr.some_param = mock_instr.add_parameter( + name="some_param", + parameter_class=InterdependentParameter, + set_cmd=lambda x: store.update({"some": x}), + get_cmd=lambda: store.get("some"), + docstring="Parameter some_param represents a primary parameter.", ) """Parameter some_param represents a primary parameter.""" - mock_instr.managed_param = cast( - "InterdependentParameter", - mock_instr.add_parameter( - name="managed_param", - parameter_class=InterdependentParameter, - dependent_on=["some_param"], - dependency_update_method=callback, - set_cmd=lambda x: store.update({"managed": x}), - get_cmd=lambda: store.get("managed"), - docstring="Parameter managed_param depends on some_param.", - ), + mock_instr.managed_param = mock_instr.add_parameter( + name="managed_param", + parameter_class=InterdependentParameter, + dependent_on=["some_param"], + dependency_update_method=callback, + set_cmd=lambda x: store.update({"managed": x}), + get_cmd=lambda: store.get("managed"), + docstring="Parameter managed_param depends on some_param.", ) """Parameter managed_param depends on some_param.""" @@ -200,31 +188,25 @@ def test_parsers_and_dependency_propagation( def dependency_update() -> None: mock_instr.managed_param.set(999) - mock_instr.some_param = cast( - "InterdependentParameter", - mock_instr.add_parameter( - name="some_param", - parameter_class=InterdependentParameter, - set_cmd=lambda x: store.update({"some": x}), - get_cmd=lambda: store.get("some"), - set_parser=lambda v: v * 2, - get_parser=lambda v: v + 1 if v is not None else None, - docstring="Parameter some_param with parsers.", - ), + mock_instr.some_param = mock_instr.add_parameter( + name="some_param", + parameter_class=InterdependentParameter, + set_cmd=lambda x: store.update({"some": x}), + get_cmd=lambda: store.get("some"), + set_parser=lambda v: v * 2, + get_parser=lambda v: v + 1 if v is not None else None, + docstring="Parameter some_param with parsers.", ) """Parameter some_param with parsers.""" - mock_instr.managed_param = cast( - "InterdependentParameter", - mock_instr.add_parameter( - name="managed_param", - parameter_class=InterdependentParameter, - dependent_on=["some_param"], - dependency_update_method=dependency_update, - set_cmd=lambda x: store.update({"managed": x}), - get_cmd=lambda: store.get("managed"), - docstring="Parameter managed_param depends on some_param.", - ), + mock_instr.managed_param = mock_instr.add_parameter( + name="managed_param", + parameter_class=InterdependentParameter, + dependent_on=["some_param"], + dependency_update_method=dependency_update, + set_cmd=lambda x: store.update({"managed": x}), + get_cmd=lambda: store.get("managed"), + docstring="Parameter managed_param depends on some_param.", ) """Parameter managed_param depends on some_param.""" diff --git a/tests/extensions/parameters/test_parameter_mixin_on_cache_change.py b/tests/extensions/parameters/test_parameter_mixin_on_cache_change.py index cdd6c7dd7988..b5b1442660fa 100644 --- a/tests/extensions/parameters/test_parameter_mixin_on_cache_change.py +++ b/tests/extensions/parameters/test_parameter_mixin_on_cache_change.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any import pytest @@ -241,18 +241,15 @@ def cb(**kwargs: Any) -> None: ) ) - mock_instr.test_parameter = cast( - "OnCacheChangeParameter", - mock_instr.add_parameter( - name="test_parameter", - parameter_class=OnCacheChangeParameter, - on_cache_change=cb, - set_cmd=lambda x: store.update({"value": x}), - get_cmd=lambda: None if store.get("value") is None else store.get("value"), - set_parser=lambda v: v * 2, - get_parser=lambda v: None if v is None else v / 2, - docstring="A parameter with set_parser.", - ), + mock_instr.test_parameter = mock_instr.add_parameter( + name="test_parameter", + parameter_class=OnCacheChangeParameter, + on_cache_change=cb, + set_cmd=lambda x: store.update({"value": x}), + get_cmd=lambda: None if store.get("value") is None else store.get("value"), + set_parser=lambda v: v * 2, + get_parser=lambda v: None if v is None else v / 2, + docstring="A parameter with set_parser.", ) assert mock_instr.test_parameter.get() is None From 0156fc3fecc3dbaae972b8702bdbf6974940a24b Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 22 Oct 2026 21:10:42 +0200 Subject: [PATCH 66/70] Fix ty errors in dataset and Keysight 34465A tests Annotate the spectrum data as one dimensional so iterating it yields scalars, annotate the list of ParamSpecDicts so the dict literals are checked against the TypedDict, and mock the ask method of the Keysight 34465A with patch.object rather than assigning to the method. --- tests/dataset/conftest.py | 2 +- tests/dataset/test_paramspec.py | 2 +- tests/drivers/test_keysight_34465a.py | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/dataset/conftest.py b/tests/dataset/conftest.py index f6a65fc26939..d9f290009212 100644 --- a/tests/dataset/conftest.py +++ b/tests/dataset/conftest.py @@ -700,7 +700,7 @@ def __init__( self.stop = 2e6 self._rng = np.random.default_rng() - def get_data(self) -> NDArray[np.float64]: + def get_data(self) -> np.ndarray[tuple[int], np.dtype[np.float64]]: # This is how it should be: the setpoints are generated at the # time we get. But that will of course not work with the old Loop self.setpoints = (tuple(np.linspace(self.start, self.stop, self.npts)),) diff --git a/tests/dataset/test_paramspec.py b/tests/dataset/test_paramspec.py index 700e2718ff2c..dac8292361f9 100644 --- a/tests/dataset/test_paramspec.py +++ b/tests/dataset/test_paramspec.py @@ -55,7 +55,7 @@ class _ParamSpecKwargs(TypedDict): @pytest.fixture def version_0_dicts() -> "list[ParamSpecDict]": - sers = [] + sers: list[ParamSpecDict] = [] sers.append( { "name": "dmm_v1", diff --git a/tests/drivers/test_keysight_34465a.py b/tests/drivers/test_keysight_34465a.py index bcd5f175a6e6..612c81a03e97 100644 --- a/tests/drivers/test_keysight_34465a.py +++ b/tests/drivers/test_keysight_34465a.py @@ -1,4 +1,5 @@ from typing import TYPE_CHECKING, Protocol +from unittest.mock import patch import numpy as np import pytest @@ -50,9 +51,11 @@ def ask_with_read_mock(cmd: str) -> str: return ask_with_read_mock - keysight_sim.ask = get_ask_with_read_mock(keysight_sim.ask, val_volt) try: - yield keysight_sim + with patch.object( + keysight_sim, "ask", new=get_ask_with_read_mock(keysight_sim.ask, val_volt) + ): + yield keysight_sim finally: Keysight34465A.close_all() From f224565ce61dd9ec220848227c55bfc1fcf326c3 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 22 Oct 2026 21:13:03 +0200 Subject: [PATCH 67/70] Accept int keys in SweepFixedValues.__getitem__ The values are stored in a list, so integer keys already worked at runtime. --- src/qcodes/parameters/sweep_values.py | 2 +- tests/test_sweep_values.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/qcodes/parameters/sweep_values.py b/src/qcodes/parameters/sweep_values.py index abea5f25e7ce..236cee15f5b8 100644 --- a/src/qcodes/parameters/sweep_values.py +++ b/src/qcodes/parameters/sweep_values.py @@ -372,7 +372,7 @@ def snapshot_base( def __iter__(self) -> Iterator[Any]: return iter(self._values) - def __getitem__(self, key: slice) -> Any: + def __getitem__(self, key: int | slice) -> Any: return self._values[key] def __len__(self) -> int: diff --git a/tests/test_sweep_values.py b/tests/test_sweep_values.py index de6b97c4498e..e6ab07b854fe 100644 --- a/tests/test_sweep_values.py +++ b/tests/test_sweep_values.py @@ -74,8 +74,7 @@ def test_valid(c0: Parameter) -> None: assert c0_sv.set == c0.set # normal sequence operations access values assert list(c0_sv) == [1] - # TODO: SweepFixedValues.__getitem__ only accepts slice but int keys work - assert c0_sv[0] == 1 # pyright: ignore[reportArgumentType] + assert c0_sv[0] == 1 assert 1 in c0_sv assert 2 not in c0_sv From a433e6575ebc249f6c3f2807418e57d950ebd1e9 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 28 Oct 2026 07:02:58 +0100 Subject: [PATCH 68/70] Suppress ty on deliberately mistyped driver tests These tests pass values that do not match the annotated value type of the parameter on purpose, either to check that they are rejected or that the strings and integers accepted by the on/off mapping still work. They are already suppressed for pyright. --- .../keysight_b1500/b1500_driver_tests/test_b1520a_cmu.py | 2 +- tests/drivers/test_coppermountain_m5065.py | 6 +++--- tests/drivers/test_keithley_7510.py | 4 ++-- tests/drivers/test_keysight_n5222b.py | 2 +- tests/drivers/test_lakeshore_325.py | 4 ++-- tests/drivers/test_rigol_dg1062.py | 4 ++-- tests/drivers/test_rs_sgs100a.py | 2 +- tests/drivers/test_tektronix_dpo7200xx.py | 2 +- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1520a_cmu.py b/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1520a_cmu.py index 60d3c3ab0906..97dac49eecec 100644 --- a/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1520a_cmu.py +++ b/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1520a_cmu.py @@ -156,7 +156,7 @@ def test_ac_dc_volt_monitor_accepts_on_off_ints_and_strings( cmu: KeysightB1520A, value: int | str, expected: bool ) -> None: # while it does not type check, on/off ints and strings are accepted - cmu.ac_dc_volt_monitor(value) # pyright: ignore[reportArgumentType] + cmu.ac_dc_volt_monitor(value) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] assert cmu.ac_dc_volt_monitor() is expected diff --git a/tests/drivers/test_coppermountain_m5065.py b/tests/drivers/test_coppermountain_m5065.py index 2d8d31091f51..c44df17bed2f 100644 --- a/tests/drivers/test_coppermountain_m5065.py +++ b/tests/drivers/test_coppermountain_m5065.py @@ -76,7 +76,7 @@ def test_averages_accepts_whole_floats( # whole-number floats, e.g. from numpy, are accepted and sent as int write_raw = mocker.spy(vna, "write_raw") - vna.averages(value) # pyright: ignore[reportArgumentType] + vna.averages(value) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] write_raw.assert_called_once_with("SENS1:AVER:COUN 3") assert vna.averages() == 3 @@ -89,7 +89,7 @@ def test_averages_rejects_non_integer( write_raw = mocker.spy(vna, "write_raw") with pytest.raises(TypeError): - vna.averages(2.5) # pyright: ignore[reportArgumentType] + vna.averages(2.5) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] write_raw.assert_not_called() @@ -110,7 +110,7 @@ def test_averages_trigger_enabled_accepts_on_off_strings_and_ints( vna: CopperMountainM5065, value: str | int, expected: bool ) -> None: # while it does not type check, on/off strings and ints are accepted - vna.averages_trigger_enabled(value) # pyright: ignore[reportArgumentType] + vna.averages_trigger_enabled(value) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] assert vna.averages_trigger_enabled() is expected diff --git a/tests/drivers/test_keithley_7510.py b/tests/drivers/test_keithley_7510.py index 06e9d0921ea6..1fa1208376fe 100644 --- a/tests/drivers/test_keithley_7510.py +++ b/tests/drivers/test_keithley_7510.py @@ -93,7 +93,7 @@ def test_average_count_accepts_whole_floats( sense = dmm_7510_driver.sense write_raw = mocker.spy(dmm_7510_driver, "write_raw") - sense.average_count(value) # pyright: ignore[reportArgumentType] + sense.average_count(value) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] write_raw.assert_called_once_with(":SENSe:voltage:AVERage:COUNt 25") assert sense.average_count() == 25 @@ -105,7 +105,7 @@ def test_average_count_rejects_non_integer( write_raw = mocker.spy(dmm_7510_driver, "write_raw") with pytest.raises(TypeError): - dmm_7510_driver.sense.average_count(2.5) # pyright: ignore[reportArgumentType] + dmm_7510_driver.sense.average_count(2.5) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] write_raw.assert_not_called() diff --git a/tests/drivers/test_keysight_n5222b.py b/tests/drivers/test_keysight_n5222b.py index b3da3016ec24..4c4b72bc18f9 100644 --- a/tests/drivers/test_keysight_n5222b.py +++ b/tests/drivers/test_keysight_n5222b.py @@ -30,7 +30,7 @@ def test_active_trace(pna: KeysightN5222B) -> None: # while it does not type check, make sure a non-integer trace number is # rejected at runtime with pytest.raises(TypeError): - pna.active_trace(1.5) # pyright: ignore[reportArgumentType] + pna.active_trace(1.5) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] @pytest.mark.parametrize("name", ["averages", "points"]) diff --git a/tests/drivers/test_lakeshore_325.py b/tests/drivers/test_lakeshore_325.py index 0afaf8442e97..deb7181a478a 100644 --- a/tests/drivers/test_lakeshore_325.py +++ b/tests/drivers/test_lakeshore_325.py @@ -67,7 +67,7 @@ def test_curve_index_rejects_invalid( lakeshore_325: LakeshoreModel325, value: float ) -> None: with pytest.raises((TypeError, ValueError)): - lakeshore_325.sensor_A.curve_index(value) # pyright: ignore[reportArgumentType] + lakeshore_325.sensor_A.curve_index(value) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] @pytest.mark.parametrize("value", [23.0, np.float64(23)], ids=["float", "numpy_float"]) @@ -78,7 +78,7 @@ def test_curve_index_accepts_whole_floats( sensor_a = lakeshore_325.sensor_A write_raw = mocker.spy(lakeshore_325, "write_raw") - sensor_a.curve_index(value) # pyright: ignore[reportArgumentType] + sensor_a.curve_index(value) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] write_raw.assert_called_once_with("INCRV A, 23") assert sensor_a.curve_index() == 23 diff --git a/tests/drivers/test_rigol_dg1062.py b/tests/drivers/test_rigol_dg1062.py index 1013b1e8026d..287e75bf8677 100644 --- a/tests/drivers/test_rigol_dg1062.py +++ b/tests/drivers/test_rigol_dg1062.py @@ -40,7 +40,7 @@ def test_burst_ncycles_accepts_whole_floats( burst = dg1062.ch1.burst write_raw = mocker.spy(dg1062, "write_raw") - burst.ncycles(value) # pyright: ignore[reportArgumentType] + burst.ncycles(value) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] write_raw.assert_called_once_with(":SOUR1:BURS:NCYC 10") assert burst.ncycles() == 10 @@ -52,7 +52,7 @@ def test_burst_ncycles_rejects_non_integer( write_raw = mocker.spy(dg1062, "write_raw") with pytest.raises(TypeError): - dg1062.ch1.burst.ncycles(2.5) # pyright: ignore[reportArgumentType] + dg1062.ch1.burst.ncycles(2.5) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] write_raw.assert_not_called() diff --git a/tests/drivers/test_rs_sgs100a.py b/tests/drivers/test_rs_sgs100a.py index a40d158b27c7..dc808616758f 100644 --- a/tests/drivers/test_rs_sgs100a.py +++ b/tests/drivers/test_rs_sgs100a.py @@ -65,7 +65,7 @@ def test_status_accepts_on_off_strings_and_ints( sg: RohdeSchwarzSGS100A, value: str | int, expected: bool ) -> None: # while it does not type check, on/off strings and ints are accepted - sg.status(value) # pyright: ignore[reportArgumentType] + sg.status(value) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] assert sg.status() is expected diff --git a/tests/drivers/test_tektronix_dpo7200xx.py b/tests/drivers/test_tektronix_dpo7200xx.py index 13556e8f9ddb..ba32498f4f37 100644 --- a/tests/drivers/test_tektronix_dpo7200xx.py +++ b/tests/drivers/test_tektronix_dpo7200xx.py @@ -95,7 +95,7 @@ def test_measurement_state_accepts_on_off_strings_and_ints( tektronix_dpo: TektronixDPO7000xx, value: str | int, expected: bool ) -> None: # while it does not type check, on/off strings and ints are accepted - tektronix_dpo.measurement[1].state(value) # pyright: ignore[reportArgumentType] + tektronix_dpo.measurement[1].state(value) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] assert tektronix_dpo.measurement[1].state() is expected From f76fa145155b892b0ea0998235c204ed8e26632c Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 28 Aug 2026 22:17:04 +0200 Subject: [PATCH 69/70] Run ty as part of the CI type checking job Add ty to the test extra and run it next to mypy and pyright. ty understands Jupyter notebooks, which the other two do not, so this also covers the example notebooks in docs. The scipy stubs are added to the test extra for the notebooks that use scipy, and the paths to check are configured in pyproject.toml. --- .github/workflows/pytest.yaml | 4 ++++ docs/changes/newsfragments/8441.underthehood.8 | 5 +++++ pyproject.toml | 2 ++ requirements.txt | 2 ++ 4 files changed, 13 insertions(+) create mode 100644 docs/changes/newsfragments/8441.underthehood.8 diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index ed613c3d52f3..40bff7bcd474 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -116,6 +116,10 @@ jobs: run: mypy -p qcodes if: ${{ !matrix.min-version && always() }} id: mypy + - name: Run ty + run: ty check --output-format github + if: ${{ !matrix.min-version && always() }} + id: ty - name: Set pytest basetemp if: ${{ runner.os == 'Windows' }} run: echo "PYTEST_BASETEMP=--basetemp=D:\\tmp" >> $GITHUB_ENV diff --git a/docs/changes/newsfragments/8441.underthehood.8 b/docs/changes/newsfragments/8441.underthehood.8 new file mode 100644 index 000000000000..0ed8752f56e6 --- /dev/null +++ b/docs/changes/newsfragments/8441.underthehood.8 @@ -0,0 +1,5 @@ +The type checker ``ty`` is now a test dependency and is run as part of the +CI job that also runs ``mypy`` and ``pyright``. Unlike the other type +checkers ``ty`` understands Jupyter notebooks, so this type checks the +example notebooks in ``docs`` in addition to the source code in ``src`` +and the test suite. ``ty`` 0.0.87 or newer is required. diff --git a/pyproject.toml b/pyproject.toml index 2a2dec1898b7..93bd5fe91c56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,9 @@ test = [ "pytest-rerunfailures>=14.0", "pytest-xdist>=3.6.1", "PyVisa-sim>=0.6.0", + "scipy-stubs>=1.18.1.0", # type check docs examples using scipy "sphinx>=4.5.0", # sphinx extension tests + "ty>=0.0.87", "types-jsonschema>=4.16.0", "types-networkx >= 3.6.1.20260512", # minimum version required for generic data types in graph "types_requests>=0.1.8", diff --git a/requirements.txt b/requirements.txt index 2b02077ce98b..50ed48c13769 100644 --- a/requirements.txt +++ b/requirements.txt @@ -451,6 +451,8 @@ traitlets==5.16.1 # nbconvert # nbformat # nbsphinx +ty==0.0.87 + # via qcodes (pyproject.toml) types-jsonschema==4.26.0.20261006 # via qcodes (pyproject.toml) types-networkx==3.6.1.20261021 From c09d148d2063c08cb7ce635875c879b79949103d Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 19:42:57 +0200 Subject: [PATCH 70/70] Add working notes on ty and suppression comments A draft report for the ty issue that requires the wider TParameter default, and notes on how mypy, pyright and ty read each other's suppression comments, with the test cases used to establish that. They are kept in a single commit so that they are easy to drop before merging. --- suppression-codes-mypy-and-ty.md | 211 ++++++++++++++++++++++++++ ty-issue-2-typevar-default-context.md | 142 +++++++++++++++++ 2 files changed, 353 insertions(+) create mode 100644 suppression-codes-mypy-and-ty.md create mode 100644 ty-issue-2-typevar-default-context.md diff --git a/suppression-codes-mypy-and-ty.md b/suppression-codes-mypy-and-ty.md new file mode 100644 index 000000000000..8479b7d269c7 --- /dev/null +++ b/suppression-codes-mypy-and-ty.md @@ -0,0 +1,211 @@ +# Combining mypy and ty suppression codes + +## Summary + +The [ty suppression docs](https://docs.astral.sh/ty/suppression/) document putting +a ty rule into a mypy `type: ignore` comment by prefixing it with `ty:`: + +```python +sum_three_numbers("one", 5, 2) # type: ignore[arg-type, ty:invalid-argument-type] +``` + +ty honours this. **mypy does not ignore the `ty:` prefixed code**, and reports it +as an unused suppression when `warn_unused_ignores` is enabled, which qcodes +enables in `pyproject.toml`. So the combined form cannot be used here. + +qcodes therefore uses two comments on the same line: + +```python +f("one") # type: ignore[arg-type] # ty: ignore[invalid-argument-type] +``` + +That is the only form of the three below that all three checkers accept. + +## Results + +| form | ty 0.0.74 | mypy 2.3.1 with `warn_unused_ignores` | mypy 2.3.1 without it | pyright 1.1.411 | +| --- | --- | --- | --- | --- | +| `# type: ignore[arg-type, ty:invalid-argument-type]` | suppressed | `Unused "type: ignore[ty:invalid-argument-type]" comment` | clean | suppressed | +| `# type: ignore[arg-type]` + `# ty: ignore[invalid-argument-type]` | suppressed | clean | clean | suppressed | +| `# type: ignore[ty:invalid-argument-type]` | suppressed | unused, and `arg-type` not covered | `arg-type` not covered | suppressed | + +Note that the `arg-type` half of the combined form *is* honoured by mypy. It is +only the `ty:` prefixed code that mypy does not recognise, and therefore reports +as unused. + +## How pyright fits in + +pyright has its own suppression comment and also honours mypy's, which is why it +accepts all three forms above. + +| comment | pyright | +| --- | --- | +| `# type: ignore` | suppressed | +| `# type: ignore[arg-type]` | suppressed | +| `# type: ignore[arg-type, ty:invalid-argument-type]` | suppressed | +| `# pyright: ignore` | suppressed | +| `# pyright: ignore[reportArgumentType]` | suppressed | +| `# pyright: ignore[reportGeneralTypeIssues]` | **not** suppressed, wrong rule | +| `# ty: ignore[invalid-argument-type]` | **not** suppressed | + +Two things follow from this. + +**`# type: ignore` is a blanket suppression for pyright.** pyright does not parse +the codes in it, so `# type: ignore[arg-type]` silences *every* pyright rule on +that line, not just the argument type one. A consequence that came up repeatedly +during the ty migration: removing a mypy suppression can surface a pyright error +on the same line that was never visible before. `# pyright: ignore[rule]` is the +precise form, and unlike `# type: ignore` it only suppresses the rules listed. + +**A ty only suppression does not silence pyright.** `# ty: ignore[...]` is just a +comment as far as pyright is concerned. That is what makes the two comment form +safe: the mypy half keeps pyright quiet as a side effect, and the ty half is +inert for both of the others. + +## Unused suppression detection + +The three checkers differ in whether they tell you a suppression has gone stale. + +| checker | setting | default | reports unused | +| --- | --- | --- | --- | +| mypy | `warn_unused_ignores` | off | enabled in `pyproject.toml` | +| ty | `unused-ignore-comment` | on | yes, for `ty: ignore` directives | +| pyright | `reportUnnecessaryTypeIgnoreComment` | off | not enabled, see below | + +With the pyright setting enabled it reports all of these: + +```python +def g(a: int) -> None: ... + + +g(1) # type: ignore +g(1) # pyright: ignore +g(1) # pyright: ignore[reportArgumentType] +``` + +``` +Unnecessary "# type: ignore" comment +Unnecessary "# type: ignore" comment +Unnecessary "# pyright: ignore" rule: "reportArgumentType" +``` + +**We cannot enable it while we also run mypy.** Because pyright treats +`# type: ignore` as a blanket suppression of *its own* rules, it calls the +comment unnecessary whenever pyright itself has nothing to report on the line, +with no knowledge of whether mypy needed it. Every mypy only suppression in the +code base would be reported as unnecessary. For example: + +```python +from typing import Any + + +class A: + def m(self) -> None: ... + + +def make(a: A, replacement: Any) -> None: + # mypy reports method-assign here, pyright has no equivalent check + a.m = replacement # type: ignore[method-assign] +``` + +mypy needs that suppression: removing it gives +`error: Cannot assign to a method [method-assign]`. pyright with +`reportUnnecessaryTypeIgnoreComment` enabled reports the very same line as +`Unnecessary "# type: ignore" comment`. + +So mypy's `warn_unused_ignores` and ty's `unused-ignore-comment` are the two +stale suppression checks we can actually rely on. + +## Test case + +```python +def f(a: int) -> None: ... + + +# 1. combined form from the ty docs +f("one") # type: ignore[arg-type, ty:invalid-argument-type] + +# 2. the two comment form used in qcodes +f("one") # type: ignore[arg-type] # ty: ignore[invalid-argument-type] + +# 3. combined form, ty rule only +f("one") # type: ignore[ty:invalid-argument-type] + +# 4. control, expected to be reported by every checker +f("one") +``` + +And for the pyright specific forms: + +```python +def h(a: int) -> None: ... + + +# 5. pyright: ignore, blanket +h("one") # pyright: ignore + +# 6. pyright: ignore with the matching rule +h("one") # pyright: ignore[reportArgumentType] + +# 7. pyright: ignore with a non matching rule +h("one") # pyright: ignore[reportGeneralTypeIssues] + +# 8. ty: ignore only +h("one") # ty: ignore[invalid-argument-type] +``` + +Run from the repository root so that the mypy configuration in `pyproject.toml` +is picked up: + +``` +uv run ty check --output-format concise +uv run --extra test mypy +uv run --extra test mypy --no-warn-unused-ignores +uv run pyright +``` + +Expected results: + +| block | ty | mypy | pyright | +| --- | --- | --- | --- | +| first, cases 1 to 4 | 4 | 1, 3, 4 and two unused directives | 4 | +| second, cases 5 to 8 | 5, 6, 7 | 5, 6, 7, 8 | 7, 8 | + +The second block deliberately exercises comments that only one checker +understands, so most cases are reported by the other two. That is the point: it +shows that `pyright: ignore` is inert for mypy and ty, and that `ty: ignore` is +inert for mypy and pyright. + +Any deviation from this table tells you that one of the checkers has changed how +it reads these comments. + +## Why we keep `warn_unused_ignores` + +Dropping `warn_unused_ignores` would make the combined form work, but that +setting is worth more than the shorter comments. As shown above it is, together +with ty's `unused-ignore-comment`, one of only two stale suppression checks +available to us. During the ty migration it caught: + +- the `issuperset` suppression becoming redundant once + [astral-sh/ty#4303](https://github.com/astral-sh/ty/issues/4303) was fixed in + ty 0.0.74 +- the two suppressions in the Keithley 7510 buffer becoming unnecessary once the + data dictionary was annotated +- several suppressions in `ParameterBase` becoming unnecessary once the duck + typed conversions were moved behind helpers + +## Suggested upstream change + +mypy could ignore codes carrying a `:` prefix in `type: ignore` comments, +rather than treating them as mypy codes that turned out to be unused. That would +make the form documented by ty usable in projects that run both checkers with +`warn_unused_ignores` enabled, and would generalise to any other checker that +wants to share the comment. + +Failing that, the ty documentation could note that the combined form conflicts +with mypy's `warn_unused_ignores`, and suggest the two comment form for projects +that run both. + +## Versions + +Measured with ty 0.0.74, mypy 2.3.1 and pyright 1.1.411. diff --git a/ty-issue-2-typevar-default-context.md b/ty-issue-2-typevar-default-context.md new file mode 100644 index 000000000000..62eec02d3d0c --- /dev/null +++ b/ty-issue-2-typevar-default-context.md @@ -0,0 +1,142 @@ +# ty issue draft 2 + +**Title** + +> Function scoped `TypeVar` default takes precedence over the declared type context, where an unsolved type variable would be accepted + +**Labels to suggest:** `bidirectional inference`, `constraint-solver`, `generics` + +--- + +### Summary + +When a function scoped type variable appears only in the return type and is not +constrained by any argument, ty leaves it unsolved as `Unknown`, which is +gradually compatible with whatever the result is assigned to. If that same type +variable declares a PEP 696 default, ty substitutes the default instead, which +is concrete and then conflicts with the declared type. + +```python +class Box[T]: + pass + + +def make[T = int](cls: type[T] | None = None) -> Box[T]: + raise NotImplementedError + + +def caller() -> None: + a: Box[str] = make() +``` + +``` +error[invalid-assignment]: Object of type `Box[int]` is not assignable to `Box[str]` + --> repro.py:8:19 + | +8 | a: Box[str] = make() + | ^^^^^^ +``` + +Removing the default makes ty accept it: + +```python +class Box[T]: + pass + + +def make[T](cls: type[T] | None = None) -> Box[T]: + raise NotImplementedError + + +def caller() -> None: + a: Box[str] = make() # ty: ok +``` + +`reveal_type` shows what is actually happening. The declared type is never used +to solve `T` in either case; the difference is only what fills the unsolved slot: + +| declaration | `reveal_type(make())` | `a: Box[str] = make()` | +| --- | --- | --- | +| `def make[T](...) -> Box[T]` | `Box[Unknown]` | accepted | +| `def make[T = int](...) -> Box[T]` | `Box[int]` | error | + +So adding a default is strictly worse than having no default at all, at every +call site that annotates its target. mypy 2.3.1 and pyright accept both forms. + +### The type context is available + +This is not a case of ty lacking the necessary context. Using the example from +#3933, the declared type of the assignment target clearly does reach the +constraint solver, since it widens the argument: + +```python +class Parent: ... + + +class Child(Parent): ... + + +def head[T](x: list[T]) -> T: + return x[0] + + +x: Parent = head(reveal_type([Child()])) # revealed: list[Parent] +``` + +I reproduced that on 0.0.74. So in `a: Box[str] = make()` the constraint +`Box[T] <: Box[str]` is available, but the default is applied in preference to +it. + +### Why this matters + +This pattern is common in factory functions, where the default exists to give a +sensible type to an unannotated call while still allowing the caller to ask for +something more specific (illustrative, from our codebase): + +```python +p = instrument.add_parameter("name") # want the default +q: Parameter[float, Self] = instrument.add_parameter("x") # want this instead +``` + +With ty's current behaviour the default wins in both cases, so the second form +is unusable and every annotated call site becomes an error. In our codebase this +produced 34 errors across instrument drivers from a single type variable +declaration. We ended up widening the default to a fully gradual type to work +around it, which loses the information the default was there to provide. + +### Relation to #3933 and the feature overview + +This looks like it may fall under #3933, constraint-set-aware bidirectional +inference. That issue is written in terms of constraints flowing into *argument* +inference, and all of its examples involve arguments that get eagerly +specialized or wrongly widened. The case here has no arguments at all, so the +symptom is different, but the underlying gap looks similar: the outer constraint +is not being unified with the specialization of the call. + +If the second approach in #3933 is taken, propagating constraints during +bidirectional inference rather than eagerly specializing, then `Box[T] <: +Box[str]` should presumably solve `T` to `str` before any default is considered, +which would fix this too. Filing separately in case that is not the intent, and +because the interaction with PEP 696 defaults is not mentioned there. + +The type system feature overview in #1889 lists "`TypeVar` defaults (PEP 696)" +as implemented under Generics. That section also has an open sub-item, "Solve +type variables in all cases" (#623), which may be the more appropriate home if +this is considered a solver limitation rather than a deliberate choice about +defaults. + +### Note on the spec + +I could not find wording in PEP 696 or the typing spec that settles whether the +declared type context should take precedence over a type variable default, so +this may be intentional. If it is, it would be helpful to say so explicitly, +since the natural reading of "the default is used when the type variable cannot +be solved" is that a solution derived from the type context counts as solving +it. The current behaviour also has the surprising property that adding a default +makes a call site fail that would otherwise have been accepted. + +Reproduced on 0.0.72, 0.0.73 and 0.0.74, checked with `--python-version 3.13`. + +### Version + +0.0.74