From c0e7e95f8a571e00b894daaef2d7dae711dac90c Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 19 Aug 2026 07:18:07 +0200 Subject: [PATCH 01/69] Narrow Arrays shape elements without callable() callable() does not narrow int | Callable[[], int], since a subclass of int could define __call__, so ty infers calling the element as returning object. Checking isinstance(s, int) first lets ty narrow the remaining branch to the callable. Elements that are not callable are still used as they are, so numpy integers in a shape keep working at runtime. --- src/qcodes/validators/validators.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/qcodes/validators/validators.py b/src/qcodes/validators/validators.py index f76331bc153..457655c4d3f 100644 --- a/src/qcodes/validators/validators.py +++ b/src/qcodes/validators/validators.py @@ -985,12 +985,12 @@ def shape_unevaluated(self) -> shape_tuple_type: def shape(self) -> tuple[int, ...] | None: if self._shape is None: return None - shape_array = [] + shape_array: list[int] = [] for s in self._shape: - if callable(s): - shape_array.append(s()) - else: + if isinstance(s, int) or not callable(s): shape_array.append(s) + else: + shape_array.append(s()) shape = tuple(shape_array) return shape From 5a120febe202f7c40142b9ee660b39bcda961a51 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 19 Aug 2026 07:53:46 +0200 Subject: [PATCH 02/69] 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 1053246c244..38265b75e81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -472,6 +472,38 @@ quote-annotations = true sdist = "versioningit.cmdclass.sdist" build_py = "versioningit.cmdclass.build_py" +[tool.ty.environment] +# a number of drivers are only usable on Windows. Checking against all +# platforms means that these are type checked no matter which platform ty +# runs on, and that the result does not depend on the platform of the developer. +python-platform = "all" + +[tool.ty.src] +# mirrors the include and ignore settings of pyright above +include = ["src", "tests"] +exclude = [ + "src/qcodes/instrument_drivers/Harvard/Decadac.py", + ] + +# these are packages that we import +# but don't have installed by default. +# Compare with ignore_missing_imports in the mypy config above +[[tool.ty.overrides]] +include = [ + "src/qcodes/instrument_drivers/Galil/dmc_41x3.py", + "src/qcodes/instrument_drivers/Minicircuits/USBHIDMixin.py", + "src/qcodes/instrument_drivers/Minicircuits/_minicircuits_usb_spdt.py", +] +[tool.ty.overrides.rules] +unresolved-import = "ignore" + +# clr is provided by pythonnet which is not installed by default +# so its members cannot be resolved either +[[tool.ty.overrides]] +include = ["src/qcodes/instrument_drivers/Minicircuits/_minicircuits_usb_spdt.py"] +[tool.ty.overrides.rules] +unresolved-attribute = "ignore" + [tool.towncrier] package = "qcodes" name = "QCoDeS" From fecd0fce61c7d231285b51c8096bf0796f667e2b Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 19 Aug 2026 20:04:20 +0200 Subject: [PATCH 03/69] 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 ee7a8a8db694489a2163c619461d2d9b6bcf5cac Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 19 Aug 2026 20:15:46 +0200 Subject: [PATCH 04/69] 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 1947e494f01..3affcc1bded 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 a3a96126714..55504814580 100644 --- a/tests/test_instrument.py +++ b/tests/test_instrument.py @@ -214,8 +214,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 33b77e8713c5f0ac6025a961b5f3f222876d8d63 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:17:24 +0200 Subject: [PATCH 05/69] 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 fe533933962..2d74bdb95c1 100644 --- a/src/qcodes/dataset/data_set.py +++ b/src/qcodes/dataset/data_set.py @@ -1562,28 +1562,21 @@ def _finalize_res_dict_standalones( for param, value in result_dict.items(): if param.type == "text": if value.shape: - new_res: list[dict[str, VALUE]] = [ - {param.name: str(val)} for val in value - ] - res_list += new_res + res_list.extend({param.name: str(val)} for val in value) else: - new_res = [{param.name: str(value)}] - res_list += new_res + res_list.append({param.name: str(value)}) elif param.type == "numeric": if value.shape: - res_list += [{param.name: number} for number in value] + res_list.extend({param.name: number} for number in value) else: - new_res = [{param.name: float(value)}] - res_list += new_res + res_list.append({param.name: float(value)}) elif param.type == "complex": if value.shape: - res_list += [{param.name: number} for number in value] + res_list.extend({param.name: number} for number in value) else: - new_res = [{param.name: complex(value)}] - res_list += new_res + res_list.append({param.name: complex(value)}) else: - new_res = [{param.name: value}] - res_list += new_res + res_list.append({param.name: value}) return res_list From bbb958dd4b48475efc3c7fa3b2333f99801d6c83 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:17:30 +0200 Subject: [PATCH 06/69] 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 631c0c7dc12..983380112e1 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 96d8b3e99d78d32f18822ec8d2dd55b4335b521b Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:21:27 +0200 Subject: [PATCH 07/69] 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 97638e7bcd6d1930e061c3355401009b9eae8d13 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:21:41 +0200 Subject: [PATCH 08/69] 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 d20a6adaabc..072820a8095 100644 --- a/src/qcodes/utils/abstractmethod.py +++ b/src/qcodes/utils/abstractmethod.py @@ -16,5 +16,5 @@ def qcodes_abstractmethod[**input, output]( instantiated and we will use this property to detect if the method is abstract and should be overwritten. """ - funcobj.__qcodes_is_abstract_method__ = True # type: ignore[attr-defined] + funcobj.__qcodes_is_abstract_method__ = True # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] return funcobj From 0148241c1c37e71975456c840043274d1f2d39eb Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:23:22 +0200 Subject: [PATCH 09/69] 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 2161c1dc94ff8bb38a6d62c4d08ed8dfec7de0fa Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 22 Oct 2026 20:51:23 +0200 Subject: [PATCH 10/69] Include numpy scalar types in the VALUE type alias add_results and the other functions that store data in the database already support numpy integer, floating point and complex scalars at runtime, since sqlite adapters are registered for them. Include them in VALUE before numpy_ints, numpy_floats and numpy_complex are annotated with the numpy scalar types, since the tests that insert those values would otherwise fail type checking. --- docs/changes/newsfragments/8441.improved.16 | 7 +++++++ src/qcodes/dataset/sqlite/query_helpers.py | 12 +++++++++++- tests/dataset/test_dataset_basic.py | 4 +--- 3 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 docs/changes/newsfragments/8441.improved.16 diff --git a/docs/changes/newsfragments/8441.improved.16 b/docs/changes/newsfragments/8441.improved.16 new file mode 100644 index 00000000000..ca74f6687b9 --- /dev/null +++ b/docs/changes/newsfragments/8441.improved.16 @@ -0,0 +1,7 @@ +The type alias ``VALUE`` in ``qcodes.dataset.sqlite.query_helpers`` now includes +the numpy integer, floating point and complex scalar types. These are already +supported at runtime by ``DataSet.add_results`` and the other functions that +store data in the database, so passing numpy scalars to them no longer produces a +type checking error. Since ``VALUE`` is also used as the return type of methods +such as ``DataSet.get_metadata``, code that annotates the returned values may +need to handle numpy scalar types too. diff --git a/src/qcodes/dataset/sqlite/query_helpers.py b/src/qcodes/dataset/sqlite/query_helpers.py index dde5b8ee331..82b07b5458a 100644 --- a/src/qcodes/dataset/sqlite/query_helpers.py +++ b/src/qcodes/dataset/sqlite/query_helpers.py @@ -25,7 +25,17 @@ import sqlite3 # represent the type of data we can/want map to sqlite column -VALUE = str | complex | list | ndarray | bool | None +VALUE = ( + str + | complex + | list + | ndarray + | bool + | np.integer + | np.floating + | np.complexfloating + | None +) VALUES = Sequence[VALUE] diff --git a/tests/dataset/test_dataset_basic.py b/tests/dataset/test_dataset_basic.py index 1af0d9af4b2..b446bc0fb1a 100644 --- a/tests/dataset/test_dataset_basic.py +++ b/tests/dataset/test_dataset_basic.py @@ -640,9 +640,7 @@ def test_numpy_complex(dataset: DataSet) -> None: dataset.mark_started() results = [{"x": tp(2.0 + 3.0j)} for tp in numpy_complex] - # TODO: VALUE in qcodes.dataset.sqlite.query_helpers does not include - # numpy scalar types although add_results supports them - dataset.add_results(results) # pyright: ignore[reportArgumentType] + dataset.add_results(results) expected_result = np.array([tp(2.0 + 3.0j) for tp in numpy_complex]) result = dataset.get_parameter_data() np.testing.assert_array_equal(result["x"]["x"], expected_result) From 7e43a752d8ea0604d66ca9fa495fa654d8a02e82 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:28:54 +0200 Subject: [PATCH 11/69] 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 cb39c378c09..32d4ffc204d 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 2a699ff33337548ed1beb3b78ff979fa6eb378a0 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:30:32 +0200 Subject: [PATCH 12/69] 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 fe60ce18dc9376d56e5333c421114dab0f60c016 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:30:55 +0200 Subject: [PATCH 13/69] 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 b7bfc347416..d45a3a3c225 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 21a9de819675ebaf87ac1fd5c605e6077c9b851f Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:31:17 +0200 Subject: [PATCH 14/69] 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 a1e64776c0b..89519af5e9a 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 623cc290b552815d5e2f26278a2436b5eb8b9fe2 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:32:25 +0200 Subject: [PATCH 15/69] 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 d916cc6b1c2f319e90627bc910ffc296b0308742 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:32:51 +0200 Subject: [PATCH 16/69] 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 0246ecfdcf3acc9da391f17f1aa393b2669c5923 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:33:14 +0200 Subject: [PATCH 17/69] 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 c95a9985eaf..1bae27c88eb 100644 --- a/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py +++ b/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py @@ -769,9 +769,9 @@ class LakeshoreBase(VisaInstrument, Generic[ChanType_co]): # Define this in the model-specific class in case you want to use a # different class for sensor channels # type error. It's not clear to me why assigning a value that matches the - # default of the TypeVar is an error but both mypy and pyright - # flags it here. - CHANNEL_CLASS: type[ChanType_co] = LakeshoreBaseSensorChannel # type: ignore[assignment] + # default of the TypeVar is an error but mypy, pyright and ty all + # flag it here. + CHANNEL_CLASS: type[ChanType_co] = LakeshoreBaseSensorChannel # type: ignore[assignment] # ty: ignore[invalid-assignment] # This dict has channel name in the driver as keys, and channel "name" that # is used in instrument commands as values. For example, if channel called From a44cec682a067586cfd63bddfc772cfe516680b7 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:33:37 +0200 Subject: [PATCH 18/69] 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 2303f323da5a94c55d42a4a0af5a15026e3f04af Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:35:03 +0200 Subject: [PATCH 19/69] 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 088bdae9ed3..d74eeb99680 100644 --- a/src/qcodes/dataset/sqlite/db_overview.py +++ b/src/qcodes/dataset/sqlite/db_overview.py @@ -286,7 +286,7 @@ def get_db_overview( # The keys of ``extra`` are only known at runtime (they are the # user-supplied ``extra_columns``), so they cannot be part of # the closed ``RunOverviewDict`` definition. - entry.update(extra) # type: ignore[typeddict-item] + entry.update(extra) # type: ignore[typeddict-item] # ty: ignore[invalid-argument-type] overview[run_id] = entry From fbedd3457a7143b1d855839d27cfa00633f75de1 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:35:55 +0200 Subject: [PATCH 20/69] 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 13e47178125..4616916576e 100644 --- a/src/qcodes/instrument/channel.py +++ b/src/qcodes/instrument/channel.py @@ -1215,7 +1215,9 @@ def __init__( chan_type: type[TAUTORELOADCHANNEL], chan_list: Sequence[TAUTORELOADCHANNEL] | None = None, snapshotable: bool = True, - multichan_paramclass: type = MultiChannelInstrumentParameter, + multichan_paramclass: type[MultiChannelInstrumentParameter] = ( + MultiChannelInstrumentParameter + ), **kwargs: Any, ) -> None: super().__init__( From e4fc8d708f8218c43d46312270e2e1159461bfe2 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:36:25 +0200 Subject: [PATCH 21/69] 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 4616916576e..7c8a2c8846b 100644 --- a/src/qcodes/instrument/channel.py +++ b/src/qcodes/instrument/channel.py @@ -741,9 +741,10 @@ def __setitem__( # asserts added to work around https://github.com/python/mypy/issues/7858 if isinstance(index, int): assert isinstance(value, InstrumentModule) - self._channels[index] = value # type: ignore[assignment] - # mypy does not know that InstrumentModuleType is a TypeVar bound to - # InstrumentModule so complains here + # neither mypy nor ty knows that InstrumentModuleType is a TypeVar + # bound to InstrumentModule, so narrowing value with the isinstance + # above does not give them the element type of the list + self._channels[index] = value # type: ignore[assignment] # ty: ignore[invalid-assignment] else: assert not isinstance(value, InstrumentModule) self._channels[index] = value From c2b44450dd0161804902f77fe6b8dd9fe1679703 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:37:12 +0200 Subject: [PATCH 22/69] 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 d98de94adf7..9f107ba06ab 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 b58a965c691c857a8378f985e71044f44e65d109 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 08:09:30 +0200 Subject: [PATCH 23/69] 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 32d4ffc204d..5ccb3e6c49b 100644 --- a/src/qcodes/dataset/sqlite/database.py +++ b/src/qcodes/dataset/sqlite/database.py @@ -138,7 +138,10 @@ def _register_numpy_sqlite_adapters_and_converters() -> None: sqlite3.register_converter("numeric", _convert_numeric) - for numpy_float in (float, *numpy_floats): + # registered separately from the numpy floats below, so that the element + # type of the loop stays a numpy float rather than widening to object + sqlite3.register_adapter(float, _adapt_float) + for numpy_float in numpy_floats: sqlite3.register_adapter(numpy_float, _adapt_float) for complex_type in complex_types: From ef0b02a0f985c1a62d4437e13f715eafad8d4bdc Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 19:35:27 +0200 Subject: [PATCH 24/69] Extend the get_ramp_values suppression to ty get_ramp_values works in numbers while the value being set has the generic parameter data type, which the comment above already records and which mypy also reports. ty reports it too, so extend the existing suppression. --- src/qcodes/parameters/parameter_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qcodes/parameters/parameter_base.py b/src/qcodes/parameters/parameter_base.py index 5d6342b04aa..6f6b348d42d 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 47d104d783b0e2791da4960caaa9a35023aa6f2a Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 21:08:32 +0200 Subject: [PATCH 25/69] 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 6cea4447594d7d753cb67c1087f55d8be1d94de0 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 21:09:48 +0200 Subject: [PATCH 26/69] 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 2d74bdb95c1..ccb2e32ce2a 100644 --- a/src/qcodes/dataset/data_set.py +++ b/src/qcodes/dataset/data_set.py @@ -1234,7 +1234,11 @@ def write_data_to_text_file( def subscribe( self, - callback: Callable[[Any, int, Any | None], None], + # ``Callable[..., None]`` rather than a three argument callable because + # ``callback_kwargs`` below is bound onto the callback with + # ``functools.partial``, so it may take further keyword arguments. This + # matches how ``_Subscriber`` types the same callback. + callback: Callable[..., None], min_wait: int = 0, min_count: int = 1, state: Any | None = None, From 8c6af2663a772e92359277b636498664ecaeb2a1 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 21:13:04 +0200 Subject: [PATCH 27/69] 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 f7389e675cd..58cd9048c7c 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 16dd47686881fb9fed805b813e4aedac9720d3e1 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 21:18:50 +0200 Subject: [PATCH 28/69] 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 1dd15a73f4340f7e4b87e04305f215cec4bdf681 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 21:20:13 +0200 Subject: [PATCH 29/69] 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 073723f1d63add3b0ef15577b34617cdbaa4f28d Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 07:45:00 +0200 Subject: [PATCH 30/69] 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 38265b75e81..16d72559286 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ Changelog = "https://microsoft.github.io/Qcodes/changes/index.html" zurichinstruments = ["zhinst-qcodes>=0.3"] minicircuits_usb_spdt = ["pythonnet>3.0.4"] minicircuits_rudat = ["pywinusb>=0.4.2"] -loop = ["qcodes_loop>=0.1.2"] +loop = ["qcodes_loop>=0.2.3"] test = [ "coverage[toml]>=7.10.5", "hypothesis>=6.130.0", @@ -86,7 +86,7 @@ test = [ "types-tabulate>=0.1.0", "types-tqdm>=4.64.6", "types_pywin32>=305.0.0.7", - "qcodes_loop>=0.1.1", + "qcodes_loop>=0.2.3", "zhinst.qcodes>=0.5; python_version < '3.15'", # typecheck zhinst driver alias. zhinst-core has no Python 3.15 wheels yet "libcst>=1.2.0", # refactor tests ] From 91af2f413da033973a0cce2871c3c5f6b40cdd9e Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:00:21 +0200 Subject: [PATCH 31/69] 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 16d72559286..6b8536519c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,6 +89,7 @@ test = [ "qcodes_loop>=0.2.3", "zhinst.qcodes>=0.5; python_version < '3.15'", # typecheck zhinst driver alias. zhinst-core has no Python 3.15 wheels yet "libcst>=1.2.0", # refactor tests + "intersphinx_registry>=0.2603.16" # typecheck docs/conf.py ] docs = [ "autodocsumm>=0.2.9", @@ -479,8 +480,9 @@ build_py = "versioningit.cmdclass.build_py" python-platform = "all" [tool.ty.src] -# mirrors the include and ignore settings of pyright above -include = ["src", "tests"] +# unlike pyright above, ty also understands Jupyter notebooks, so the example +# notebooks in docs are checked too. That is coverage we get from ty alone. +include = ["src", "tests", "docs"] exclude = [ "src/qcodes/instrument_drivers/Harvard/Decadac.py", ] @@ -504,6 +506,15 @@ include = ["src/qcodes/instrument_drivers/Minicircuits/_minicircuits_usb_spdt.py [tool.ty.overrides.rules] unresolved-attribute = "ignore" +# plottr is a separate package that this notebook demonstrates integrating with, +# it is not a dependency of qcodes +[[tool.ty.overrides]] +include = [ + "docs/examples/plotting/How-to-use-Plottr-with-QCoDeS-for-live-plotting.ipynb", +] +[tool.ty.overrides.rules] +unresolved-import = "ignore" + [tool.towncrier] package = "qcodes" name = "QCoDeS" From b6a62bb037f9ed368e184b983024ffe8fca84521 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:21:09 +0200 Subject: [PATCH 32/69] 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 15dd0e3aefa358ba24a2ae289400f8dd3f4df397 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:21:38 +0200 Subject: [PATCH 33/69] 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 ee6a769da61449e9ceb9f24bc0fc92634ccf2cc3 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:25:30 +0200 Subject: [PATCH 34/69] 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 d3ec82bce828322d89366d9505e041896c2d9ee0 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:33:51 +0200 Subject: [PATCH 35/69] 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 bef6698ce8415d863542ca79f2e4681e4aed6219 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:38:11 +0200 Subject: [PATCH 36/69] 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 6a261b7ff89..f2051e3441a 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 fb73be166a7e04f3e05ce912a825a104ee24aa34 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:40:55 +0200 Subject: [PATCH 37/69] 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 f1fcfe65f3021fd41b41e59da6ed7756ab1b4aae Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:57:53 +0200 Subject: [PATCH 38/69] 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 b098f53829985b409b436f79e9789b3d428bceb8 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 08:02:26 +0200 Subject: [PATCH 39/69] 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 618a954d286b5b10ce688de3a5e5768ca5360ac3 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 08:07:51 +0200 Subject: [PATCH 40/69] 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 13d1ced21f9..5628aab7c72 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 027be867622..aad6be991ab 100644 --- a/tests/dataset/test_dataset_export.py +++ b/tests/dataset/test_dataset_export.py @@ -1506,11 +1506,8 @@ def _assert_xarray_metadata_is_as_expected( assert xarray_ds.ds_name == qc_dataset.name assert xarray_ds.sample_name == qc_dataset.sample_name assert xarray_ds.exp_name == qc_dataset.exp_name - assert ( - xarray_ds.snapshot == qc_dataset.snapshot_raw - if qc_dataset.snapshot_raw is not None - else "null" - ) + if qc_dataset.snapshot_raw is not None: + assert xarray_ds.snapshot == qc_dataset.snapshot_raw assert xarray_ds.guid == qc_dataset.guid assert xarray_ds.run_timestamp == qc_dataset.run_timestamp() assert xarray_ds.completed_timestamp == qc_dataset.completed_timestamp() diff --git a/tests/dataset/test_snapshot.py b/tests/dataset/test_snapshot.py index d58a4077bb8..57eb0c13533 100644 --- a/tests/dataset/test_snapshot.py +++ b/tests/dataset/test_snapshot.py @@ -77,9 +77,7 @@ def test_station_snapshot_during_measurement( assert expected_snapshot == snapshot_from_dataset # 2. Test `snapshot_raw` property - # this is not part of the DatasetProtocol interface - # but we test it anyway - assert json_snapshot_from_dataset == data_saver.dataset.snapshot_raw # type: ignore[attr-defined] + assert json_snapshot_from_dataset == data_saver.dataset.snapshot_raw # 3. Test `snapshot` property From f81d884d92238689ea11c33090b98654379864e8 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 08:08:26 +0200 Subject: [PATCH 41/69] 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 ca7c9df0733786dcadcd8a67a3c5062a9bc6cc21 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 27 Aug 2026 16:09:29 +0200 Subject: [PATCH 42/69] Unpack dond results safely in the AMI430 example dond returns a single dataset rather than a tuple of them unless squeeze=False is passed, so unpacking the result in the AMI430 example does not type check. Pass squeeze=False and take the first dataset. --- .../driver_examples/Qcodes example with AMI430.ipynb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/examples/driver_examples/Qcodes example with AMI430.ipynb b/docs/examples/driver_examples/Qcodes example with AMI430.ipynb index 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 c9afaafa364bc8aef0e7624aa9c791a000dfb537 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 27 Aug 2026 16:25:05 +0200 Subject: [PATCH 43/69] 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 618a22d24e7bfd5986c42f6bf6017032ef1fa1ac Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 27 Aug 2026 16:32:20 +0200 Subject: [PATCH 44/69] Allow ParamSpecBase in get_parameter_data get_parameter_data only uses the name of each parameter, so it already accepts any ParamSpecBase at runtime, but it was annotated as taking a ParamSpec. Widen the annotation on the protocol and both implementations, so that the example notebook can pass ParamSpecBase objects. Also drop the cell in that notebook that displays dataset.parameters. The dataset there is a DataSetProtocol, which has no parameters attribute, and the cell before it already shows dataset.get_parameters(). --- .../DataSet/Accessing-data-in-DataSet.ipynb | 24 ++----------------- src/qcodes/dataset/data_set.py | 2 +- src/qcodes/dataset/data_set_in_memory.py | 4 ++-- src/qcodes/dataset/data_set_protocol.py | 6 ++--- 4 files changed, 8 insertions(+), 28 deletions(-) diff --git a/docs/examples/DataSet/Accessing-data-in-DataSet.ipynb b/docs/examples/DataSet/Accessing-data-in-DataSet.ipynb index 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 ccb2e32ce2a..4dbc176b977 100644 --- a/src/qcodes/dataset/data_set.py +++ b/src/qcodes/dataset/data_set.py @@ -899,7 +899,7 @@ def _ensure_dataset_written(self) -> None: def get_parameter_data( self, - *params: str | ParamSpec | ParameterBase, + *params: str | ParamSpecBase | ParameterBase, start: int | None = None, end: int | None = None, callback: Callable[[float], None] | None = None, diff --git a/src/qcodes/dataset/data_set_in_memory.py b/src/qcodes/dataset/data_set_in_memory.py index 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 5628aab7c72..222a9af47d7 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 a1c3fd88c0c07f304ab839b6e97d3ce096ee7547 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 27 Aug 2026 16:34:48 +0200 Subject: [PATCH 45/69] 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 db5c32c6457c4907a52b2b94b77efdeb0df3c905 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 27 Aug 2026 16:37:12 +0200 Subject: [PATCH 46/69] 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 aa5b9460ef1b89c6893aada70792685bde17652b Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 27 Aug 2026 16:48:42 +0200 Subject: [PATCH 47/69] 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 094423db830974bf96cfdb35a8248e7833dc075f Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 28 Aug 2026 21:51:03 +0200 Subject: [PATCH 48/69] Suppress ty on the InstrumentMeta patch in docs docs/conf.py replaces InstrumentMeta with ABCMeta, so that sphinx documents the signature of __init__ rather than __call__. ty rejects assigning a different class to the module attribute, which is the whole point of the patch. --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 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 20c6d0822c841a769e0d247375ad4e83e67ee4e0 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 28 Aug 2026 21:52:26 +0200 Subject: [PATCH 49/69] 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 b36024a07e745a67a0bfe65c980af379b13e3e96 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 28 Aug 2026 22:06:27 +0200 Subject: [PATCH 50/69] 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 222a9af47d7..ed96f751dac 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 d04577f66564fc57edaacbcedfd34cd4d6eb38c8 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:10:44 +0200 Subject: [PATCH 51/69] Suppress ty errors where mypy is already suppressed ty does not understand the error codes in a mypy type: ignore comment, so every deliberately wrong call in the test suite is reported twice. Add a matching ty: ignore comment on those lines. This is the mechanical part of getting the tests to type check with ty and leaves only the diagnostics that are not already suppressed for mypy. --- .../test_measurement_context_manager.py | 8 ++--- tests/dataset/test_converters.py | 4 +-- tests/dataset/test_datasaver.py | 4 +-- tests/dataset/test_dataset_basic.py | 4 +-- tests/dataset/test_db_overview.py | 2 +- tests/dataset/test_dependencies.py | 12 +++---- tests/dataset/test_descriptions.py | 2 +- ...st_parameter_with_setpoints_has_control.py | 2 +- tests/dataset/test_paramspec.py | 2 +- tests/dataset/test_snapshot.py | 2 +- tests/dataset/test_sqlite_base.py | 2 +- tests/dataset/test_sqlite_connection.py | 4 +-- tests/dataset/test_string_data.py | 4 +-- tests/delegate/test_delegate_instrument.py | 2 +- .../b1500_driver_tests/test_b1517a_smu.py | 4 +-- tests/drivers/test_ami430_visa.py | 4 +-- tests/drivers/test_lakeshore_372.py | 10 +++--- tests/drivers/test_mercuryips.py | 2 +- tests/drivers/test_signal_hound_usb_sa124b.py | 2 +- tests/drivers/test_sr830.py | 4 +-- tests/drivers/test_tektronix_awg70000a.py | 2 +- tests/helpers/test_delegate_attribues.py | 10 +++--- tests/helpers/test_strip_attrs.py | 6 ++-- tests/parameter/test_array_parameter.py | 6 ++-- tests/parameter/test_delegate_parameter.py | 6 ++-- .../parameter/test_elapsed_time_parameter.py | 2 +- tests/parameter/test_get_latest.py | 6 ++-- tests/parameter/test_get_set_wrapping.py | 2 +- tests/parameter/test_group_parameter.py | 2 +- tests/parameter/test_issequenceof.py | 4 +-- tests/parameter/test_keyword_only_args.py | 28 ++++++++-------- tests/parameter/test_manual_parameter.py | 2 +- tests/parameter/test_multi_parameter.py | 4 +-- tests/parameter/test_on_off_mapping.py | 4 +-- tests/parameter/test_parameter_basics.py | 14 ++++---- tests/parameter/test_parameter_cache.py | 14 ++++---- .../test_parameter_context_manager.py | 2 +- tests/parameter/test_parameter_validation.py | 2 +- .../test_parameter_with_setpoints.py | 8 ++--- tests/parameter/test_scaled_parameter.py | 4 +-- tests/parameter/test_snapshot.py | 32 +++++++++---------- tests/test_channels.py | 8 ++--- tests/test_command.py | 10 +++--- tests/test_import.py | 18 +++++------ tests/test_instrument.py | 4 +-- tests/test_logger.py | 4 +-- tests/test_metadata.py | 4 +-- tests/test_station.py | 4 +-- tests/utils/test_class_strings.py | 2 +- tests/utils/test_isfunction.py | 2 +- tests/validators/test_arrays.py | 10 +++--- tests/validators/test_basic.py | 4 +-- tests/validators/test_bool.py | 2 +- tests/validators/test_callable.py | 2 +- tests/validators/test_complex.py | 2 +- tests/validators/test_dict.py | 2 +- tests/validators/test_enum.py | 2 +- tests/validators/test_ints.py | 14 ++++---- tests/validators/test_lists.py | 4 +-- tests/validators/test_multi_type.py | 6 ++-- tests/validators/test_multi_type_and.py | 2 +- tests/validators/test_multi_type_or.py | 2 +- tests/validators/test_multiples.py | 4 +-- tests/validators/test_numbers.py | 2 +- tests/validators/test_sequence.py | 2 +- tests/validators/test_string.py | 16 +++++----- 66 files changed, 186 insertions(+), 186 deletions(-) diff --git a/tests/dataset/measurement/test_measurement_context_manager.py b/tests/dataset/measurement/test_measurement_context_manager.py index d42fdaaa2cb..92481999e18 100644 --- a/tests/dataset/measurement/test_measurement_context_manager.py +++ b/tests/dataset/measurement/test_measurement_context_manager.py @@ -95,10 +95,10 @@ def test_register_parameter_arg_types( meas.register_parameter(DMM.v1, setpoints="foo") with pytest.raises(TypeError): - meas.register_parameter(DMM.v1, basis=(DAC.ch1, 3)) # type: ignore[arg-type] + meas.register_parameter(DMM.v1, basis=(DAC.ch1, 3)) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises(TypeError): - meas.register_parameter(DMM.v1, setpoints=(DAC.ch1, 3)) # type: ignore[arg-type] + meas.register_parameter(DMM.v1, setpoints=(DAC.ch1, 3)) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_register_parameter_numbers(DAC: DummyInstrument, DMM: DummyInstrument) -> None: @@ -112,7 +112,7 @@ def test_register_parameter_numbers(DAC: DummyInstrument, DMM: DummyInstrument) for not_a_parameter in not_parameters: with pytest.raises(ValueError): - meas.register_parameter(not_a_parameter) # type: ignore[arg-type] + meas.register_parameter(not_a_parameter) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] my_param = DAC.ch1 meas.register_parameter(my_param) @@ -1015,7 +1015,7 @@ def test_datasaver_foul_input(bg_writing: bool) -> None: with meas.run(bg_writing) as datasaver: for ft in foul_stuff: with pytest.raises(ValueError): - datasaver.add_result(("foul", ft)) # type: ignore[arg-type] + datasaver.add_result(("foul", ft)) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] @settings(max_examples=10, deadline=None) diff --git a/tests/dataset/test_converters.py b/tests/dataset/test_converters.py index e970618d482..eb3d2d524c9 100644 --- a/tests/dataset/test_converters.py +++ b/tests/dataset/test_converters.py @@ -198,11 +198,11 @@ def test_construct_current_rundescriber_from_fake_v4( version=4, shapes=None, ) - v4["foobar"] = {"foo": ["bar"]} # type: ignore[typeddict-unknown-key] + v4["foobar"] = {"foo": ["bar"]} # type: ignore[typeddict-unknown-key] # ty: ignore[invalid-key] rds1 = RunDescriber._from_dict(v4) rds_upgraded = from_dict_to_current(v4) v3 = v4.copy() - v3.pop("foobar") # type: ignore[typeddict-item] + v3.pop("foobar") # type: ignore[typeddict-item] # ty: ignore[invalid-key] v3["version"] = 3 assert rds1._to_dict() == v3 assert rds_upgraded._to_dict() == v3 diff --git a/tests/dataset/test_datasaver.py b/tests/dataset/test_datasaver.py index a552b1b7c23..695dde6f17c 100644 --- a/tests/dataset/test_datasaver.py +++ b/tests/dataset/test_datasaver.py @@ -177,7 +177,7 @@ def test_saving_numeric_values_as_text( data_saver.add_result((p.name, value)) finally: data_saver.dataset.mark_completed() - data_saver.dataset.conn.close() # type: ignore[attr-defined] + data_saver.dataset.conn.close() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] @pytest.mark.usefixtures("experiment") @@ -209,4 +209,4 @@ def test_duplicated_parameter_raises() -> None: data_saver.add_result((p.name, 1), (p.name, 1)) finally: data_saver.dataset.mark_completed() - data_saver.dataset.conn.close() # type: ignore[attr-defined] + data_saver.dataset.conn.close() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] diff --git a/tests/dataset/test_dataset_basic.py b/tests/dataset/test_dataset_basic.py index b446bc0fb1a..350decb674e 100644 --- a/tests/dataset/test_dataset_basic.py +++ b/tests/dataset/test_dataset_basic.py @@ -321,7 +321,7 @@ def test_load_by_id_for_none() -> None: with pytest.raises( ValueError, match=re.escape("run_id has to be a positive integer, not None.") ): - _ = load_by_id(None) # type: ignore[arg-type] + _ = load_by_id(None) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] @settings(deadline=None, max_examples=6) @@ -845,7 +845,7 @@ def test_parent_dataset_links_invalid_input() -> None: match = re.escape("Invalid input. Did not receive a list of Links") with pytest.raises(ValueError, match=match): - ds.parent_dataset_links = [ds.guid] # type: ignore[list-item] + ds.parent_dataset_links = [ds.guid] # type: ignore[list-item] # ty: ignore[invalid-assignment] match = re.escape( "Invalid input. All links must point to this dataset. " diff --git a/tests/dataset/test_db_overview.py b/tests/dataset/test_db_overview.py index 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 8aee0c94575..22a6ee3c1c3 100644 --- a/tests/dataset/test_dependencies.py +++ b/tests/dataset/test_dependencies.py @@ -24,7 +24,7 @@ def test_wrong_input_raises() -> None: ["p1", ParamSpec("p2", paramtype="text")], ): with pytest.raises(ValueError): - InterDependencies(pspecs) # type: ignore[arg-type] + InterDependencies(pspecs) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_init( @@ -82,18 +82,18 @@ def test_init_validation_raises( for tree, cause in zip(invalid_trees, causes): with pytest.raises(ValueError, match="Invalid dependencies") as ei: - InterDependencies_(dependencies=tree, inferences={}) # type: ignore[arg-type] + InterDependencies_(dependencies=tree, inferences={}) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert error_caused_by(ei, cause=cause) for tree, cause in zip(invalid_trees, causes): with pytest.raises(ValueError, match="Invalid inferences") as ei: - InterDependencies_(dependencies={}, inferences=tree) # type: ignore[arg-type] + InterDependencies_(dependencies={}, inferences=tree) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert error_caused_by(ei, cause=cause) with pytest.raises(ValueError, match="Invalid standalones") as ei: - InterDependencies_(standalones=("ps1", "ps2")) # type: ignore[arg-type] + InterDependencies_(standalones=("ps1", "ps2")) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert error_caused_by(ei, cause="Standalones must be a sequence of ParamSpecs") @@ -105,8 +105,8 @@ def test_init_validation_raises( for inv in invalid_trees_2: with pytest.raises(ValueError, match="already exists"): InterDependencies_( - dependencies=inv["deps"], # type: ignore[arg-type] - inferences=inv["inffs"], # type: ignore[arg-type] + dependencies=inv["deps"], # type: ignore[arg-type] # ty: ignore[invalid-argument-type] + inferences=inv["inffs"], # type: ignore[arg-type] # ty: ignore[invalid-argument-type] ) diff --git a/tests/dataset/test_descriptions.py b/tests/dataset/test_descriptions.py index 44747961758..2ebf8424e5b 100644 --- a/tests/dataset/test_descriptions.py +++ b/tests/dataset/test_descriptions.py @@ -16,7 +16,7 @@ def test_wrong_input_type_raises() -> None: for interdeps in ("interdeps", ["p1", "p2"], 0): with pytest.raises(ValueError): - RunDescriber(interdeps=interdeps) # type: ignore[arg-type] + RunDescriber(interdeps=interdeps) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_equality( diff --git a/tests/dataset/test_parameter_with_setpoints_has_control.py b/tests/dataset/test_parameter_with_setpoints_has_control.py index 430a0b68fa9..6bb284085e9 100644 --- a/tests/dataset/test_parameter_with_setpoints_has_control.py +++ b/tests/dataset/test_parameter_with_setpoints_has_control.py @@ -33,7 +33,7 @@ def unpack_self( res.append((controlled, controlled())) return res - p = _ControlledSetpoints(name, **kwargs) # type: ignore[arg-type] + p = _ControlledSetpoints(name, **kwargs) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] p.has_control_of.add(controlled) return p diff --git a/tests/dataset/test_paramspec.py b/tests/dataset/test_paramspec.py index 7ad5d1a286e..700e2718ff2 100644 --- a/tests/dataset/test_paramspec.py +++ b/tests/dataset/test_paramspec.py @@ -129,7 +129,7 @@ def test_creation( invalid_types = ("np.array", "ndarray", "lala", "", Number, ndarray, 0, None) for inv_type in invalid_types: with pytest.raises(ValueError): - ParamSpec(name, inv_type) # type: ignore[arg-type] + ParamSpec(name, inv_type) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] if not inff1.isidentifier(): inff1 = "inff1" diff --git a/tests/dataset/test_snapshot.py b/tests/dataset/test_snapshot.py index 57eb0c13533..6d05d2ba54e 100644 --- a/tests/dataset/test_snapshot.py +++ b/tests/dataset/test_snapshot.py @@ -67,7 +67,7 @@ def test_station_snapshot_during_measurement( # 1. Test `get_metadata('snapshot')` method # this is not part of the DatasetProtocol interface # but we test it anyway - json_snapshot_from_dataset = data_saver.dataset.get_metadata("snapshot") # type: ignore[attr-defined] + json_snapshot_from_dataset = data_saver.dataset.get_metadata("snapshot") # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] snapshot_from_dataset = json.loads(json_snapshot_from_dataset) expected_snapshot = { diff --git a/tests/dataset/test_sqlite_base.py b/tests/dataset/test_sqlite_base.py index 19af1c240bd..3cfea0c9bd6 100644 --- a/tests/dataset/test_sqlite_base.py +++ b/tests/dataset/test_sqlite_base.py @@ -236,7 +236,7 @@ def test_get_layout_id_with_invalid_parameter_type(dataset: "DataSet") -> None: with pytest.raises(ValueError, match="Wrong parameter type, must be ParamSpec"): mut_queries._get_layout_id( dataset.conn, - 42, # type: ignore[arg-type] + 42, # type: ignore[arg-type] # ty: ignore[invalid-argument-type] dataset.run_id, ) diff --git a/tests/dataset/test_sqlite_connection.py b/tests/dataset/test_sqlite_connection.py index e55d29f645d..f808aab827b 100644 --- a/tests/dataset/test_sqlite_connection.py +++ b/tests/dataset/test_sqlite_connection.py @@ -78,7 +78,7 @@ def test_atomic_raises_for_non_atomic_conn() -> None: with ( closing(sqlite3.connect(":memory:")) as sqlite_conn, pytest.raises(ValueError, match=match_str), - atomic(sqlite_conn), # type: ignore[arg-type] + atomic(sqlite_conn), # type: ignore[arg-type] # ty: ignore[invalid-argument-type] ): pass @@ -362,7 +362,7 @@ def test_atomic_transaction_on_sqlite3_connection_raises(tmp_path: "Path") -> No closing(sqlite3.connect(dbfile)) as conn, pytest.raises(ValueError, match=match_str), ): - atomic_transaction(conn, "whatever sql query") # type: ignore[arg-type] + atomic_transaction(conn, "whatever sql query") # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_connect() -> None: diff --git a/tests/dataset/test_string_data.py b/tests/dataset/test_string_data.py index 3dba7e800d2..8484dd02d03 100644 --- a/tests/dataset/test_string_data.py +++ b/tests/dataset/test_string_data.py @@ -144,7 +144,7 @@ def test_string_with_wrong_paramtype_via_datasaver() -> None: with pytest.raises(ValueError, match=msg): data_saver.add_result(("p", "some text")) finally: - data_saver.dataset.conn.close() # type: ignore[attr-defined] + data_saver.dataset.conn.close() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] @pytest.mark.usefixtures("experiment") @@ -200,7 +200,7 @@ def test_list_of_strings(experiment: "Experiment") -> None: try: np.testing.assert_array_equal(actual_data, expec_data) finally: - test_set.conn.close() # type: ignore[attr-defined] + test_set.conn.close() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] @settings(suppress_health_check=(HealthCheck.function_scoped_fixture,), deadline=None) diff --git a/tests/delegate/test_delegate_instrument.py b/tests/delegate/test_delegate_instrument.py index b221757afa1..3d856abb853 100644 --- a/tests/delegate/test_delegate_instrument.py +++ b/tests/delegate/test_delegate_instrument.py @@ -83,5 +83,5 @@ def close_partially_constructed_instrument() -> None: DelegateInstrument( name=name, station=station, - parameters={"X": 42}, # type: ignore[dict-item] + parameters={"X": 42}, # type: ignore[dict-item] # ty: ignore[invalid-argument-type] ) diff --git a/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1517a_smu.py b/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1517a_smu.py index 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 dca80adbec8..dfdf009588a 100644 --- a/tests/drivers/test_lakeshore_372.py +++ b/tests/drivers/test_lakeshore_372.py @@ -32,7 +32,7 @@ class MockVisaInstrument: def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.visa_log = get_instrument_logger(self, VISA_LOGGER) # type: ignore[arg-type] + self.visa_log = get_instrument_logger(self, VISA_LOGGER) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] # This base class mixin holds two dictionaries associated with the # pyvisa_instrument.write() @@ -70,7 +70,7 @@ def write_raw(self, cmd: str) -> None: self.visa_log.debug(f"Query: {cmd} for command {cmd_str} with args {args}") self.cmds[cmd_str](args) else: - super().write_raw(cmd) # type: ignore[misc] + super().write_raw(cmd) # type: ignore[misc] # ty: ignore[unresolved-attribute] def ask_raw(self, cmd: str) -> Any: query_parts = cmd.split(" ") @@ -84,12 +84,12 @@ def ask_raw(self, cmd: str) -> Any: self.visa_log.debug(f"Response: {response}") return response else: - return super().ask_raw(cmd) # type: ignore[misc] + return super().ask_raw(cmd) # type: ignore[misc] # ty: ignore[unresolved-attribute] def query[**P, T](name: str) -> Callable[[Callable[P, T]], Callable[P, T]]: def wrapper(func: Callable[P, T]) -> Callable[P, T]: - func.query_name = name.upper() # type: ignore[attr-defined] + func.query_name = name.upper() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] return func return wrapper @@ -97,7 +97,7 @@ def wrapper(func: Callable[P, T]) -> Callable[P, T]: def command[**P, T](name: str) -> Callable[[Callable[P, T]], Callable[P, T]]: def wrapper(func: Callable[P, T]) -> Callable[P, T]: - func.command_name = name.upper() # type: ignore[attr-defined] + func.command_name = name.upper() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] return func return wrapper diff --git a/tests/drivers/test_mercuryips.py b/tests/drivers/test_mercuryips.py index e63ecec31c3..45303998be4 100644 --- a/tests/drivers/test_mercuryips.py +++ b/tests/drivers/test_mercuryips.py @@ -96,7 +96,7 @@ def test_wrong_field_limit_raises() -> None: "mips", address="GPIB::1::INSTR", pyvisa_sim_file="MercuryiPS.yaml", - field_limits=0, # type: ignore[arg-type] + field_limits=0, # type: ignore[arg-type] # ty: ignore[invalid-argument-type] ) diff --git a/tests/drivers/test_signal_hound_usb_sa124b.py b/tests/drivers/test_signal_hound_usb_sa124b.py index 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 6248fa50eba..5464b7ddb9c 100644 --- a/tests/drivers/test_tektronix_awg70000a.py +++ b/tests/drivers/test_tektronix_awg70000a.py @@ -181,7 +181,7 @@ def test_seqxfilefromfs_failing(forged_sequence: dict[int, dict[str, Any]]) -> N forged_sequence, [1, 1, 1], seqname="dummyname", - channel_mapping={1: None, 3: None}, # type: ignore[dict-item] + channel_mapping={1: None, 3: None}, # type: ignore[dict-item] # ty: ignore[invalid-argument-type] ) # wrong channel mapping values diff --git a/tests/helpers/test_delegate_attribues.py b/tests/helpers/test_delegate_attribues.py index 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 e032ed292f1..f8bf9fcff52 100644 --- a/tests/helpers/test_strip_attrs.py +++ b/tests/helpers/test_strip_attrs.py @@ -23,7 +23,7 @@ def __delitem__(self, item: Any) -> NoReturn: def test_normal() -> None: a = A() a.x = 15 - a.z = 25 # type: ignore[attr-defined] + a.z = 25 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] strip_attrs(a) @@ -38,13 +38,13 @@ def test_pathological() -> None: a = A() a.__dict__ = BadKeysDict() - a.fruit = "mango" # type: ignore[attr-defined] + a.fruit = "mango" # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] with pytest.raises(RuntimeError): a.__dict__.keys() strip_attrs(a) # no error, but the attribute is still there - assert a.fruit == "mango" # type: ignore[attr-defined] + assert a.fruit == "mango" # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] a = A() a.__dict__ = NoDelDict() diff --git a/tests/parameter/test_array_parameter.py b/tests/parameter/test_array_parameter.py index 710e94344fc..096e2e5cd1c 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 0fe8245d87a..42385039348 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 5d9fb36c89f..ad5c655254f 100644 --- a/tests/parameter/test_get_latest.py +++ b/tests/parameter/test_get_latest.py @@ -64,8 +64,8 @@ def test_get_latest_unknown() -> None: local_parameter = BetterGettableParam("test_param", set_cmd=None, get_cmd=None) # fake a parameter that has a value but never been get/set to mock # an instrument. - local_parameter.cache._value = value # type: ignore[attr-defined] - local_parameter.cache._raw_value = value # type: ignore[attr-defined] + local_parameter.cache._value = value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] + local_parameter.cache._raw_value = value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert local_parameter.get_latest.get_timestamp() is None before_get = datetime.now(UTC) assert local_parameter._get_count == 0 @@ -169,7 +169,7 @@ def test_no_get_max_val_age() -> None: class LocalParameter(ParameterBase): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.set_raw = lambda x: x # type: ignore[method-assign] + self.set_raw = lambda x: x # type: ignore[method-assign] # ty: ignore[invalid-assignment] self.set = self._wrap_set(self.set_raw) localparameter = LocalParameter("test_param", instrument=None, max_val_age=1) diff --git a/tests/parameter/test_get_set_wrapping.py b/tests/parameter/test_get_set_wrapping.py index 83220d3a7d6..def375cb45f 100644 --- a/tests/parameter/test_get_set_wrapping.py +++ b/tests/parameter/test_get_set_wrapping.py @@ -69,7 +69,7 @@ def test_gettable_settable_attributes_with_get_set_raw( """Test that parameters that have get_raw,set_raw are listed as gettable/settable and reverse.""" - class GetSetParam(baseclass): # type: ignore[valid-type,misc] + class GetSetParam(baseclass): # type: ignore[valid-type,misc] # ty: ignore[unsupported-base] def __init__( self, *args: Any, initial_value: Any = None, **kwargs: Any ) -> None: diff --git a/tests/parameter/test_group_parameter.py b/tests/parameter/test_group_parameter.py index d6152dbc7fb..b3a1f9c9541 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 2a16ac36e45..03bfa95c67e 100644 --- a/tests/parameter/test_issequenceof.py +++ b/tests/parameter/test_issequenceof.py @@ -39,9 +39,9 @@ def test_simple_bad(args: tuple[Any, ...]) -> None: def test_examples_raises() -> None: with pytest.raises(TypeError): - is_sequence_of([1], 1) # type: ignore[arg-type] + is_sequence_of([1], 1) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises(TypeError): - is_sequence_of([1], (1, 2)) # type: ignore[arg-type] + is_sequence_of([1], (1, 2)) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] good_depth = [ diff --git a/tests/parameter/test_keyword_only_args.py b/tests/parameter/test_keyword_only_args.py index 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 453e319872f..502623e136c 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 68d5954be05..8628481e6ed 100644 --- a/tests/parameter/test_parameter_cache.py +++ b/tests/parameter/test_parameter_cache.py @@ -100,8 +100,8 @@ def test_get_cache_unknown() -> None: local_parameter = BetterGettableParam("test_param", set_cmd=None, get_cmd=None) # fake a parameter that has a value but never been get/set to mock # an instrument. - local_parameter.cache._value = value # type: ignore[attr-defined] - local_parameter.cache._raw_value = value # type: ignore[attr-defined] + local_parameter.cache._value = value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] + local_parameter.cache._raw_value = value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert local_parameter.cache.timestamp is None before_get = datetime.now(UTC) assert local_parameter._get_count == 0 @@ -258,7 +258,7 @@ def test_no_get_max_val_age_runtime_error( class LocalParameter(ParameterBase): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.set_raw = lambda x: x # type: ignore[method-assign] + self.set_raw = lambda x: x # type: ignore[method-assign] # ty: ignore[invalid-assignment] self.set = self._wrap_set(self.set_raw) local_parameter = LocalParameter("test_param", instrument=None, max_val_age=1) @@ -298,13 +298,13 @@ def test_no_get_timestamp_none_runtime_error( def test_latest_dictionary_gets_updated_upon_set_of_memory_parameter() -> None: p = Parameter("p", set_cmd=None, get_cmd=None) - assert p.cache._value is None # type: ignore[attr-defined] + assert p.cache._value is None # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert p.cache.raw_value is None assert p.cache.timestamp is None p(42) - assert p.cache._value == 42 # type: ignore[attr-defined] + assert p.cache._value == 42 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert p.cache.raw_value == 42 assert p.cache.timestamp is not None @@ -350,7 +350,7 @@ def test_set_latest_works_for_plain_memory_parameter( assert p.raw_value == raw_value # Assert latest value and raw_value via private attributes for strictness - assert p.cache._value == value # type: ignore[attr-defined] + assert p.cache._value == value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert p.cache.raw_value == raw_value # Now let's get the value of the parameter to ensure that the value that @@ -371,7 +371,7 @@ def test_set_latest_works_for_plain_memory_parameter( assert p.raw_value == raw_value # Assert latest value and raw_value via private attributes for strictness - assert p.cache._value == value # type: ignore[attr-defined] + assert p.cache._value == value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert p.cache.raw_value == raw_value diff --git a/tests/parameter/test_parameter_context_manager.py b/tests/parameter/test_parameter_context_manager.py index f4cc7fa6f1b..38b0c12230c 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 c16fe018d43..1166dc833e0 100644 --- a/tests/parameter/test_parameter_with_setpoints.py +++ b/tests/parameter/test_parameter_with_setpoints.py @@ -112,7 +112,7 @@ def test_setpoints_non_parameter_raises() -> None: param_with_setpoints_1 = ParameterWithSetpoints( "param_1", get_cmd=lambda: _rng.random(n_points_1()), - setpoints=(lambda x: x,), # type: ignore[arg-type] + setpoints=(lambda x: x,), # type: ignore[arg-type] # ty: ignore[invalid-argument-type] vals=vals.Arrays(shape=(n_points_1,)), ) @@ -123,7 +123,7 @@ def test_setpoints_non_parameter_raises() -> None: ) with pytest.raises(TypeError, match=err_msg): - param_with_setpoints_1.setpoints = (lambda x: x,) # type: ignore[assignment] + param_with_setpoints_1.setpoints = (lambda x: x,) # type: ignore[assignment] # ty: ignore[invalid-assignment] def test_validation_inconsistent_shape() -> None: @@ -331,7 +331,7 @@ def test_validation_one_dim_missing() -> None: "param_6", get_cmd=lambda: _rng.random(n_points_1()), setpoints=(setpoints_1,), - vals=vals.Arrays(shape=(n_points_1, None)), # type: ignore[arg-type] + vals=vals.Arrays(shape=(n_points_1, None)), # type: ignore[arg-type] # ty: ignore[invalid-argument-type] ) expected_err_msg = ( r"One or more dimensions have unknown shape " @@ -358,7 +358,7 @@ def test_validation_one_sp_dim_missing() -> None: setpoints_1 = Parameter( "setpoints_1", get_cmd=lambda: _rng.random(n_points_1()), - vals=vals.Arrays(shape=(n_points_1, None)), # type: ignore[arg-type] + vals=vals.Arrays(shape=(n_points_1, None)), # type: ignore[arg-type] # ty: ignore[invalid-argument-type] ) param_sp_without_shape = ParameterWithSetpoints( "param_6", diff --git a/tests/parameter/test_scaled_parameter.py b/tests/parameter/test_scaled_parameter.py index fb2357ce406..9b5d480abd8 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 79097757323..573de8e5b4d 100644 --- a/tests/parameter/test_snapshot.py +++ b/tests/parameter/test_snapshot.py @@ -56,13 +56,13 @@ def wrapped_func(*args: P.args, **kwargs: P.kwargs) -> T: call_count += 1 return get_func(*args, **kwargs) - wrapped_func.call_count = lambda: call_count # type: ignore[attr-defined] + wrapped_func.call_count = lambda: call_count # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] return wrapped_func p.get = wrap_in_call_counter(p.get) # pre-condition - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] else: # pre-condition assert not hasattr(p, "get") @@ -159,7 +159,7 @@ def test_snapshot_timestamp_for_valid_cache_depends_on_cache_update( # Hack cache's timestamp to simplify this test timestamp = p.cache.timestamp assert timestamp is not None - p.cache._timestamp = timestamp - timedelta(days=31) # type: ignore[attr-defined] + p.cache._timestamp = timestamp - timedelta(days=31) # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] tu = datetime.now(UTC) assert p.cache.timestamp is not None @@ -240,7 +240,7 @@ def test_snapshot_when_snapshot_value_is_false( assert "raw_value" not in s if get_cmd is not False: - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def test_snapshot_value_is_true_by_default( @@ -295,7 +295,7 @@ def test_snapshot_when_snapshot_get_is_false( assert s["raw_value"] is None if get_cmd is not False: - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def test_snapshot_of_non_gettable_parameter_mirrors_cache( @@ -346,15 +346,15 @@ def test_snapshot_of_gettable_parameter_depends_on_update( if should_get: assert s["value"] == 65 assert s["raw_value"] == 69 - assert p.get.call_count() == 1 # type: ignore[attr-defined] + assert p.get.call_count() == 1 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] elif cache_is_valid: assert s["value"] == 42 assert s["raw_value"] == 46 - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] else: assert s["value"] is None assert s["raw_value"] is None - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def test_snapshot_value() -> None: @@ -389,7 +389,7 @@ def test_normalize_snapshot_update_maps_to_canonical_values() -> None: def test_normalize_snapshot_update_rejects_unknown_string() -> None: with pytest.raises(ValueError, match="Invalid value for snapshot"): - normalize_snapshot_update("bogus") # type: ignore[arg-type] + normalize_snapshot_update("bogus") # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_snapshot_update_all_always_calls_get() -> None: @@ -401,7 +401,7 @@ def test_snapshot_update_all_always_calls_get() -> None: ) s = p.snapshot(update="All") assert s["value"] == 69 - assert p.get.call_count() == 1 # type: ignore[attr-defined] + assert p.get.call_count() == 1 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def test_snapshot_update_never_never_calls_get() -> None: @@ -413,7 +413,7 @@ def test_snapshot_update_never_never_calls_get() -> None: ) s = p.snapshot(update="Never") assert s["value"] is None - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def test_snapshot_update_only_invalid_calls_get_when_cache_invalid() -> None: @@ -425,7 +425,7 @@ def test_snapshot_update_only_invalid_calls_get_when_cache_invalid() -> None: ) s = p.snapshot(update="Only_invalid") assert s["value"] == 69 - assert p.get.call_count() == 1 # type: ignore[attr-defined] + assert p.get.call_count() == 1 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def test_snapshot_update_only_invalid_skips_get_when_cache_valid() -> None: @@ -438,7 +438,7 @@ def test_snapshot_update_only_invalid_skips_get_when_cache_valid() -> None: s = p.snapshot(update="Only_invalid") # the cached (set) value is used, ``get`` is not called assert s["value"] == 42 - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] @pytest.mark.parametrize( @@ -471,8 +471,8 @@ def test_snapshot_update_string_matches_legacy_value( assert s_legacy["value"] == s_string["value"] assert s_legacy["raw_value"] == s_string["raw_value"] assert ( - p_legacy.get.call_count() # type: ignore[attr-defined] - == p_string.get.call_count() # type: ignore[attr-defined] + p_legacy.get.call_count() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] + == p_string.get.call_count() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] ) @@ -484,4 +484,4 @@ def test_snapshot_rejects_unknown_update_value() -> None: cache_is_valid=True, ) with pytest.raises(ValueError, match="Invalid value for snapshot"): - p.snapshot(update="bogus") # type: ignore[arg-type] + p.snapshot(update="bogus") # type: ignore[arg-type] # ty: ignore[no-matching-overload] diff --git a/tests/test_channels.py b/tests/test_channels.py index 0537146357a..041ebcf2043 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 7b9dbc34bfa..4f662685258 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 55504814580..34dd4ef9548 100644 --- a/tests/test_instrument.py +++ b/tests/test_instrument.py @@ -438,7 +438,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") @@ -552,7 +552,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 dfa00e32dbc..81d3c3c3b1e 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -372,7 +372,7 @@ def test_installation_info_logging() -> None: def test_get_level_name_with_invalid_type() -> None: """Only str and int can be converted to a logging level name.""" with pytest.raises(RuntimeError, match="get_level_name: Cannot to convert level"): - logger.get_level_name(1.5) # type: ignore[arg-type] + logger.get_level_name(1.5) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_get_level_code_with_invalid_type() -> None: @@ -661,7 +661,7 @@ def test_submodule_logger_is_not_replaced_without_instrument_scope() -> None: def test_nested_submodule_logger_is_child_of_its_parent_module() -> None: inst = ScopedDummyChannelInstrument("nested_modules") channel = inst.submodules["A"] - nested = InstrumentModule(channel, "nested") # type: ignore[arg-type] + nested = InstrumentModule(channel, "nested") # type: ignore[arg-type] # ty: ignore[invalid-argument-type] channel.add_submodule("nested", nested) # type: ignore[union-attr] assert nested.name_parts == ["nested_modules", "ChanA", "nested"] diff --git a/tests/test_metadata.py b/tests/test_metadata.py index b608d7bd2b0..2071cfc9834 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -25,7 +25,7 @@ def snapshot_base( class HasSnapshot(Metadatable): # Users shouldn't do this... but we'll test its behavior # for completeness - def snapshot(self, update: bool | None = False) -> Snapshot: # type: ignore[misc] + def snapshot(self, update: bool | None = False) -> Snapshot: # type: ignore[misc] # ty: ignore[invalid-method-override, override-of-final-method] return {"fruit": "kiwi"} @@ -64,7 +64,7 @@ def test_load() -> None: def test_init() -> None: with pytest.raises(TypeError): - Metadatable(metadata={"2": 3}, not_metadata={4: 5}) # type: ignore[call-arg] + Metadatable(metadata={"2": 3}, not_metadata={4: 5}) # type: ignore[call-arg] # ty: ignore[unknown-argument] m = Metadatable(metadata={"2": 3}) assert m.metadata == {"2": 3} diff --git a/tests/test_station.py b/tests/test_station.py index 0c633b325f0..8ae4590889e 100644 --- a/tests/test_station.py +++ b/tests/test_station.py @@ -118,13 +118,13 @@ def test_add_component_with_no_name() -> None: """ bob = {"name", "bob"} station = Station() - station.add_component(bob) # type: ignore[arg-type] + station.add_component(bob) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert ["component0"] == list(station.components.keys()) assert bob == station.components["component0"] # type: ignore[comparison-overlap] jay = {"name", "jay"} - station.add_component(jay) # type: ignore[arg-type] + station.add_component(jay) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert ["component0", "component1"] == list(station.components.keys()) assert jay == station.components["component1"] # type: ignore[comparison-overlap] diff --git a/tests/utils/test_class_strings.py b/tests/utils/test_class_strings.py index 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 4088c798619..9af685caf73 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 d9f36a519ce5fb851190ce680eb25998e452ad24 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:16:01 +0200 Subject: [PATCH 52/69] Suppress ty where pyright is already suppressed These lines deliberately pass invalid arguments or subscripts, or mock attributes that are not declared on the annotated type, to check the runtime error handling. They are already suppressed for pyright. --- .../test_measurement_context_manager.py | 2 +- tests/dataset/test_dataset_export.py | 8 ++++---- tests/dataset/test_measurement_extensions.py | 6 +++--- tests/dataset/test_string_data.py | 2 +- .../b1500_driver_tests/test_b1500.py | 14 +++++++------- .../b1500_driver_tests/test_b1517a_smu.py | 6 +++--- .../test_sampling_measurement.py | 2 +- tests/parameter/test_keyword_only_args.py | 4 ++-- tests/parameter/test_snapshot.py | 2 +- .../sphinx_extension/test_parse_parameter_attr.py | 2 +- tests/test_channels.py | 12 ++++++------ tests/validators/test_enum.py | 2 +- tests/validators/test_literal.py | 4 ++-- 13 files changed, 33 insertions(+), 33 deletions(-) diff --git a/tests/dataset/measurement/test_measurement_context_manager.py b/tests/dataset/measurement/test_measurement_context_manager.py index 92481999e18..51815dc9e64 100644 --- a/tests/dataset/measurement/test_measurement_context_manager.py +++ b/tests/dataset/measurement/test_measurement_context_manager.py @@ -310,7 +310,7 @@ def test_unregister_parameter(DAC: DummyInstrument, DMM: DummyInstrument) -> Non not_parameters = [DAC, DMM, 0.0, 1] for notparam in not_parameters: with pytest.raises(ValueError): - meas.unregister_parameter(notparam) # pyright: ignore[reportArgumentType] + meas.unregister_parameter(notparam) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] # unregistering something not registered should silently "succeed" meas.unregister_parameter("totes_not_registered") diff --git a/tests/dataset/test_dataset_export.py b/tests/dataset/test_dataset_export.py index aad6be991ab..5c8fcf451e0 100644 --- a/tests/dataset/test_dataset_export.py +++ b/tests/dataset/test_dataset_export.py @@ -1874,7 +1874,7 @@ def test_multi_index_export_with_inferred_parameter( ) -> None: """Inferred parameters must export correctly when a MultiIndex dim is used.""" xds = mock_dataset_non_grid_inferred.to_xarray_dataset( - use_multi_index=use_multi_index # pyright: ignore[reportArgumentType] + use_multi_index=use_multi_index # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] ) assert xds.sizes == {"multi_index": 50} @@ -1898,13 +1898,13 @@ def test_non_unique_multi_index_export_with_inferred_parameter( def test_multi_index_wrong_option(mock_dataset_non_grid: DataSet) -> None: with pytest.raises(ValueError, match="Invalid value for use_multi_index"): - mock_dataset_non_grid.to_xarray_dataset(use_multi_index=True) # pyright: ignore[reportArgumentType] + mock_dataset_non_grid.to_xarray_dataset(use_multi_index=True) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] with pytest.raises(ValueError, match="Invalid value for use_multi_index"): - mock_dataset_non_grid.to_xarray_dataset(use_multi_index=False) # pyright: ignore[reportArgumentType] + mock_dataset_non_grid.to_xarray_dataset(use_multi_index=False) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] with pytest.raises(ValueError, match="Invalid value for use_multi_index"): - mock_dataset_non_grid.to_xarray_dataset(use_multi_index="perhaps") # pyright: ignore[reportArgumentType] + mock_dataset_non_grid.to_xarray_dataset(use_multi_index="perhaps") # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] def test_geneate_pandas_index() -> None: diff --git a/tests/dataset/test_measurement_extensions.py b/tests/dataset/test_measurement_extensions.py index ab84afca6cd..a88f2caddc7 100644 --- a/tests/dataset/test_measurement_extensions.py +++ b/tests/dataset/test_measurement_extensions.py @@ -371,7 +371,7 @@ def test_dond_into_fails_with_together_sweeps( ): dond_into( datasaver, - TogetherSweep(sweep1, sweep2), # pyright: ignore [reportArgumentType] + TogetherSweep(sweep1, sweep2), # pyright: ignore [reportArgumentType] # ty: ignore[invalid-argument-type] meas1, ) @@ -394,8 +394,8 @@ def test_dond_into_fails_with_groups( dond_into( datasaver, sweep1, - [meas1], # pyright: ignore [reportArgumentType] - [meas2], # pyright: ignore [reportArgumentType] + [meas1], # pyright: ignore [reportArgumentType] # ty: ignore[invalid-argument-type] + [meas2], # pyright: ignore [reportArgumentType] # ty: ignore[invalid-argument-type] ) diff --git a/tests/dataset/test_string_data.py b/tests/dataset/test_string_data.py index 8484dd02d03..a5639f67fc0 100644 --- a/tests/dataset/test_string_data.py +++ b/tests/dataset/test_string_data.py @@ -205,7 +205,7 @@ def test_list_of_strings(experiment: "Experiment") -> None: @settings(suppress_health_check=(HealthCheck.function_scoped_fixture,), deadline=None) @given( - p_values=hypnumpy.arrays( + p_values=hypnumpy.arrays( # ty: ignore[no-matching-overload] dtype=hst.sampled_from( ( hypnumpy.unicode_string_dtypes(), diff --git a/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1500.py b/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1500.py index 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/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 573de8e5b4d..c5f531eb9de 100644 --- a/tests/parameter/test_snapshot.py +++ b/tests/parameter/test_snapshot.py @@ -465,7 +465,7 @@ def test_snapshot_update_string_matches_legacy_value( # ``update=legacy`` intentionally uses the deprecated bool/None values to # confirm they still map to the new canonical behavior. - s_legacy = p_legacy.snapshot(update=legacy) # pyright: ignore[reportDeprecated] + s_legacy = p_legacy.snapshot(update=legacy) # pyright: ignore[reportDeprecated] # ty: ignore[deprecated] s_string = p_string.snapshot(update=string) assert s_legacy["value"] == s_string["value"] diff --git a/tests/sphinx_extension/test_parse_parameter_attr.py b/tests/sphinx_extension/test_parse_parameter_attr.py index c0331a71519..fc2dbc86902 100644 --- a/tests/sphinx_extension/test_parse_parameter_attr.py +++ b/tests/sphinx_extension/test_parse_parameter_attr.py @@ -104,7 +104,7 @@ def test_decorated_init_func() -> None: def test_decorated_class() -> None: - attr = qcodes_parameter_attr_getter(DummyDecoratedClassTestClass, "other_attr") # pyright: ignore[reportDeprecated] + attr = qcodes_parameter_attr_getter(DummyDecoratedClassTestClass, "other_attr") # pyright: ignore[reportDeprecated] # ty: ignore[deprecated] assert isinstance(attr, ParameterProxy) assert repr(attr) == '"InstanceAttribute"' diff --git a/tests/test_channels.py b/tests/test_channels.py index 041ebcf2043..6bc42dc3576 100644 --- a/tests/test_channels.py +++ b/tests/test_channels.py @@ -214,7 +214,7 @@ def test_append_channel_wrong_type_raises(dci_with_list: DCIWithList) -> None: channel = EmptyChannel(dci_with_list, "foo") with pytest.raises(TypeError, match="All items in a channel list must"): - dci_with_list.channels.append(channel) # pyright: ignore[reportArgumentType] + dci_with_list.channels.append(channel) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] assert len(dci_with_list.channels) == n_channels @@ -244,7 +244,7 @@ def test_extend_wrong_type_raises(dci_with_list: DCIWithList) -> None: TypeError, match=re.escape("All items in a channel list must be of the same type."), ): - dci_with_list.channels.extend(channels) # pyright: ignore[reportArgumentType] + dci_with_list.channels.extend(channels) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] def test_extend_locked_list_raises(dci_with_list: DCIWithList) -> None: @@ -252,7 +252,7 @@ def test_extend_locked_list_raises(dci_with_list: DCIWithList) -> None: names = ("foo", "bar", "foobar") channels = tuple(EmptyChannel(dci_with_list, "Chan" + name) for name in names) with pytest.raises(AttributeError, match="Cannot extend a locked channel list"): - dci_with_list.channels.extend(channels) # pyright: ignore[reportArgumentType] + dci_with_list.channels.extend(channels) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] def test_extend_then_remove(dci_with_list: DCIWithList) -> None: @@ -292,7 +292,7 @@ def test_insert_channel(dci_with_list: DCIWithList) -> None: def test_insert_channel_wrong_type_raises(dci_with_list: DCIWithList) -> None: with pytest.raises(TypeError, match="All items in a channel list"): - dci_with_list.channels.insert(1, EmptyChannel(parent=dci_with_list, name="foo")) # pyright: ignore[reportArgumentType] + dci_with_list.channels.insert(1, EmptyChannel(parent=dci_with_list, name="foo")) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] def test_channel_type_can_be_inferred( @@ -308,7 +308,7 @@ def test_add_none_channel_tuple_to_channel_tuple_raises( dci: DummyChannelInstrument, ) -> None: with pytest.raises(TypeError, match="Can't add objects of type"): - _ = dci.channels + [1] # pyright: ignore[reportOperatorIssue] # noqa: RUF005 + _ = dci.channels + [1] # pyright: ignore[reportOperatorIssue] # ty: ignore[unsupported-operator] # noqa: RUF005 def test_add_channel_tuples_of_different_types_raises( @@ -524,7 +524,7 @@ def test_access_channels_by_name_empty_raises(dci: DummyChannelInstrument) -> No def test_access_channel_by_name_empty_raises(dci: DummyChannelInstrument) -> None: with pytest.raises(TypeError, match="missing 1 required positional argument"): - dci.channels.get_channel_by_name() # pyright: ignore[reportCallIssue] + dci.channels.get_channel_by_name() # pyright: ignore[reportCallIssue] # ty: ignore[missing-argument] def test_delete_from_channel_list(dci_with_list: DCIWithList) -> None: diff --git a/tests/validators/test_enum.py b/tests/validators/test_enum.py index 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 cb625d1d122..69bbfcd4b2c 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 cb09830f53433e973701e87485a4200f90c2c82e Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:16:45 +0200 Subject: [PATCH 53/69] 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 b07ef2cef9bf17b7e8c9b19ec1ad01541c1338d0 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:17:12 +0200 Subject: [PATCH 54/69] 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 16130be44e9eedd89e9c66d64f025e81fe82e73a Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:18:19 +0200 Subject: [PATCH 55/69] 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 350decb674e..6f8c924079a 100644 --- a/tests/dataset/test_dataset_basic.py +++ b/tests/dataset/test_dataset_basic.py @@ -1017,7 +1017,7 @@ def test_get_array_parameter_data_no_nulls( expected_names = {} expected_names["val1"] = ["val1", "sp1", "sp2"] expected_names["val2"] = ["val2", "sp1"] - expected_shapes = {} + expected_shapes: dict[str, list[tuple[int, ...]]] = {} expected_values = {} if "array" in types: @@ -1056,7 +1056,7 @@ def test_get_array_parameter_data(array_dataset: DataSet) -> None: expected_shapes: dict[str, list[tuple[int, ...]]] = {} expected_len = 5 expected_shapes[par_name] = [(expected_len,), (expected_len,)] - expected_values = {} + expected_values: dict[str, list[np.ndarray]] = {} expected_values[par_name] = [ np.ones(expected_len) + 1, np.linspace(5, 9, expected_len), diff --git a/tests/dataset/test_measurement_extensions.py b/tests/dataset/test_measurement_extensions.py index a88f2caddc7..b21c6392731 100644 --- a/tests/dataset/test_measurement_extensions.py +++ b/tests/dataset/test_measurement_extensions.py @@ -546,7 +546,7 @@ def test_shapes_in_dataset_definition_with_scalar_params( _ = default_database_and_experiment set1, set2, _, meas1, meas2, _ = default_params - expected_shapes = { + expected_shapes: dict[str, tuple[int, ...]] = { meas1.register_name: (11, 11), meas2.register_name: (11, 11), } @@ -575,7 +575,7 @@ def test_shapes_in_dataset_definition_with_pws( _ = default_database_and_experiment pws1, set1 = pws_params - expected_shapes = { + expected_shapes: dict[str, tuple[int, ...]] = { pws1.register_name: (11, 11), } dataset_definition = [ @@ -625,7 +625,7 @@ def test_setup_measurement_instances_sets_shapes( _ = default_database_and_experiment set1, _, _, meas1, _, _ = default_params - expected_shapes = {meas1.register_name: (5,)} + expected_shapes: dict[str, tuple[int, ...]] = {meas1.register_name: (5,)} dataset_definitions = [ DataSetDefinition( name="test_shapes", @@ -667,8 +667,8 @@ def test_shapes_with_multiple_datasets( _ = default_database_and_experiment set1, set2, set3, meas1, _, meas3 = default_params - shapes_1 = {meas1.register_name: (11, 11)} - shapes_2 = {meas3.register_name: (11, 11)} + shapes_1: dict[str, tuple[int, ...]] = {meas1.register_name: (11, 11)} + shapes_2: dict[str, tuple[int, ...]] = {meas3.register_name: (11, 11)} dataset_definition = [ DataSetDefinition( name="dataset_1", diff --git a/tests/dataset/test_sqlite_base.py b/tests/dataset/test_sqlite_base.py index 3cfea0c9bd6..d00b3aa5fad 100644 --- a/tests/dataset/test_sqlite_base.py +++ b/tests/dataset/test_sqlite_base.py @@ -64,7 +64,7 @@ def _make_simple_run_describer() -> "Generator[RunDescriber, None, None]": t = ParamSpecBase("t", "numeric") y = ParamSpecBase("y", "numeric") - paramtree = {y: (x, t)} + paramtree: dict[ParamSpecBase, tuple[ParamSpecBase, ...]] = {y: (x, t)} interdependencies = InterDependencies_(dependencies=paramtree) rundescriber = RunDescriber(interdependencies) From 4d78a1cd08e021007a772751b02c1f36743725bf Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:18:59 +0200 Subject: [PATCH 56/69] 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 42385039348..dd65060cb48 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 549f263c500..bb6777895e5 100644 --- a/tests/parameter/test_parameter_ramp.py +++ b/tests/parameter/test_parameter_ramp.py @@ -62,6 +62,7 @@ def test_ramp_scaled(scale: int, value: float) -> None: assert p.raw_value == first_step * scale # then check the generated steps. They should not be scaled as the # scaling happens when setting them + assert p.step is not None expected_steps = np.linspace(first_step + p.step, second_step, 90) actual_steps = p.get_ramp_values(second_step, p.step) np.testing.assert_allclose(np.array(actual_steps), expected_steps) @@ -104,6 +105,7 @@ def test_ramp_parser(value: float) -> None: assert p.raw_value == -first_step # then check the generated steps. They should not be parsed as the # scaling happens when setting them + assert p.step is not None expected_steps = np.linspace((first_step + p.step), second_step, 90) actual_steps = p.get_ramp_values(second_step, p.step) np.testing.assert_allclose(np.array(actual_steps), expected_steps) @@ -142,6 +144,7 @@ def test_ramp_parsed_scaled(scale: int, value: float) -> None: # these are parsed in the set_wrapper np.testing.assert_allclose(np.array(p.set_values), expected_raw_steps) assert p.raw_value == -scale * first_step + assert p.step is not None expected_steps = np.linspace(first_step + p.step, second_step, 90) actual_steps = p.get_ramp_values(10, p.step) np.testing.assert_allclose(np.array(actual_steps), expected_steps) From f247940a9a2e356da18ee69f22e59e89251e10ed Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:20:22 +0200 Subject: [PATCH 57/69] Suppress ty on TypeVars used as plain values ty requires a legacy TypeVar to be assigned to a variable matching its name, so it rejects the ones these tests store straight into a dict. They are values handed to _make_deprecated_typevars_getattr rather than type variables used to parametrize anything, and both mypy and pyright accept them. --- tests/utils/test_deprecate.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/utils/test_deprecate.py b/tests/utils/test_deprecate.py index 24b371f4962..454d396c1b5 100644 --- a/tests/utils/test_deprecate.py +++ b/tests/utils/test_deprecate.py @@ -16,9 +16,11 @@ @pytest.fixture def deprecated_getattr() -> _FixtureT: """Create a __getattr__ with two deprecated TypeVars.""" + # these TypeVars are values handed to the deprecation shim rather than + # type variables used to parametrize anything, which ty does not allow deprecated = { - "MyT": TypeVar("MyT"), - "MyK": TypeVar("MyK", bound=int), + "MyT": TypeVar("MyT"), # ty: ignore[invalid-legacy-type-variable] + "MyK": TypeVar("MyK", bound=int), # ty: ignore[invalid-legacy-type-variable] } getattr_fn = _make_deprecated_typevars_getattr("fake.module", deprecated) return deprecated, getattr_fn @@ -63,7 +65,7 @@ def test_repeated_access_returns_same_object( def test_fallback_is_called_for_unknown_names() -> None: - deprecated: dict[str, TypeVar] = {"X": TypeVar("X")} + deprecated: dict[str, TypeVar] = {"X": TypeVar("X")} # ty: ignore[invalid-legacy-type-variable] def fallback(name: str) -> str: return f"fallback:{name}" @@ -73,7 +75,7 @@ def fallback(name: str) -> str: def test_fallback_not_called_for_deprecated_names() -> None: - deprecated: dict[str, TypeVar] = {"X": TypeVar("X")} + deprecated: dict[str, TypeVar] = {"X": TypeVar("X")} # ty: ignore[invalid-legacy-type-variable] fallback_called = False def fallback(name: str) -> str: From 2dcb733a2fc4a526ffeda2def629a66a05595bc7 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:20:48 +0200 Subject: [PATCH 58/69] 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 3017b87c913..c9dfc5d574f 100644 --- a/tests/parameter/test_parameter_override.py +++ b/tests/parameter/test_parameter_override.py @@ -33,7 +33,7 @@ def __init__(self, name: str, **kwargs: "Unpack[InstrumentBaseKWArgs]") -> None: This instrument errors because it tries to override an attribute with a parameter. """ super().__init__(name, **kwargs) - self.voltage = self.add_parameter("voltage", set_cmd=None, get_cmd=None) + self.voltage = self.add_parameter("voltage", set_cmd=None, get_cmd=None) # ty: ignore[invalid-assignment] def voltage(self) -> int: return 0 From e41a981a19ce4dcd813b740f9db924945534431d Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:21:37 +0200 Subject: [PATCH 59/69] 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 0f3b7aff565..00ea6fc4f7e 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 08df14c11e422afd1cb7d1e7418004f60ce34cde Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 21 Sep 2026 21:47:21 +0200 Subject: [PATCH 60/69] Fix the tests in the simulated instrument notebook The test cells imported the Weinschel8320 driver from qcodes, replacing the class of the same name that the notebook defines, which ty reports as an invalid assignment. Remove those imports so the tests use the driver from the notebook, and annotate the fixtures and tests. test_init_v2 requested a driver fixture that does not exist, so request weinschel_driver_2 instead. Also clear the stale output of the first cell. --- ...reating-Simulated-PyVISA-Instruments.ipynb | 39 +++++-------------- 1 file changed, 10 insertions(+), 29 deletions(-) diff --git a/docs/examples/writing_drivers/Creating-Simulated-PyVISA-Instruments.ipynb b/docs/examples/writing_drivers/Creating-Simulated-PyVISA-Instruments.ipynb index 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 7a53aff1e913ad51ef835f5b204f6a3ea0b754f5 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 21 Sep 2026 21:51:07 +0200 Subject: [PATCH 61/69] Annotate the IDN parameter as a dict IDN returns the dict produced by get_idn. Annotate it as Parameter[dict[str, str], Self]. Strictly the values may be None, but including that would require asserts at every use. The Weinschel 8320 test treated the result as a string. It only runs with an instance of the driver, so this went unnoticed. Check the vendor and model instead. --- src/qcodes/instrument/instrument.py | 7 ++++++- tests/drivers/test_weinchel.py | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/qcodes/instrument/instrument.py b/src/qcodes/instrument/instrument.py index a9b11372bf4..43a95f1a495 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 641c1f365e421baad05adc7d2140c28ec00fb121 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 25 Sep 2026 18:46:19 +0200 Subject: [PATCH 62/69] Compare the global set callback against None ty warns that testing a callable for truthiness is likely a missing call. Checking for None instead states what is meant and matches how ParameterBase itself guards the callback. --- tests/parameter/test_parameter_on_set_callback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/parameter/test_parameter_on_set_callback.py b/tests/parameter/test_parameter_on_set_callback.py index 89bc5a7f4aa..adf5be97045 100644 --- a/tests/parameter/test_parameter_on_set_callback.py +++ b/tests/parameter/test_parameter_on_set_callback.py @@ -275,7 +275,7 @@ def test_set_callback_for_instance( captured_instance_params = [] def callback(param: ParameterBase, val: Any) -> None: - if ParameterBase.global_on_set_callback: + if ParameterBase.global_on_set_callback is not None: ParameterBase.global_on_set_callback(param, val) captured_instance_params.append(val) From 0a4c7f04ddb00ca650a5009149b18844fbfb463e Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 14 Oct 2026 07:29:03 +0200 Subject: [PATCH 63/69] Suppress ty on deliberately invalid test code ty reports the fixtures that overwrite get and set on a Parameter subclass as invalid overrides of a mutable attribute. ParameterBase rejects that at runtime, and asserting it is the whole point of the fixtures, so they are invalid on purpose. The invalid types assigned in test_parameter_mixin and the float passed to get_level_code are deliberate too and were already suppressed for mypy. --- tests/extensions/parameters/test_parameter_mixin.py | 4 ++-- tests/parameter/conftest.py | 8 ++++++-- tests/test_logger.py | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/extensions/parameters/test_parameter_mixin.py b/tests/extensions/parameters/test_parameter_mixin.py index 424397260b6..6b87855c50a 100644 --- a/tests/extensions/parameters/test_parameter_mixin.py +++ b/tests/extensions/parameters/test_parameter_mixin.py @@ -90,7 +90,7 @@ def test_compatible_bases_must_be_list() -> None: with pytest.raises(TypeError): class IncorrectParameterMixin(ParameterMixin): - _COMPATIBLE_BASES = "notalist" # type: ignore[assignment] + _COMPATIBLE_BASES = "notalist" # type: ignore[assignment] # ty: ignore[invalid-assignment] def test_incompatible_bases_must_be_list() -> None: @@ -100,7 +100,7 @@ class IncorrectParameterMixin(ParameterMixin): _COMPATIBLE_BASES: ClassVar[list[type[ParameterBase]]] = [ CompatibleParameter ] - _INCOMPATIBLE_BASES = IncompatibleParameter # type: ignore[assignment] + _INCOMPATIBLE_BASES = IncompatibleParameter # type: ignore[assignment] # ty: ignore[invalid-assignment] ############################## diff --git a/tests/parameter/conftest.py b/tests/parameter/conftest.py index 49128488b22..c1665e64e11 100644 --- a/tests/parameter/conftest.py +++ b/tests/parameter/conftest.py @@ -103,7 +103,9 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.set_count = 0 self.get_count = 0 - def get(self) -> int: + # overwriting get is not allowed and raises at runtime, which is what + # test_parameter_with_overwritten_get_raises checks + def get(self) -> int: # ty: ignore[invalid-mutable-override] self.get_count += 1 return self._value @@ -117,7 +119,9 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.set_count = 0 self.get_count = 0 - def set(self, value: Any) -> None: + # overwriting set is not allowed and raises at runtime, which is what + # test_parameter_with_overwritten_set_raises checks + def set(self, value: Any) -> None: # ty: ignore[invalid-mutable-override] self.set_count += 1 self._value = value diff --git a/tests/test_logger.py b/tests/test_logger.py index 81d3c3c3b1e..45bfc62b82a 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -378,7 +378,7 @@ def test_get_level_name_with_invalid_type() -> None: def test_get_level_code_with_invalid_type() -> None: """Only str and int can be converted to a logging level code.""" with pytest.raises(RuntimeError, match="get_level_code: Cannot to convert level"): - logger.get_level_code(1.5) # type: ignore[arg-type] + logger.get_level_code(1.5) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] class ScopedDummyInstrument(DummyInstrument): From a8cbb5a75e690c3f61edd7040f2daf7699b60832 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 22 Oct 2026 20:57:58 +0200 Subject: [PATCH 64/69] Suppress ty errors on deliberately mistyped tests The type annotations added to the tests on main come with pyright and mypy suppressions on the lines that pass invalid types on purpose, mostly to check runtime error handling. Suppress the corresponding ty errors too. --- tests/dataset/dond/test_dond.py | 4 ++-- .../measurement/test_measurement_context_manager.py | 2 +- tests/dataset/test_dataset_basic.py | 6 +++--- tests/dataset/test_experiment_container.py | 4 ++-- .../test_parameter_mixin_interdependent.py | 6 +++--- .../test_parameter_mixin_on_cache_change.py | 2 +- tests/parameter/test_permissive_range.py | 2 +- tests/test_config.py | 12 ++++++------ tests/test_sweep_values.py | 4 ++-- 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/tests/dataset/dond/test_dond.py b/tests/dataset/dond/test_dond.py index dc12841c423..162ad2535c2 100644 --- a/tests/dataset/dond/test_dond.py +++ b/tests/dataset/dond/test_dond.py @@ -1092,8 +1092,8 @@ def test_dond_2d_multi_datasets_with_callable_output_data( exp_1 = dond( sweep_1, sweep_2, - [_string_callable, _param, _param_complex], # pyright: ignore[reportArgumentType] - [_string_callable, _param_2, _param_complex_2], # pyright: ignore[reportArgumentType] + [_string_callable, _param, _param_complex], # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + [_string_callable, _param_2, _param_complex_2], # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] ) datasets = exp_1[0] assert isinstance(datasets, tuple) diff --git a/tests/dataset/measurement/test_measurement_context_manager.py b/tests/dataset/measurement/test_measurement_context_manager.py index 51815dc9e64..b7e3df237e9 100644 --- a/tests/dataset/measurement/test_measurement_context_manager.py +++ b/tests/dataset/measurement/test_measurement_context_manager.py @@ -414,7 +414,7 @@ def test_setting_write_period(wp: float | str) -> None: if isinstance(wp, str): with pytest.raises(ValueError): - meas.write_period = wp # type: ignore[assignment] + meas.write_period = wp # type: ignore[assignment] # ty: ignore[invalid-assignment] elif wp < 1e-3: with pytest.raises(ValueError): meas.write_period = wp diff --git a/tests/dataset/test_dataset_basic.py b/tests/dataset/test_dataset_basic.py index 6f8c924079a..91e094468ce 100644 --- a/tests/dataset/test_dataset_basic.py +++ b/tests/dataset/test_dataset_basic.py @@ -284,7 +284,7 @@ def test_create_dataset_from_non_existing_run_id( ValueError, match=f"Run with run_id {non_existing_run_id} does not exist in the database", ): - _ = DataSet(run_id=non_existing_run_id) # pyright: ignore[reportArgumentType] + _ = DataSet(run_id=non_existing_run_id) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] def test_create_dataset_pass_both_connection_and_path_to_db( @@ -573,7 +573,7 @@ def test_load_by_counter_for_nonexisting_counter( ) -> None: exp_id = 1 with pytest.raises(RuntimeError, match="Expected one row"): - _ = load_by_counter(exp_id, nonexisting_counter) # pyright: ignore[reportArgumentType] + _ = load_by_counter(exp_id, nonexisting_counter) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] @pytest.mark.usefixtures("empty_temp_db") @@ -582,7 +582,7 @@ def test_load_by_counter_for_nonexisting_experiment( nonexisting_exp_id: int | None, ) -> None: with pytest.raises(RuntimeError, match="Expected one row"): - _ = load_by_counter(nonexisting_exp_id, 1) # pyright: ignore[reportArgumentType] + _ = load_by_counter(nonexisting_exp_id, 1) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] @pytest.mark.usefixtures("empty_temp_db") diff --git a/tests/dataset/test_experiment_container.py b/tests/dataset/test_experiment_container.py index 720f47757a2..8e7126681eb 100644 --- a/tests/dataset/test_experiment_container.py +++ b/tests/dataset/test_experiment_container.py @@ -169,7 +169,7 @@ def test_experiment_read_only_properties(experiment: "Experiment") -> None: @pytest.mark.parametrize("non_existing_id", (1, 0, -1, "number#42")) def test_create_experiment_from_non_existing_id(non_existing_id: int | str) -> None: with pytest.raises(ValueError, match="No such experiment in the database"): - _ = Experiment(exp_id=non_existing_id) # pyright: ignore[reportArgumentType] + _ = Experiment(exp_id=non_existing_id) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] @pytest.mark.usefixtures("empty_temp_db") @@ -183,7 +183,7 @@ def test_load_experiment_from_non_existing_id(non_existing_id: "int") -> None: @pytest.mark.parametrize("bad_id", (None, "number#42")) def test_load_experiment_from_bad_id(bad_id: str | None) -> None: with pytest.raises(ValueError, match="Experiment ID must be an integer"): - _ = load_experiment(bad_id) # pyright: ignore[reportArgumentType] + _ = load_experiment(bad_id) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] def test_format_string(empty_temp_db: "None") -> None: diff --git a/tests/extensions/parameters/test_parameter_mixin_interdependent.py b/tests/extensions/parameters/test_parameter_mixin_interdependent.py index 407a44f795d..b8d4a304a69 100644 --- a/tests/extensions/parameters/test_parameter_mixin_interdependent.py +++ b/tests/extensions/parameters/test_parameter_mixin_interdependent.py @@ -247,7 +247,7 @@ def test_typeerror_if_dependency_update_method_invalid( with pytest.raises( TypeError, match=re.escape("dependency_update_method must be callable or None.") ): - param.dependency_update_method = 123 # pyright: ignore[reportAttributeAccessIssue] + param.dependency_update_method = 123 # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[invalid-assignment] def test_typeerror_if_dependent_on_invalid(mock_instr: MockInstrument) -> None: @@ -261,10 +261,10 @@ def test_typeerror_if_dependent_on_invalid(mock_instr: MockInstrument) -> None: with pytest.raises( TypeError, match=re.escape("dependent_on must be a list of strings.") ): - param.dependent_on = "not_a_list" # pyright: ignore[reportAttributeAccessIssue] + param.dependent_on = "not_a_list" # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[invalid-assignment] # Assign a list with non-string with pytest.raises( TypeError, match=re.escape("dependent_on must be a list of strings.") ): - param.dependent_on = [123, None] # pyright: ignore[reportAttributeAccessIssue] + param.dependent_on = [123, None] # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[invalid-assignment] diff --git a/tests/extensions/parameters/test_parameter_mixin_on_cache_change.py b/tests/extensions/parameters/test_parameter_mixin_on_cache_change.py index 27d161307ac..cdd6c7dd798 100644 --- a/tests/extensions/parameters/test_parameter_mixin_on_cache_change.py +++ b/tests/extensions/parameters/test_parameter_mixin_on_cache_change.py @@ -100,7 +100,7 @@ def test_error_on_non_callable_callback( docstring="A parameter for testing non-callable callbacks.", ) with pytest.raises(TypeError): - test_parameter.on_cache_change = invalid_callback # pyright: ignore[reportAttributeAccessIssue] + test_parameter.on_cache_change = invalid_callback # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[invalid-assignment] def test_no_callback_invocation_on_init_or_get( diff --git a/tests/parameter/test_permissive_range.py b/tests/parameter/test_permissive_range.py index c4e511b3666..e639b1b831c 100644 --- a/tests/parameter/test_permissive_range.py +++ b/tests/parameter/test_permissive_range.py @@ -25,7 +25,7 @@ @pytest.mark.parametrize("args", bad_args) def test_bad_calls(args: list[float | str | None]) -> None: with pytest.raises(TypeError): - permissive_range(*args) # pyright: ignore[reportArgumentType] + permissive_range(*args) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] @pytest.mark.parametrize("args,result", good_args) diff --git a/tests/test_config.py b/tests/test_config.py index f1cf2315411..762d815a053 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -170,9 +170,9 @@ def test_missing_config_file(config: Config) -> None: def test_default_config_files(config: Config, load_config: "MockType") -> None: load_config.side_effect = partial(side_effect, GOOD_CONFIG_MAP) # don't try to load custom schemas - config.schema_cwd_file_name = None # pyright: ignore[reportAttributeAccessIssue] - config.schema_home_file_name = None # pyright: ignore[reportAttributeAccessIssue] - config.schema_env_file_name = None # pyright: ignore[reportAttributeAccessIssue] + config.schema_cwd_file_name = None # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[invalid-assignment] + config.schema_home_file_name = None # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[invalid-assignment] + config.schema_env_file_name = None # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[invalid-assignment] config.defaults, _ = config.load_default() updated_config = config.update_config() assert updated_config == CONFIG @@ -181,9 +181,9 @@ def test_default_config_files(config: Config, load_config: "MockType") -> None: def test_bad_config_files(config: Config, load_config: "MockType") -> None: load_config.side_effect = partial(side_effect, BAD_CONFIG_MAP) # don't try to load custom schemas - config.schema_cwd_file_name = None # pyright: ignore[reportAttributeAccessIssue] - config.schema_home_file_name = None # pyright: ignore[reportAttributeAccessIssue] - config.schema_env_file_name = None # pyright: ignore[reportAttributeAccessIssue] + config.schema_cwd_file_name = None # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[invalid-assignment] + config.schema_home_file_name = None # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[invalid-assignment] + config.schema_env_file_name = None # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[invalid-assignment] config.defaults, _ = config.load_default() with pytest.raises(jsonschema.exceptions.ValidationError): config.update_config() diff --git a/tests/test_sweep_values.py b/tests/test_sweep_values.py index d01002af449..de6b97c4498 100644 --- a/tests/test_sweep_values.py +++ b/tests/test_sweep_values.py @@ -61,11 +61,11 @@ def test_errors(c0: Parameter, c1: Parameter, c2: Parameter) -> None: # improper use of extend with pytest.raises(TypeError): - c0[0.1].extend(5) # pyright: ignore[reportArgumentType] + c0[0.1].extend(5) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] # SweepValue object has no getter, even if the parameter does with pytest.raises(AttributeError): - _ = c0[0.1].get # pyright: ignore[reportAttributeAccessIssue] + _ = c0[0.1].get # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute] def test_valid(c0: Parameter) -> None: From 5c9e693fc7febf4b9f9676eb6af53817e9faf678 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 22 Oct 2026 21:05:44 +0200 Subject: [PATCH 65/69] Remove casts made redundant by test annotations The test inputs these casts were narrowing are now annotated, so the values already have the type the casts asserted. --- tests/drivers/test_keithley_26xx.py | 4 +- .../test_parameter_mixin_interdependent.py | 108 ++++++++---------- .../test_parameter_mixin_on_cache_change.py | 23 ++-- 3 files changed, 56 insertions(+), 79 deletions(-) diff --git a/tests/drivers/test_keithley_26xx.py b/tests/drivers/test_keithley_26xx.py index 0ddfd44b6d1..61b1d4ca301 100644 --- a/tests/drivers/test_keithley_26xx.py +++ b/tests/drivers/test_keithley_26xx.py @@ -1,6 +1,6 @@ import logging from collections import Counter -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING from unittest.mock import patch import numpy as np @@ -217,8 +217,6 @@ def test_setting_measure_current_range_disables_autorange( def test_fast_sweep_parameters(smus: "tuple[Keithley2600Channel, ...]") -> None: for smu in smus: - smu = cast("Keithley2600Channel", smu) - start_val = 0.0001 stop_val = 0.001 npts = 50 diff --git a/tests/extensions/parameters/test_parameter_mixin_interdependent.py b/tests/extensions/parameters/test_parameter_mixin_interdependent.py index b8d4a304a69..dea4b22eb91 100644 --- a/tests/extensions/parameters/test_parameter_mixin_interdependent.py +++ b/tests/extensions/parameters/test_parameter_mixin_interdependent.py @@ -1,5 +1,5 @@ import re -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any import pytest @@ -94,29 +94,23 @@ def test_dependency_update_invoked_on_change( callback: "Callable[[], None]", mock_instr: MockInstrument, ) -> None: - mock_instr.some_param = cast( - "InterdependentParameter", - mock_instr.add_parameter( - name="some_param", - parameter_class=InterdependentParameter, - set_cmd=lambda x: store.update({"some": x}), - get_cmd=lambda: store.get("some"), - docstring="Parameter some_param represents a primary parameter.", - ), + mock_instr.some_param = mock_instr.add_parameter( + name="some_param", + parameter_class=InterdependentParameter, + set_cmd=lambda x: store.update({"some": x}), + get_cmd=lambda: store.get("some"), + docstring="Parameter some_param represents a primary parameter.", ) """Parameter some_param represents a primary parameter.""" - mock_instr.managed_param = cast( - "InterdependentParameter", - mock_instr.add_parameter( - name="managed_param", - parameter_class=InterdependentParameter, - dependent_on=["some_param"], - dependency_update_method=callback, - set_cmd=lambda x: store.update({"managed": x}), - get_cmd=lambda: store.get("managed"), - docstring="Parameter managed_param depends on some_param.", - ), + mock_instr.managed_param = mock_instr.add_parameter( + name="managed_param", + parameter_class=InterdependentParameter, + dependent_on=["some_param"], + dependency_update_method=callback, + set_cmd=lambda x: store.update({"managed": x}), + get_cmd=lambda: store.get("managed"), + docstring="Parameter managed_param depends on some_param.", ) """Parameter managed_param depends on some_param.""" @@ -130,29 +124,23 @@ def test_adding_dependent_parameter_later( callback: "Callable[[], None]", mock_instr: MockInstrument, ) -> None: - mock_instr.some_param = cast( - "InterdependentParameter", - mock_instr.add_parameter( - name="some_param", - parameter_class=InterdependentParameter, - set_cmd=lambda x: store.update({"some": x}), - get_cmd=lambda: store.get("some"), - docstring="Parameter some_param represents a primary parameter.", - ), + mock_instr.some_param = mock_instr.add_parameter( + name="some_param", + parameter_class=InterdependentParameter, + set_cmd=lambda x: store.update({"some": x}), + get_cmd=lambda: store.get("some"), + docstring="Parameter some_param represents a primary parameter.", ) """Parameter some_param represents a primary parameter.""" - mock_instr.managed_param = cast( - "InterdependentParameter", - mock_instr.add_parameter( - name="managed_param", - parameter_class=InterdependentParameter, - dependent_on=["some_param"], - dependency_update_method=callback, - set_cmd=lambda x: store.update({"managed": x}), - get_cmd=lambda: store.get("managed"), - docstring="Parameter managed_param depends on some_param.", - ), + mock_instr.managed_param = mock_instr.add_parameter( + name="managed_param", + parameter_class=InterdependentParameter, + dependent_on=["some_param"], + dependency_update_method=callback, + set_cmd=lambda x: store.update({"managed": x}), + get_cmd=lambda: store.get("managed"), + docstring="Parameter managed_param depends on some_param.", ) """Parameter managed_param depends on some_param.""" @@ -200,31 +188,25 @@ def test_parsers_and_dependency_propagation( def dependency_update() -> None: mock_instr.managed_param.set(999) - mock_instr.some_param = cast( - "InterdependentParameter", - mock_instr.add_parameter( - name="some_param", - parameter_class=InterdependentParameter, - set_cmd=lambda x: store.update({"some": x}), - get_cmd=lambda: store.get("some"), - set_parser=lambda v: v * 2, - get_parser=lambda v: v + 1 if v is not None else None, - docstring="Parameter some_param with parsers.", - ), + mock_instr.some_param = mock_instr.add_parameter( + name="some_param", + parameter_class=InterdependentParameter, + set_cmd=lambda x: store.update({"some": x}), + get_cmd=lambda: store.get("some"), + set_parser=lambda v: v * 2, + get_parser=lambda v: v + 1 if v is not None else None, + docstring="Parameter some_param with parsers.", ) """Parameter some_param with parsers.""" - mock_instr.managed_param = cast( - "InterdependentParameter", - mock_instr.add_parameter( - name="managed_param", - parameter_class=InterdependentParameter, - dependent_on=["some_param"], - dependency_update_method=dependency_update, - set_cmd=lambda x: store.update({"managed": x}), - get_cmd=lambda: store.get("managed"), - docstring="Parameter managed_param depends on some_param.", - ), + mock_instr.managed_param = mock_instr.add_parameter( + name="managed_param", + parameter_class=InterdependentParameter, + dependent_on=["some_param"], + dependency_update_method=dependency_update, + set_cmd=lambda x: store.update({"managed": x}), + get_cmd=lambda: store.get("managed"), + docstring="Parameter managed_param depends on some_param.", ) """Parameter managed_param depends on some_param.""" diff --git a/tests/extensions/parameters/test_parameter_mixin_on_cache_change.py b/tests/extensions/parameters/test_parameter_mixin_on_cache_change.py index cdd6c7dd798..b5b1442660f 100644 --- a/tests/extensions/parameters/test_parameter_mixin_on_cache_change.py +++ b/tests/extensions/parameters/test_parameter_mixin_on_cache_change.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any import pytest @@ -241,18 +241,15 @@ def cb(**kwargs: Any) -> None: ) ) - mock_instr.test_parameter = cast( - "OnCacheChangeParameter", - mock_instr.add_parameter( - name="test_parameter", - parameter_class=OnCacheChangeParameter, - on_cache_change=cb, - set_cmd=lambda x: store.update({"value": x}), - get_cmd=lambda: None if store.get("value") is None else store.get("value"), - set_parser=lambda v: v * 2, - get_parser=lambda v: None if v is None else v / 2, - docstring="A parameter with set_parser.", - ), + mock_instr.test_parameter = mock_instr.add_parameter( + name="test_parameter", + parameter_class=OnCacheChangeParameter, + on_cache_change=cb, + set_cmd=lambda x: store.update({"value": x}), + get_cmd=lambda: None if store.get("value") is None else store.get("value"), + set_parser=lambda v: v * 2, + get_parser=lambda v: None if v is None else v / 2, + docstring="A parameter with set_parser.", ) assert mock_instr.test_parameter.get() is None From 46af2b5ca8c01f109dc1301def8c670686395de9 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 22 Oct 2026 21:10:42 +0200 Subject: [PATCH 66/69] Fix ty errors in dataset and Keysight 34465A tests Annotate the spectrum data as one dimensional so iterating it yields scalars, annotate the list of ParamSpecDicts so the dict literals are checked against the TypedDict, and mock the ask method of the Keysight 34465A with patch.object rather than assigning to the method. --- tests/dataset/conftest.py | 2 +- tests/dataset/test_paramspec.py | 2 +- tests/drivers/test_keysight_34465a.py | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/dataset/conftest.py b/tests/dataset/conftest.py index f6a65fc2693..d9f29000921 100644 --- a/tests/dataset/conftest.py +++ b/tests/dataset/conftest.py @@ -700,7 +700,7 @@ def __init__( self.stop = 2e6 self._rng = np.random.default_rng() - def get_data(self) -> NDArray[np.float64]: + def get_data(self) -> np.ndarray[tuple[int], np.dtype[np.float64]]: # This is how it should be: the setpoints are generated at the # time we get. But that will of course not work with the old Loop self.setpoints = (tuple(np.linspace(self.start, self.stop, self.npts)),) diff --git a/tests/dataset/test_paramspec.py b/tests/dataset/test_paramspec.py index 700e2718ff2..dac8292361f 100644 --- a/tests/dataset/test_paramspec.py +++ b/tests/dataset/test_paramspec.py @@ -55,7 +55,7 @@ class _ParamSpecKwargs(TypedDict): @pytest.fixture def version_0_dicts() -> "list[ParamSpecDict]": - sers = [] + sers: list[ParamSpecDict] = [] sers.append( { "name": "dmm_v1", diff --git a/tests/drivers/test_keysight_34465a.py b/tests/drivers/test_keysight_34465a.py index bcd5f175a6e..612c81a03e9 100644 --- a/tests/drivers/test_keysight_34465a.py +++ b/tests/drivers/test_keysight_34465a.py @@ -1,4 +1,5 @@ from typing import TYPE_CHECKING, Protocol +from unittest.mock import patch import numpy as np import pytest @@ -50,9 +51,11 @@ def ask_with_read_mock(cmd: str) -> str: return ask_with_read_mock - keysight_sim.ask = get_ask_with_read_mock(keysight_sim.ask, val_volt) try: - yield keysight_sim + with patch.object( + keysight_sim, "ask", new=get_ask_with_read_mock(keysight_sim.ask, val_volt) + ): + yield keysight_sim finally: Keysight34465A.close_all() From 987dc9e03d7a1ddea8568e76588c18bc2f336c14 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 22 Oct 2026 21:13:03 +0200 Subject: [PATCH 67/69] Accept int keys in SweepFixedValues.__getitem__ The values are stored in a list, so integer keys already worked at runtime. --- src/qcodes/parameters/sweep_values.py | 2 +- tests/test_sweep_values.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/qcodes/parameters/sweep_values.py b/src/qcodes/parameters/sweep_values.py index abea5f25e7c..236cee15f5b 100644 --- a/src/qcodes/parameters/sweep_values.py +++ b/src/qcodes/parameters/sweep_values.py @@ -372,7 +372,7 @@ def snapshot_base( def __iter__(self) -> Iterator[Any]: return iter(self._values) - def __getitem__(self, key: slice) -> Any: + def __getitem__(self, key: int | slice) -> Any: return self._values[key] def __len__(self) -> int: diff --git a/tests/test_sweep_values.py b/tests/test_sweep_values.py index de6b97c4498..e6ab07b854f 100644 --- a/tests/test_sweep_values.py +++ b/tests/test_sweep_values.py @@ -74,8 +74,7 @@ def test_valid(c0: Parameter) -> None: assert c0_sv.set == c0.set # normal sequence operations access values assert list(c0_sv) == [1] - # TODO: SweepFixedValues.__getitem__ only accepts slice but int keys work - assert c0_sv[0] == 1 # pyright: ignore[reportArgumentType] + assert c0_sv[0] == 1 assert 1 in c0_sv assert 2 not in c0_sv From 93820e66268c6dbc45d4733a5868eb3ea6e06407 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 28 Aug 2026 22:17:04 +0200 Subject: [PATCH 68/69] 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 e383a4e1d78..919cf97d20c 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -116,6 +116,10 @@ jobs: run: mypy -p qcodes if: ${{ !matrix.min-version && always() }} id: mypy + - name: Run ty + run: ty check --output-format github + if: ${{ !matrix.min-version && always() }} + id: ty - name: Set pytest basetemp if: ${{ runner.os == 'Windows' }} run: echo "PYTEST_BASETEMP=--basetemp=D:\\tmp" >> $GITHUB_ENV diff --git a/docs/changes/newsfragments/8441.underthehood.8 b/docs/changes/newsfragments/8441.underthehood.8 new file mode 100644 index 00000000000..0ed8752f56e --- /dev/null +++ b/docs/changes/newsfragments/8441.underthehood.8 @@ -0,0 +1,5 @@ +The type checker ``ty`` is now a test dependency and is run as part of the +CI job that also runs ``mypy`` and ``pyright``. Unlike the other type +checkers ``ty`` understands Jupyter notebooks, so this type checks the +example notebooks in ``docs`` in addition to the source code in ``src`` +and the test suite. ``ty`` 0.0.87 or newer is required. diff --git a/pyproject.toml b/pyproject.toml index 6b8536519c3..5144558c113 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,9 @@ test = [ "pytest-rerunfailures>=14.0", "pytest-xdist>=3.6.1", "PyVisa-sim>=0.6.0", + "scipy-stubs>=1.18.1.0", # type check docs examples using scipy "sphinx>=4.5.0", # sphinx extension tests + "ty>=0.0.87", "types-jsonschema>=4.16.0", "types-networkx >= 3.6.1.20260512", # minimum version required for generic data types in graph "types_requests>=0.1.8", diff --git a/requirements.txt b/requirements.txt index 90bbaf05276..e06ad8fb85d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -451,6 +451,8 @@ traitlets==5.16.1 # nbconvert # nbformat # nbsphinx +ty==0.0.87 + # via qcodes (pyproject.toml) types-jsonschema==4.26.0.20261006 # via qcodes (pyproject.toml) types-networkx==3.6.1.20261006 From dc239aeac6c8d8ce47de6f23d0d5a818cc66149b Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 19:42:57 +0200 Subject: [PATCH 69/69] Add working notes on ty and suppression comments A draft report for the ty issue that requires the wider TParameter default, and notes on how mypy, pyright and ty read each other's suppression comments, with the test cases used to establish that. They are kept in a single commit so that they are easy to drop before merging. --- suppression-codes-mypy-and-ty.md | 211 ++++++++++++++++++++++++++ ty-issue-2-typevar-default-context.md | 142 +++++++++++++++++ 2 files changed, 353 insertions(+) create mode 100644 suppression-codes-mypy-and-ty.md create mode 100644 ty-issue-2-typevar-default-context.md diff --git a/suppression-codes-mypy-and-ty.md b/suppression-codes-mypy-and-ty.md new file mode 100644 index 00000000000..8479b7d269c --- /dev/null +++ b/suppression-codes-mypy-and-ty.md @@ -0,0 +1,211 @@ +# Combining mypy and ty suppression codes + +## Summary + +The [ty suppression docs](https://docs.astral.sh/ty/suppression/) document putting +a ty rule into a mypy `type: ignore` comment by prefixing it with `ty:`: + +```python +sum_three_numbers("one", 5, 2) # type: ignore[arg-type, ty:invalid-argument-type] +``` + +ty honours this. **mypy does not ignore the `ty:` prefixed code**, and reports it +as an unused suppression when `warn_unused_ignores` is enabled, which qcodes +enables in `pyproject.toml`. So the combined form cannot be used here. + +qcodes therefore uses two comments on the same line: + +```python +f("one") # type: ignore[arg-type] # ty: ignore[invalid-argument-type] +``` + +That is the only form of the three below that all three checkers accept. + +## Results + +| form | ty 0.0.74 | mypy 2.3.1 with `warn_unused_ignores` | mypy 2.3.1 without it | pyright 1.1.411 | +| --- | --- | --- | --- | --- | +| `# type: ignore[arg-type, ty:invalid-argument-type]` | suppressed | `Unused "type: ignore[ty:invalid-argument-type]" comment` | clean | suppressed | +| `# type: ignore[arg-type]` + `# ty: ignore[invalid-argument-type]` | suppressed | clean | clean | suppressed | +| `# type: ignore[ty:invalid-argument-type]` | suppressed | unused, and `arg-type` not covered | `arg-type` not covered | suppressed | + +Note that the `arg-type` half of the combined form *is* honoured by mypy. It is +only the `ty:` prefixed code that mypy does not recognise, and therefore reports +as unused. + +## How pyright fits in + +pyright has its own suppression comment and also honours mypy's, which is why it +accepts all three forms above. + +| comment | pyright | +| --- | --- | +| `# type: ignore` | suppressed | +| `# type: ignore[arg-type]` | suppressed | +| `# type: ignore[arg-type, ty:invalid-argument-type]` | suppressed | +| `# pyright: ignore` | suppressed | +| `# pyright: ignore[reportArgumentType]` | suppressed | +| `# pyright: ignore[reportGeneralTypeIssues]` | **not** suppressed, wrong rule | +| `# ty: ignore[invalid-argument-type]` | **not** suppressed | + +Two things follow from this. + +**`# type: ignore` is a blanket suppression for pyright.** pyright does not parse +the codes in it, so `# type: ignore[arg-type]` silences *every* pyright rule on +that line, not just the argument type one. A consequence that came up repeatedly +during the ty migration: removing a mypy suppression can surface a pyright error +on the same line that was never visible before. `# pyright: ignore[rule]` is the +precise form, and unlike `# type: ignore` it only suppresses the rules listed. + +**A ty only suppression does not silence pyright.** `# ty: ignore[...]` is just a +comment as far as pyright is concerned. That is what makes the two comment form +safe: the mypy half keeps pyright quiet as a side effect, and the ty half is +inert for both of the others. + +## Unused suppression detection + +The three checkers differ in whether they tell you a suppression has gone stale. + +| checker | setting | default | reports unused | +| --- | --- | --- | --- | +| mypy | `warn_unused_ignores` | off | enabled in `pyproject.toml` | +| ty | `unused-ignore-comment` | on | yes, for `ty: ignore` directives | +| pyright | `reportUnnecessaryTypeIgnoreComment` | off | not enabled, see below | + +With the pyright setting enabled it reports all of these: + +```python +def g(a: int) -> None: ... + + +g(1) # type: ignore +g(1) # pyright: ignore +g(1) # pyright: ignore[reportArgumentType] +``` + +``` +Unnecessary "# type: ignore" comment +Unnecessary "# type: ignore" comment +Unnecessary "# pyright: ignore" rule: "reportArgumentType" +``` + +**We cannot enable it while we also run mypy.** Because pyright treats +`# type: ignore` as a blanket suppression of *its own* rules, it calls the +comment unnecessary whenever pyright itself has nothing to report on the line, +with no knowledge of whether mypy needed it. Every mypy only suppression in the +code base would be reported as unnecessary. For example: + +```python +from typing import Any + + +class A: + def m(self) -> None: ... + + +def make(a: A, replacement: Any) -> None: + # mypy reports method-assign here, pyright has no equivalent check + a.m = replacement # type: ignore[method-assign] +``` + +mypy needs that suppression: removing it gives +`error: Cannot assign to a method [method-assign]`. pyright with +`reportUnnecessaryTypeIgnoreComment` enabled reports the very same line as +`Unnecessary "# type: ignore" comment`. + +So mypy's `warn_unused_ignores` and ty's `unused-ignore-comment` are the two +stale suppression checks we can actually rely on. + +## Test case + +```python +def f(a: int) -> None: ... + + +# 1. combined form from the ty docs +f("one") # type: ignore[arg-type, ty:invalid-argument-type] + +# 2. the two comment form used in qcodes +f("one") # type: ignore[arg-type] # ty: ignore[invalid-argument-type] + +# 3. combined form, ty rule only +f("one") # type: ignore[ty:invalid-argument-type] + +# 4. control, expected to be reported by every checker +f("one") +``` + +And for the pyright specific forms: + +```python +def h(a: int) -> None: ... + + +# 5. pyright: ignore, blanket +h("one") # pyright: ignore + +# 6. pyright: ignore with the matching rule +h("one") # pyright: ignore[reportArgumentType] + +# 7. pyright: ignore with a non matching rule +h("one") # pyright: ignore[reportGeneralTypeIssues] + +# 8. ty: ignore only +h("one") # ty: ignore[invalid-argument-type] +``` + +Run from the repository root so that the mypy configuration in `pyproject.toml` +is picked up: + +``` +uv run ty check --output-format concise +uv run --extra test mypy +uv run --extra test mypy --no-warn-unused-ignores +uv run pyright +``` + +Expected results: + +| block | ty | mypy | pyright | +| --- | --- | --- | --- | +| first, cases 1 to 4 | 4 | 1, 3, 4 and two unused directives | 4 | +| second, cases 5 to 8 | 5, 6, 7 | 5, 6, 7, 8 | 7, 8 | + +The second block deliberately exercises comments that only one checker +understands, so most cases are reported by the other two. That is the point: it +shows that `pyright: ignore` is inert for mypy and ty, and that `ty: ignore` is +inert for mypy and pyright. + +Any deviation from this table tells you that one of the checkers has changed how +it reads these comments. + +## Why we keep `warn_unused_ignores` + +Dropping `warn_unused_ignores` would make the combined form work, but that +setting is worth more than the shorter comments. As shown above it is, together +with ty's `unused-ignore-comment`, one of only two stale suppression checks +available to us. During the ty migration it caught: + +- the `issuperset` suppression becoming redundant once + [astral-sh/ty#4303](https://github.com/astral-sh/ty/issues/4303) was fixed in + ty 0.0.74 +- the two suppressions in the Keithley 7510 buffer becoming unnecessary once the + data dictionary was annotated +- several suppressions in `ParameterBase` becoming unnecessary once the duck + typed conversions were moved behind helpers + +## Suggested upstream change + +mypy could ignore codes carrying a `:` prefix in `type: ignore` comments, +rather than treating them as mypy codes that turned out to be unused. That would +make the form documented by ty usable in projects that run both checkers with +`warn_unused_ignores` enabled, and would generalise to any other checker that +wants to share the comment. + +Failing that, the ty documentation could note that the combined form conflicts +with mypy's `warn_unused_ignores`, and suggest the two comment form for projects +that run both. + +## Versions + +Measured with ty 0.0.74, mypy 2.3.1 and pyright 1.1.411. diff --git a/ty-issue-2-typevar-default-context.md b/ty-issue-2-typevar-default-context.md new file mode 100644 index 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