From 33b590772644552b73b645afe2bbde58053670a6 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 19 Aug 2026 07:18:07 +0200 Subject: [PATCH 01/66] Fix isinstance validator logic order --- 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 6bb6ad27ebf..e195d31feb3 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): shape_array.append(s) + else: + shape_array.append(s()) shape = tuple(shape_array) return shape From 2a441b57f3e747325b926f718059378e5934d219 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 19 Aug 2026 07:53:46 +0200 Subject: [PATCH 02/66] 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 57155d5267b..712e2acb474 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -453,6 +453,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 b5c47f49ccf2e8fe6c82636ea72850d0a30502ea Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 19 Aug 2026 20:04:20 +0200 Subject: [PATCH 03/66] 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 00000000000..b7ee1dd573a --- /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 f81dcc9d303..564da889852 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 4b0a0acb03d609693eb12eb2fc4de33bad5c8046 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 19 Aug 2026 20:15:46 +0200 Subject: [PATCH 04/66] 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 00000000000..674cd1067ae --- /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 733bfa0ecb6..b94e33a0bfa 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 b2ba6dec987..3b77fed4569 100644 --- a/tests/test_instrument.py +++ b/tests/test_instrument.py @@ -212,8 +212,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 340550e9909ae78977ba5d59d5600666e3056830 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:17:24 +0200 Subject: [PATCH 05/66] 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 00000000000..81eef08c23d --- /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 9364b2f85fb..a5352d9a31e 100644 --- a/src/qcodes/dataset/data_set.py +++ b/src/qcodes/dataset/data_set.py @@ -1406,28 +1406,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 1c6f3c04bcf8fff93f3252ada57356f7db797a01 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:17:30 +0200 Subject: [PATCH 06/66] 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 00000000000..7cab22c1c78 --- /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 425ab143f5e..2432ae04845 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 5f4bb2a20bd4538b3db6ae7f040c99dc3dd6172d Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:21:27 +0200 Subject: [PATCH 07/66] 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 ce85b2774c8..e611e8f3b2f 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 e4145f77ab3d9e1c7bec4049ddc575204af959db Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:21:41 +0200 Subject: [PATCH 08/66] 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 26d0ac3611f..ed6db0ecbf4 100644 --- a/src/qcodes/utils/abstractmethod.py +++ b/src/qcodes/utils/abstractmethod.py @@ -16,7 +16,7 @@ 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 b345ce7193271147e393a506189315127942f295 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:23:22 +0200 Subject: [PATCH 09/66] 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 0494c21f161..d08f587de90 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 e6006475ec65cd4da29ef3941cb3a44fa982516e Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:28:54 +0200 Subject: [PATCH 10/66] 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 00000000000..3317ec3768c --- /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 86e6405e051..5c6a1f358b3 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 01fe50e3a2e..dc82f169267 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 3b108e973c3f4ba13ebfd5f235b73f694ec42624 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:30:32 +0200 Subject: [PATCH 11/66] 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 0be4fff4b70..f4431839c4c 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 399dfee728546e9c0dff25f7e7166f5932b957d6 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:30:55 +0200 Subject: [PATCH 12/66] 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 67d61acb421..1183509a3f5 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 d380edae777d66b2ecf357d3a6abc666405befb5 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:31:17 +0200 Subject: [PATCH 13/66] 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 4e4725021f9..a16208682a7 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 c6be0b3a0a59c2863d0dbee98ecb23c96b17fceb Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:32:25 +0200 Subject: [PATCH 14/66] 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 00000000000..10becb126c8 --- /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 8e897d45518..dc1b9f7c91b 100644 --- a/src/qcodes/instrument_drivers/tektronix/AWG5014.py +++ b/src/qcodes/instrument_drivers/tektronix/AWG5014.py @@ -605,7 +605,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. @@ -613,7 +613,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") @@ -629,7 +629,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, @@ -638,12 +638,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 12b14f0989b404fb69c134128943aaa67f0fac2e Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:32:51 +0200 Subject: [PATCH 15/66] 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 564da889852..c8acb6b4831 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 c152c306e02cbc620df5573beead69387e6a0223 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:33:14 +0200 Subject: [PATCH 16/66] 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 1b8b9ff9d8e..2ad9601ae4f 100644 --- a/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py +++ b/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py @@ -690,9 +690,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 2467d1b5f6fbe30111fc7d0069af1a5953693889 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:33:37 +0200 Subject: [PATCH 17/66] 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 edd9ca9cbc8..fdeafd7c670 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 bc8a2bd3d52badbf1912b577ff27396e7bac3997 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:35:03 +0200 Subject: [PATCH 18/66] 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 e596439f46a..590a94fb18a 100644 --- a/src/qcodes/dataset/sqlite/db_overview.py +++ b/src/qcodes/dataset/sqlite/db_overview.py @@ -253,7 +253,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 9910db1c98fd4dac8194927e4404edd6fd4c442b Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:35:55 +0200 Subject: [PATCH 19/66] 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 e21eaa5d85d..4f313ca2a49 100644 --- a/src/qcodes/instrument/channel.py +++ b/src/qcodes/instrument/channel.py @@ -1199,7 +1199,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 377e6d375d4bf6d0f8367d7c1abb970307f176e5 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:36:25 +0200 Subject: [PATCH 20/66] 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 4f313ca2a49..86855901e18 100644 --- a/src/qcodes/instrument/channel.py +++ b/src/qcodes/instrument/channel.py @@ -725,9 +725,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 18ac6c2e466bc1a6210191732606f30ad6a507b5 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:37:12 +0200 Subject: [PATCH 21/66] 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 e2da0b14d01..0561dab1dab 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 6df9bb70b47f4d3e9d1a358a138a0b34c55f7ff5 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 08:09:30 +0200 Subject: [PATCH 22/66] 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 5c6a1f358b3..81be36318a2 100644 --- a/src/qcodes/dataset/sqlite/database.py +++ b/src/qcodes/dataset/sqlite/database.py @@ -174,7 +174,10 @@ def connect( 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 940b03957828134c7311012f059ecf19d585238f Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 19:35:27 +0200 Subject: [PATCH 23/66] Suppress the get_ramp_values mismatch newly reported by ty 0.0.74 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 has always reported. The constraint solver changes in 0.0.74 mean ty now 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 16d96c8b08e..2169c173dcb 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 c08392407ed6932c4ddb0d9b04e3934da33e607d Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 19:42:57 +0200 Subject: [PATCH 24/66] Add a draft report for the remaining ty issue A function scoped TypeVar default is preferred over the declared type context, which is why the default of TParameter had to be widened. Keeping the draft alongside the code it explains, so that the repro stays with the workaround it describes. --- ty-issue-2-typevar-default-context.md | 142 ++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 ty-issue-2-typevar-default-context.md diff --git a/ty-issue-2-typevar-default-context.md b/ty-issue-2-typevar-default-context.md new file mode 100644 index 00000000000..62eec02d3d0 --- /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 From 6f2c10f7b90346fc513db001ea286c14a48b4ed5 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 19:56:55 +0200 Subject: [PATCH 25/66] Document how mypy and ty suppression codes interact ty documents putting a ty rule into a mypy type: ignore comment by prefixing it with ty:. mypy does not recognise the prefixed code and reports it as unused when warn_unused_ignores is enabled, which we enable, so we use two comments on one line instead. Record the test case and the commands to run it, so the conclusion can be rechecked when either checker changes. --- suppression-codes-mypy-and-ty.md | 100 +++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 suppression-codes-mypy-and-ty.md diff --git a/suppression-codes-mypy-and-ty.md b/suppression-codes-mypy-and-ty.md new file mode 100644 index 00000000000..98128dc46ed --- /dev/null +++ b/suppression-codes-mypy-and-ty.md @@ -0,0 +1,100 @@ +# 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. + +pyright honours a `# type: ignore` comment regardless of the codes in it, so it +accepts all three forms. That is also why removing a mypy suppression can +surface a pyright error on the same line. + +## 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") +``` + +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 +``` + +Only case 4 should be reported. Every checker reporting anything on cases 1 to 3 +tells you which form is currently supported. + +## 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. It is what tells us when a +suppression has become obsolete. 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. From 2137afa62d31e705618488d671d65c2e7b246efc Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 20:03:08 +0200 Subject: [PATCH 26/66] Document how pyright reads suppression comments pyright honours mypy's type: ignore as a blanket suppression of its own rules, ignoring the codes in it, which is why removing a mypy suppression can surface a pyright error on the same line. It does not read ty: ignore at all. Also record why we cannot enable reportUnnecessaryTypeIgnoreComment: it calls a comment unnecessary whenever pyright itself has nothing to report, so every mypy only suppression would be flagged. --- suppression-codes-mypy-and-ty.md | 125 +++++++++++++++++++++++++++++-- 1 file changed, 118 insertions(+), 7 deletions(-) diff --git a/suppression-codes-mypy-and-ty.md b/suppression-codes-mypy-and-ty.md index 98128dc46ed..8479b7d269c 100644 --- a/suppression-codes-mypy-and-ty.md +++ b/suppression-codes-mypy-and-ty.md @@ -33,9 +33,88 @@ 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. -pyright honours a `# type: ignore` comment regardless of the codes in it, so it -accepts all three forms. That is also why removing a mypy suppression can -surface a pyright error on the same line. +## 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 @@ -56,6 +135,25 @@ f("one") # type: ignore[ty:invalid-argument-type] 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: @@ -66,14 +164,27 @@ uv run --extra test mypy --no-warn-unused-ignores uv run pyright ``` -Only case 4 should be reported. Every checker reporting anything on cases 1 to 3 -tells you which form is currently supported. +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. It is what tells us when a -suppression has become obsolete. During the ty migration it caught: +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 From 41af6397a878189aa73681932346614c9fba3da2 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 21:08:32 +0200 Subject: [PATCH 27/66] 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 00000000000..e519a10b079 --- /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 dcf4ac3fef1..6c60aa79e4d 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 330b683a7afc414a8dbc7b5e090e1d8297a9a2a4 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 21:09:48 +0200 Subject: [PATCH 28/66] 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 00000000000..e0c385c8e16 --- /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 a5352d9a31e..ba9c43c237d 100644 --- a/src/qcodes/dataset/data_set.py +++ b/src/qcodes/dataset/data_set.py @@ -1144,7 +1144,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 8d8312ba46607220c9233afa19dae0b49a43bc47 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 21:13:04 +0200 Subject: [PATCH 29/66] Type the 34980A module dict as its submodules self.module was built with dict.fromkeys, so its values were typed as possibly None even though scan_slots fills in every slot, either with the driver for the installed module or with a generic submodule. Every use of instrument.module[slot] therefore had to account for a None that cannot occur. Start from an empty dict of the submodule type and test membership rather than None, which keeps the behaviour of scan_slots unchanged for a repeated call. The notebook keeps one suppression: it sets _is_locked to demonstrate the safety interlock, and that attribute belongs to the 34934A driver rather than to the shared submodule base class. --- docs/changes/newsfragments/8441.improved.2 | 7 +++++++ ...with Keysight 34980A Switch Mainframe and Modules.ipynb | 7 ++++--- src/qcodes/instrument_drivers/Keysight/keysight_34980a.py | 5 +++-- 3 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 docs/changes/newsfragments/8441.improved.2 diff --git a/docs/changes/newsfragments/8441.improved.2 b/docs/changes/newsfragments/8441.improved.2 new file mode 100644 index 00000000000..b90716a9f22 --- /dev/null +++ b/docs/changes/newsfragments/8441.improved.2 @@ -0,0 +1,7 @@ +``Keysight34980A.module`` is now a ``dict`` of +``Keysight34980ASwitchMatrixSubModule`` rather than one built with +``dict.fromkeys``, whose values were typed as possibly ``None``. ``scan_slots`` +puts an entry in for every slot, either the driver for the installed module or a +generic submodule, so the values were never ``None`` once the instrument was +constructed. Code using ``instrument.module[slot]`` no longer has to account for +a ``None`` that cannot occur. 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 3726074aa3e..b1a64ba32a6 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]" ] }, { diff --git a/src/qcodes/instrument_drivers/Keysight/keysight_34980a.py b/src/qcodes/instrument_drivers/Keysight/keysight_34980a.py index 576b9888251..f93c08a149f 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysight_34980a.py +++ b/src/qcodes/instrument_drivers/Keysight/keysight_34980a.py @@ -70,7 +70,8 @@ def __init__( self._total_slot = 8 self._system_slots_info_dict: dict[int, dict[str, str]] | None = None - self.module = dict.fromkeys(self.system_slots_info.keys()) + # populated by scan_slots below, which puts an entry in for every slot + self.module: dict[int, Keysight34980ASwitchMatrixSubModule] = {} self.scan_slots() self.connect_message() @@ -132,7 +133,7 @@ def scan_slots(self) -> None: self.module[slot] = sub_module self.add_submodule(sub_module_name, sub_module) break - if self.module[slot] is None: + if slot not in self.module: sub_module_name = f"slot_{slot}_{model_string}_no_driver" sub_module_no_driver = Keysight34980ASwitchMatrixSubModule( self, sub_module_name, slot From 3b53ea090a62c074fd72d554425b8b781078e2bb Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 21:18:50 +0200 Subject: [PATCH 30/66] 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 00000000000..0437c9e88e2 --- /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 9c7ed36b4c0..da64e7919eb 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 b11d441be43ad6de8d9b70218e8210b746cda001 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 21:20:13 +0200 Subject: [PATCH 31/66] 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 abe47966ccc..494e9a4697a 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 53496d6fb22fc0c2ffc879927c56c435a4b14b5d Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 07:45:00 +0200 Subject: [PATCH 32/66] 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 712e2acb474..7ce096ea908 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,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", "deepdiff>=8.6.1", @@ -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", # typecheck zhinst driver alias "libcst>=1.2.0", # refactor tests ] From e3cd7501afab818692e8e06704eff7600e882b65 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:00:21 +0200 Subject: [PATCH 33/66] 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 7ce096ea908..ba9a8a0aae1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,6 +89,7 @@ test = [ "qcodes_loop>=0.2.3", "zhinst.qcodes>=0.5", # typecheck zhinst driver alias "libcst>=1.2.0", # refactor tests + "intersphinx_registry>=0.2603.16" # typecheck docs/conf.py ] docs = [ "autodocsumm>=0.2.9", @@ -460,8 +461,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", ] @@ -485,6 +487,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 824addec577eed8bcf844b6289defc841cad8b11 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:21:09 +0200 Subject: [PATCH 34/66] 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 fffbf3d1222..90da3e16552 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 ad0dec38deaf358533b7ee8a152fceb7bca9620e Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:21:38 +0200 Subject: [PATCH 35/66] 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 00000000000..b124c324651 --- /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 90da3e16552..e5ee5a34304 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 afc81b064fd1f1704dcd2be91bdd0a670de46a50 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:25:30 +0200 Subject: [PATCH 36/66] 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 00000000000..3727910a2a5 --- /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 5b339d2e90b..0c0e0f2107b 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 f3eb283edcf2693021dd618bd6b80322ed37bf81 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:33:51 +0200 Subject: [PATCH 37/66] 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 00000000000..786b261bb83 --- /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 4adf9855f8f..1f8aae138e1 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 3d802ebce06dc6d935af5204930169a74aead48c Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:38:11 +0200 Subject: [PATCH 38/66] 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 00000000000..ffc16e01e34 --- /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 d6c4f9ee2c9..c8e01b51962 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 33e1d1fa4c9da812d815b72640561a804ff70130 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:40:55 +0200 Subject: [PATCH 39/66] 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 00000000000..60fc859b5b8 --- /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 b206bf73347..a91d4f3d49b 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 67c44060a61..ae9ad9b0d37 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 2b8e92f0d7def9204eff68f9b158845c257679cb Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:57:53 +0200 Subject: [PATCH 40/66] 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 00000000000..7d3920c0da1 --- /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 44a36f8baed..042ed8d79c9 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 8837ebb9e9f..b8c566172e2 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 89bc65603d26c68ad320ada6534695b7b33bd4ae Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 08:02:26 +0200 Subject: [PATCH 41/66] 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 00000000000..b2f6f471d3d --- /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 dfddf0e41b0..164fafe2ed9 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 9753a1af56d7c28dce7fa9cd995e217fff4e7b7e Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 08:07:51 +0200 Subject: [PATCH 42/66] 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 00000000000..080e53462aa --- /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 241b789ba3b..5fe228c189f 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 cd31082e880..339e11ab5a7 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 217dc94c99e..ec80c28bf1a 100644 --- a/tests/dataset/test_dataset_export.py +++ b/tests/dataset/test_dataset_export.py @@ -1496,11 +1496,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 b723033574e..1a72ec0c7e1 100644 --- a/tests/dataset/test_snapshot.py +++ b/tests/dataset/test_snapshot.py @@ -68,9 +68,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 2d96b0b35f0ebe5f3758a4c9adfd5f448646e956 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 08:08:26 +0200 Subject: [PATCH 43/66] 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 00000000000..04a44aad1a7 --- /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 49d42c62d4e..801126e0a1b 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 7609756498f25fc9ab010db93c5620aba2200787 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 27 Aug 2026 16:09:29 +0200 Subject: [PATCH 44/66] Use type safe dataset unpacking --- .../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 3705dc28438..750ca3d212d 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 e3977ce9a24359f27305cea7a5f13f5b8f96883b Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 27 Aug 2026 16:25:05 +0200 Subject: [PATCH 45/66] 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 fd2977868fc..20d10cb774b 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 3cd2ca6c7bc..cb672c7fb20 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 05c86968d74ba32499f8619fcfa995ea4a0e0842 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 27 Aug 2026 16:32:20 +0200 Subject: [PATCH 46/66] Allow ParamSpecBase in get_parameter_data --- .../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 e3909e71bdc..15b33bcd9d9 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 ba9c43c237d..122126d4de1 100644 --- a/src/qcodes/dataset/data_set.py +++ b/src/qcodes/dataset/data_set.py @@ -810,7 +810,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 5fe228c189f..d42fac295d5 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 339e11ab5a7..62100c61703 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 e1df6d8c3e3bcc8ce8eec5f8d7e8ff9e19189c96 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 27 Aug 2026 16:34:48 +0200 Subject: [PATCH 47/66] 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 0a02fb04a7c..b6f2229db17 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 e000af077bb69f51c235e1a432ac1d6e9d47b926 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 27 Aug 2026 16:37:12 +0200 Subject: [PATCH 48/66] 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 f5d488a6c9f..aff6c052798 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 ed88c2f57e80eb5294f3c283fc490b9461e55e44 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 27 Aug 2026 16:48:42 +0200 Subject: [PATCH 49/66] 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 ae5e2c38cbd..0f41089fcd0 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 c86165c1306..8225df8c836 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 0358ae1f4c9..1db1eb9e5b0 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 4a78c18780e0fcd84deb5e9b562e79f7a3fbd699 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 28 Aug 2026 21:51:03 +0200 Subject: [PATCH 50/66] Fix ty typechecking of docs hack --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 0514625c4bd..135398f4799 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 446bd85cfd4db25bc0dee5c527eed84b902b4f48 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 28 Aug 2026 21:52:26 +0200 Subject: [PATCH 51/66] 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 44e9fe09ef0..9c360867a7e 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 8ea718a348e..930555694e9 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 d612a29da4ae171c47d7759f90851d9a7414bf4a Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 28 Aug 2026 22:06:27 +0200 Subject: [PATCH 52/66] 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 00000000000..3a3cbdae81b --- /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 62100c61703..8e95a7d67e1 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 b98a13ac0330207c88b200bc354073d6526eb582 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 28 Aug 2026 22:17:04 +0200 Subject: [PATCH 53/66] 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 fbccdd912a4..0950365f902 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -103,6 +103,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 00000000000..66b6742332a --- /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.80 or newer is required. diff --git a/pyproject.toml b/pyproject.toml index ba9a8a0aae1..e379844b6ca 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.80", "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 f70f6df5650..3810ebf596b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -457,6 +457,8 @@ traitlets==5.16.1 # nbconvert # nbformat # nbsphinx +ty==0.0.80 + # via qcodes (pyproject.toml) types-jsonschema==4.26.0.20260518 # via qcodes (pyproject.toml) types-networkx==3.6.1.20260911 From eadbfb78d99c94ae6bb0bec6bca5aa39eeec5e8a Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:10:44 +0200 Subject: [PATCH 54/66] 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 | 6 ++-- 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, 187 insertions(+), 187 deletions(-) diff --git a/tests/dataset/measurement/test_measurement_context_manager.py b/tests/dataset/measurement/test_measurement_context_manager.py index bac839bb628..5ac9da06dd2 100644 --- a/tests/dataset/measurement/test_measurement_context_manager.py +++ b/tests/dataset/measurement/test_measurement_context_manager.py @@ -85,10 +85,10 @@ def test_register_parameter_arg_types(DAC, DMM): 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, DMM) -> None: @@ -102,7 +102,7 @@ def test_register_parameter_numbers(DAC, DMM) -> None: 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) @@ -979,7 +979,7 @@ def test_datasaver_foul_input(bg_writing) -> 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 a832f736963..3b1f397280b 100644 --- a/tests/dataset/test_converters.py +++ b/tests/dataset/test_converters.py @@ -164,11 +164,11 @@ def test_construct_current_rundescriber_from_fake_v4(some_interdeps) -> None: 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 1a4f965adec..5545d98dc62 100644 --- a/tests/dataset/test_datasaver.py +++ b/tests/dataset/test_datasaver.py @@ -168,7 +168,7 @@ def test_saving_numeric_values_as_text(numeric_type, bg_writing) -> None: 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") @@ -200,4 +200,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 bd3e6ae77a0..36225b7329a 100644 --- a/tests/dataset/test_dataset_basic.py +++ b/tests/dataset/test_dataset_basic.py @@ -316,7 +316,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) @@ -813,7 +813,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 5e4fdaf63bf..218f3c93b48 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 d43699c8c9d..50ad68e19a2 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(some_paramspecbases) -> None: @@ -78,18 +78,18 @@ def test_init_validation_raises(some_paramspecbases) -> None: 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") @@ -101,8 +101,8 @@ def test_init_validation_raises(some_paramspecbases) -> None: 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 248c52ebfc9..ec03c7cfa01 100644 --- a/tests/dataset/test_descriptions.py +++ b/tests/dataset/test_descriptions.py @@ -12,7 +12,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(some_paramspecbases) -> None: diff --git a/tests/dataset/test_parameter_with_setpoints_has_control.py b/tests/dataset/test_parameter_with_setpoints_has_control.py index d2aae82e39f..b16653a666a 100644 --- a/tests/dataset/test_parameter_with_setpoints_has_control.py +++ b/tests/dataset/test_parameter_with_setpoints_has_control.py @@ -29,7 +29,7 @@ def unpack_self(self, value): # type: ignore[override] 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 ed344a01203..fea4b0e957f 100644 --- a/tests/dataset/test_paramspec.py +++ b/tests/dataset/test_paramspec.py @@ -107,7 +107,7 @@ def test_creation(name, sp1, sp2, inff1, inff2, paramtype) -> None: 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 1a72ec0c7e1..058bcb92754 100644 --- a/tests/dataset/test_snapshot.py +++ b/tests/dataset/test_snapshot.py @@ -58,7 +58,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 a48978d00ae..3883f1c75eb 100644 --- a/tests/dataset/test_sqlite_base.py +++ b/tests/dataset/test_sqlite_base.py @@ -224,7 +224,7 @@ def test_get_layout_id_with_invalid_parameter_type(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 54747dd2b95..2847f32358d 100644 --- a/tests/dataset/test_sqlite_connection.py +++ b/tests/dataset/test_sqlite_connection.py @@ -58,7 +58,7 @@ def test_atomic_raises_for_non_atomic_conn() -> None: "atomic context manager only accepts AtomicConnection " "database connection objects." ) - with pytest.raises(ValueError, match=match_str), atomic(sqlite_conn): # type: ignore[arg-type] + with pytest.raises(ValueError, match=match_str), atomic(sqlite_conn): # type: ignore[arg-type] # ty: ignore[invalid-argument-type] pass @@ -295,7 +295,7 @@ def test_atomic_transaction_on_sqlite3_connection_raises(tmp_path) -> None: ) with 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 928ffbd7069..41cc2837502 100644 --- a/tests/dataset/test_string_data.py +++ b/tests/dataset/test_string_data.py @@ -138,7 +138,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") @@ -194,7 +194,7 @@ def test_list_of_strings(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 a712f01fad6..53846dd5939 100644 --- a/tests/delegate/test_delegate_instrument.py +++ b/tests/delegate/test_delegate_instrument.py @@ -74,5 +74,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 6c3a094568d..a3da6f5c448 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 3bb380f1458..2f00af6ebae 100644 --- a/tests/drivers/test_ami430_visa.py +++ b/tests/drivers/test_ami430_visa.py @@ -414,7 +414,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, ) @@ -1873,7 +1873,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 4659e073297..e15d88e5db5 100644 --- a/tests/drivers/test_lakeshore_372.py +++ b/tests/drivers/test_lakeshore_372.py @@ -36,7 +36,7 @@ class MockVisaInstrument: def __init__(self, *args, **kwargs) -> 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() @@ -74,7 +74,7 @@ def write_raw(self, cmd) -> 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) -> Any: query_parts = cmd.split(" ") @@ -88,12 +88,12 @@ def ask_raw(self, cmd) -> 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(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 @@ -101,7 +101,7 @@ def wrapper(func: Callable[P, T]) -> Callable[P, T]: def command(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 d6e19e6df29..3b00a1bb35e 100644 --- a/tests/drivers/test_mercuryips.py +++ b/tests/drivers/test_mercuryips.py @@ -92,7 +92,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 34ee5c44996..f71985c66e4 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 31831a60c25..680d2a23d86 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 99c955aefaf..badaceb8222 100644 --- a/tests/drivers/test_tektronix_awg70000a.py +++ b/tests/drivers/test_tektronix_awg70000a.py @@ -175,7 +175,7 @@ def test_seqxfilefromfs_failing(forged_sequence) -> None: 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 69d54084cf1..300c4871054 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 5dc8e39ddbd..35f8c6cf4d9 100644 --- a/tests/helpers/test_strip_attrs.py +++ b/tests/helpers/test_strip_attrs.py @@ -23,7 +23,7 @@ def __delitem__(self, item): 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 8d9ff7cc75e..5f6a0831d3b 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 55278f3f9aa..e9aca5043e0 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 3add38e4515..f5dc73174f3 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 856f7443933..578b1088203 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): 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 3bbe3416d5b..7938d72c0d7 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): self._value = initial_value super().__init__(*args, **kwargs) diff --git a/tests/parameter/test_group_parameter.py b/tests/parameter/test_group_parameter.py index 9464b031e1c..931225489b7 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 7c710473dfe..12ea03866ac 100644 --- a/tests/parameter/test_issequenceof.py +++ b/tests/parameter/test_issequenceof.py @@ -36,9 +36,9 @@ def test_simple_bad(args) -> 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 3ae82f64cdb..3927d1962b6 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 f1e21c50543..c26d3da586a 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 5e60778a965..fd5580bc0ae 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 2bb4df4f8ed..a9839576cbe 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 18a5446e137..aa2356f049e 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 ca1220f063a..579cbf0384d 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): 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 9f30bcc5838..54679bcfef8 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 c25a69afc2c..56058e6a6b4 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 497fe08a627..288c82beb51 100644 --- a/tests/parameter/test_parameter_with_setpoints.py +++ b/tests/parameter/test_parameter_with_setpoints.py @@ -103,7 +103,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,)), ) @@ -114,7 +114,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: @@ -322,7 +322,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 " @@ -349,7 +349,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 9c9a0092eb2..2bcb9f6c550 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 af3f69bf8e1..15c7ce4da31 100644 --- a/tests/parameter/test_snapshot.py +++ b/tests/parameter/test_snapshot.py @@ -58,13 +58,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") @@ -161,7 +161,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 @@ -242,7 +242,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( @@ -297,7 +297,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( @@ -348,15 +348,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: @@ -391,7 +391,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: @@ -403,7 +403,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: @@ -415,7 +415,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: @@ -427,7 +427,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: @@ -440,7 +440,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( @@ -473,8 +473,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] ) @@ -486,4 +486,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 910c9bec549..199f862d212 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 8db963dcb9a..5615919fb55 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 73d312b0720..b40392163fb 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 3b77fed4569..b23c6a68fde 100644 --- a/tests/test_instrument.py +++ b/tests/test_instrument.py @@ -436,7 +436,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") @@ -550,7 +550,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] def test_instrument_metadata(request: FixtureRequest) -> None: diff --git a/tests/test_logger.py b/tests/test_logger.py index cacfdc5d594..b99b5765809 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -344,10 +344,10 @@ 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: """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] diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 87da1e55699..b4b1194d733 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -19,7 +19,7 @@ def snapshot_base(self, update=False, params_to_skip_update=None): 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"} @@ -58,7 +58,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 4607199ebd1..5f1128dd985 100644 --- a/tests/test_station.py +++ b/tests/test_station.py @@ -114,13 +114,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] @@ -1140,7 +1140,7 @@ def test_load_all_instruments_only_names(example_station) -> None: def test_load_all_instruments_without_config_raises() -> None: station = Station() with pytest.raises(ValueError, match="Station has no config"): - station.load_all_instruments() # type: ignore[call-overload] + station.load_all_instruments() # type: ignore[call-overload] # ty: ignore[no-matching-overload] def test_station_config_created_with_multiple_config_files() -> None: diff --git a/tests/utils/test_class_strings.py b/tests/utils/test_class_strings.py index 4badd3ca4d2..d059d3aa5e2 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 81bc0268ab8..45da5b8957e 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 02a0549f670..607b612fe95 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 ee606a4205a..763e3d88ded 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 f41da91ee6a..3ec9739f615 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 02a75a59cb6..8a43ae2234e 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 2f0f9e45544..ff1e4995108 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 76746374ba7..e3cca0b6733 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 72628bbc043..9496f63789d 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 92450f80baa..22f2ec76dec 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 92eb8311fa3..722e237facb 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 334f2a6922c..a48a1f34673 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 8d323ea1453..b943ed72334 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 52ac0c1ced7..c21c9b2b453 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 cf3f894ca9e..6a79f5b3705 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 79a1acf1929..0aca7efd874 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 269460565dd..97d47092c56 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 f196afaf6f4..d7e0186110f 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 6c180447969a55bfd5032501db04459fce9a73b9 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:16:01 +0200 Subject: [PATCH 55/66] Suppress ty errors where pyright is already suppressed These are calls that deliberately pass invalid arguments to check that they are rejected at runtime. They are only reported by pyright and ty since the enclosing test functions are untyped and therefore skipped by mypy. --- .../measurement/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 +- tests/parameter/test_keyword_only_args.py | 4 ++-- tests/parameter/test_snapshot.py | 2 +- tests/sphinx_extension/test_parse_parameter_attr.py | 2 +- tests/test_channels.py | 2 +- tests/validators/test_enum.py | 2 +- tests/validators/test_literal.py | 4 ++-- 10 files changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/dataset/measurement/test_measurement_context_manager.py b/tests/dataset/measurement/test_measurement_context_manager.py index 5ac9da06dd2..ab87b321417 100644 --- a/tests/dataset/measurement/test_measurement_context_manager.py +++ b/tests/dataset/measurement/test_measurement_context_manager.py @@ -300,7 +300,7 @@ def test_unregister_parameter(DAC, DMM) -> None: 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 ec80c28bf1a..d747cdc28c5 100644 --- a/tests/dataset/test_dataset_export.py +++ b/tests/dataset/test_dataset_export.py @@ -1819,7 +1819,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} @@ -1843,13 +1843,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 2d88f6819b9..db5bc4cf994 100644 --- a/tests/dataset/test_measurement_extensions.py +++ b/tests/dataset/test_measurement_extensions.py @@ -333,7 +333,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, ) @@ -353,8 +353,8 @@ def test_dond_into_fails_with_groups(default_params, default_database_and_experi 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 41cc2837502..ffabb4412a3 100644 --- a/tests/dataset/test_string_data.py +++ b/tests/dataset/test_string_data.py @@ -199,7 +199,7 @@ def test_list_of_strings(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/parameter/test_keyword_only_args.py b/tests/parameter/test_keyword_only_args.py index 3927d1962b6..99c2bffc552 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 15c7ce4da31..6abcdda1e0a 100644 --- a/tests/parameter/test_snapshot.py +++ b/tests/parameter/test_snapshot.py @@ -467,7 +467,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 a19d457eb54..b424fb07caf 100644 --- a/tests/sphinx_extension/test_parse_parameter_attr.py +++ b/tests/sphinx_extension/test_parse_parameter_attr.py @@ -102,7 +102,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 199f862d212..a9151356012 100644 --- a/tests/test_channels.py +++ b/tests/test_channels.py @@ -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 9496f63789d..1c0c5220e52 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 c549e8003af..e001d461ea2 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 8286a8cb43edac7671ff2b94fca3b55b70159462 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:16:45 +0200 Subject: [PATCH 56/66] 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 00000000000..1093e7f909c --- /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 dc82f169267..223adb26162 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 8594dab20918a09fceeb8eaad06beb99079643ce Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:17:12 +0200 Subject: [PATCH 57/66] 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 00000000000..bec83574a15 --- /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 eb9caa7e883..3d8fccc0e1e 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 d854c08c821d57b00df6487fc1013946040ae580 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:18:19 +0200 Subject: [PATCH 58/66] 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 36225b7329a..06009d9aff1 100644 --- a/tests/dataset/test_dataset_basic.py +++ b/tests/dataset/test_dataset_basic.py @@ -975,7 +975,7 @@ def test_get_array_parameter_data_no_nulls(array_dataset_with_nulls) -> None: 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: @@ -1014,7 +1014,7 @@ def test_get_array_parameter_data(array_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 db5bc4cf994..8a18c922e6b 100644 --- a/tests/dataset/test_measurement_extensions.py +++ b/tests/dataset/test_measurement_extensions.py @@ -499,7 +499,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), } @@ -527,7 +527,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 = [ @@ -573,7 +573,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", @@ -611,8 +611,8 @@ def test_shapes_with_multiple_datasets(default_params, default_database_and_expe _ = 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 3883f1c75eb..5651c5906b3 100644 --- a/tests/dataset/test_sqlite_base.py +++ b/tests/dataset/test_sqlite_base.py @@ -54,7 +54,7 @@ def _make_simple_run_describer(): 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 73cfb78111263bda84b479f4dab496405d1b4618 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:18:59 +0200 Subject: [PATCH 59/66] 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 e9aca5043e0..1fa997c1bb8 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 a1d49af003e..450edcb5386 100644 --- a/tests/parameter/test_parameter_ramp.py +++ b/tests/parameter/test_parameter_ramp.py @@ -62,6 +62,7 @@ def test_ramp_scaled(scale, value) -> 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) -> 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, value) -> 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 dd4eef3eab4397bd1c6ac86f74d4cbf53f66e3d5 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:20:22 +0200 Subject: [PATCH 60/66] Suppress ty on TypeVars used as plain values The deprecation shim tests create TypeVar objects to hand to _make_deprecated_typevars_getattr and read a TypeVar that only the module level __getattr__ provides. Neither is something a type checker can follow, and mypy and pyright accept both without complaint. --- tests/utils/test_deprecate.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/utils/test_deprecate.py b/tests/utils/test_deprecate.py index 5fa3e58006e..164b1f4ecd2 100644 --- a/tests/utils/test_deprecate.py +++ b/tests/utils/test_deprecate.py @@ -17,9 +17,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 @@ -64,7 +66,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}" @@ -74,7 +76,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: @@ -92,6 +94,8 @@ def test_real_module_import_triggers_warning() -> None: """Test that importing a deprecated TypeVar from an actual module works.""" mod = importlib.import_module("qcodes.utils.deep_update_utils") with pytest.warns(QCoDeSDeprecationWarning, match="'K'"): - k = mod.K + # K is only served by the module level __getattr__ that this test + # exercises, so it is invisible to a type checker + k = mod.K # ty: ignore[unresolved-attribute] assert isinstance(k, TypeVar) From 482de300e1d70f2f0087f146f2808e685dd6e2f2 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:20:48 +0200 Subject: [PATCH 61/66] 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 a66914bf80d..5837cf2b0f0 100644 --- a/tests/parameter/test_parameter_override.py +++ b/tests/parameter/test_parameter_override.py @@ -30,7 +30,7 @@ def __init__(self, name, **kwargs): 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): return 0 From dddcfdfaad94f03536e994be1e5d369d9e38148d Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:21:37 +0200 Subject: [PATCH 62/66] 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 11d4df019b2..9c73b66b36b 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 7eb8fd26b13d516d300654ae526f7f0b36657340 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 21 Sep 2026 21:47:21 +0200 Subject: [PATCH 63/66] Fix type checking for example notebook --- ...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 03a84173933..47f12c5af6a 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 fb21938f1b7a1e11d4c4eea38b284e2faa58e4fb Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 21 Sep 2026 21:51:07 +0200 Subject: [PATCH 64/66] Add type for idn parameter --- 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 231ae56762d..9a8c68a8b79 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 48d687f47db..b6ac9a790a7 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 83b2209f996aaca989d9298080cbe48bcfc1b5c2 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 25 Sep 2026 18:45:31 +0200 Subject: [PATCH 65/66] Suppress new ty errors in negative type tests ty 0.0.84 now reports subscripts and arguments that these tests pass deliberately to check the runtime error handling, as well as mocking an attribute that is not declared on the annotated type. All of them are already suppressed for pyright. --- .../b1500_driver_tests/test_b1500.py | 14 +++++++------- .../b1500_driver_tests/test_b1517a_smu.py | 6 +++--- .../test_sampling_measurement.py | 2 +- tests/test_channels.py | 10 +++++----- 4 files changed, 16 insertions(+), 16 deletions(-) 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 887e58c9711..04d6c60ae2e 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 a3da6f5c448..73a7d8bca04 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 0e734182659..4c393c4e70a 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/test_channels.py b/tests/test_channels.py index a9151356012..ee9d9e9b254 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( From b0de75f2627bc46237e18274ebcb141ccea75310 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 25 Sep 2026 18:46:19 +0200 Subject: [PATCH 66/66] Compare the global set callback against None ty 0.0.84 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 f60aeded297..44f00b48ba2 100644 --- a/tests/parameter/test_parameter_on_set_callback.py +++ b/tests/parameter/test_parameter_on_set_callback.py @@ -294,7 +294,7 @@ def test_set_callback_for_instance( captured_instance_params = [] def callback(param: ParameterBase, val): - 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)