From fa01e2449681db7440280ce44a2cde2bd4d5b488 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 19 Aug 2026 07:18:07 +0200 Subject: [PATCH 01/76] Fix isinstance validator logic order --- src/qcodes/validators/validators.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/qcodes/validators/validators.py b/src/qcodes/validators/validators.py index bcda83550e1..24d5f38be7b 100644 --- a/src/qcodes/validators/validators.py +++ b/src/qcodes/validators/validators.py @@ -987,12 +987,12 @@ def shape_unevaluated(self) -> shape_tuple_type: def shape(self) -> tuple[int, ...] | None: if self._shape is None: return None - shape_array = [] + shape_array: list[int] = [] for s in self._shape: - if callable(s): - shape_array.append(s()) - else: + if isinstance(s, int): shape_array.append(s) + else: + shape_array.append(s()) shape = tuple(shape_array) return shape From 423870bbb159deb469559cc11f6ef56edcca09d6 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 19 Aug 2026 07:53:46 +0200 Subject: [PATCH 02/76] 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 0ff7239f30e..983f6bfa7ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -293,6 +293,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 8b9173e7f89b64b9898e96589632c298cbc2c4cd Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 19 Aug 2026 20:04:20 +0200 Subject: [PATCH 03/76] 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. --- docs/changes/newsfragments/8360.underthehood | 9 ++++ src/qcodes/parameters/parameter.py | 55 ++++++++++++++++---- 2 files changed, 53 insertions(+), 11 deletions(-) create mode 100644 docs/changes/newsfragments/8360.underthehood diff --git a/docs/changes/newsfragments/8360.underthehood b/docs/changes/newsfragments/8360.underthehood new file mode 100644 index 00000000000..b7ee1dd573a --- /dev/null +++ b/docs/changes/newsfragments/8360.underthehood @@ -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 47626681be8..dbed818e008 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 53fbc6f9ce049443a39261dbfb3a6e998818e6d2 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 19 Aug 2026 20:15:46 +0200 Subject: [PATCH 04/76] 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/8361.underthehood | 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/8361.underthehood diff --git a/docs/changes/newsfragments/8361.underthehood b/docs/changes/newsfragments/8361.underthehood new file mode 100644 index 00000000000..674cd1067ae --- /dev/null +++ b/docs/changes/newsfragments/8361.underthehood @@ -0,0 +1,9 @@ +The ``TParameter`` type variable used by :meth:`.InstrumentBase.add_parameter` +now defaults to ``Parameter[Any, Any]`` rather than to a bare ``Parameter``. +When ``add_parameter`` is called without an explicit ``parameter_class`` the +returned parameter is bound to the instrument it is added to, so the previous +default (which expands to ``Parameter[Any, InstrumentBase | None]``) wrongly +claimed that the instrument was ``InstrumentBase | None``. This made the result +unassignable to the ``Parameter[SomeType, Self]`` annotations that drivers use. +Code that relies on the inferred type of an unannotated +``instrument.add_parameter("name")`` will now see ``Parameter[Any, Any]``. diff --git a/src/qcodes/instrument/instrument_base.py b/src/qcodes/instrument/instrument_base.py index 733bfa0ecb6..b94e33a0bfa 100644 --- a/src/qcodes/instrument/instrument_base.py +++ b/src/qcodes/instrument/instrument_base.py @@ -32,7 +32,13 @@ log = logging.getLogger(__name__) # Cannot convert to PEP 695: uses default= which requires PEP 696 (Python 3.13+). -TParameter = TypeVar("TParameter", bound="ParameterBase", default="Parameter") +# The default is `Parameter[Any, Any]` rather than a bare `Parameter`: when +# `add_parameter` is called without a `parameter_class` the returned parameter is +# bound to `self`, so spelling the default as `Parameter` (which expands to +# `Parameter[Any, InstrumentBase | None]`) would wrongly claim that the +# instrument is `InstrumentBase | None` and make the result unassignable to the +# `Parameter[SomeType, Self]` annotations that drivers use. +TParameter = TypeVar("TParameter", bound="ParameterBase", default="Parameter[Any, Any]") TSubmodule = TypeVar( "TSubmodule", bound="InstrumentModule | ChannelTuple", default="InstrumentModule" ) diff --git a/tests/test_instrument.py b/tests/test_instrument.py index b2ba6dec987..3b77fed4569 100644 --- a/tests/test_instrument.py +++ b/tests/test_instrument.py @@ -212,8 +212,10 @@ def test_attr_access(testdummy: DummyInstrument) -> None: def test_parameter_property(testdummy: DummyInstrument) -> None: # since this is added dynamically we cannot know the type statically assert_type(testdummy.dac1, Any) - # this is an assigned attribute so we know it statically - assert_type(testdummy.fixed_parameter, Parameter) + # this is an assigned attribute so we know it statically. Without an + # explicit ``parameter_class`` the data and instrument types of the + # returned parameter are unknown, hence ``Parameter[Any, Any]``. + assert_type(testdummy.fixed_parameter, Parameter[Any, Any]) assert testdummy.fixed_parameter.get() == 5 testdummy.fixed_parameter.set(10) From 2459a45f6b23f35c8553d1aea7cb50f221ea5a99 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 19 Aug 2026 21:39:21 +0200 Subject: [PATCH 05/76] Reject legacy setpoint arrays without an array_id store_array_to_database asserted that the measured array has an array_id, but passed the array_id of its setpoint arrays straight to add_result without checking them. Those are different arrays, so a legacy dataset with an unnamed setpoint array failed deep inside the data saver. Raise a clear ValueError instead. Hoist the setpoint arrays and their ids out of the loops rather than re-indexing set_arrays on every iteration, and drop a pyright suppression that the qcodes_loop annotations make unnecessary. --- docs/changes/newsfragments/8362.underthehood | 5 ++ src/qcodes/dataset/legacy_import.py | 60 ++++++++++++++------ 2 files changed, 47 insertions(+), 18 deletions(-) create mode 100644 docs/changes/newsfragments/8362.underthehood diff --git a/docs/changes/newsfragments/8362.underthehood b/docs/changes/newsfragments/8362.underthehood new file mode 100644 index 00000000000..b1e9b3fd772 --- /dev/null +++ b/docs/changes/newsfragments/8362.underthehood @@ -0,0 +1,5 @@ +The legacy dataset importer now raises a clear :class:`ValueError` when a +setpoint array has no ``array_id``, instead of passing ``None`` on to +``add_result`` where a parameter name is expected. This was found by annotating +``DataArray`` in ``qcodes_loop``, which also removes the need for a ``pyright`` +suppression on the array shape. diff --git a/src/qcodes/dataset/legacy_import.py b/src/qcodes/dataset/legacy_import.py index babd3955209..22cd030ce43 100644 --- a/src/qcodes/dataset/legacy_import.py +++ b/src/qcodes/dataset/legacy_import.py @@ -44,23 +44,45 @@ def setup_measurement( return meas +def _array_id(array: DataArray) -> str: + """ + Return the ``array_id`` of a legacy ``DataArray``. + + Args: + array: Legacy data array to read the id from. + + Raises: + ValueError: If the array has no ``array_id``. Parameters are registered + by name, so an array without an id cannot be stored. + + """ + array_id = array.array_id + if array_id is None: + raise ValueError(f"Cannot store an array without an array_id: {array!r}") + return array_id + + def store_array_to_database(datasaver: DataSaver, array: DataArray) -> int: assert array.shape is not None dims = len(array.shape) assert array.array_id is not None if dims == 2: - for index1, i in enumerate(array.set_arrays[0]): - for index2, j in enumerate(array.set_arrays[1][index1]): + setpoints_outer = array.set_arrays[0] + setpoints_inner = array.set_arrays[1] + outer_id = _array_id(setpoints_outer) + inner_id = _array_id(setpoints_inner) + for index1, i in enumerate(setpoints_outer): + for index2, j in enumerate(setpoints_inner[index1]): datasaver.add_result( - (array.set_arrays[0].array_id, i), - (array.set_arrays[1].array_id, j), + (outer_id, i), + (inner_id, j), (array.array_id, array[index1, index2]), ) elif dims == 1: - for index, i in enumerate(array.set_arrays[0]): - datasaver.add_result( - (array.set_arrays[0].array_id, i), (array.array_id, array[index]) - ) + setpoints = array.set_arrays[0] + setpoints_id = _array_id(setpoints) + for index, i in enumerate(setpoints): + datasaver.add_result((setpoints_id, i), (array.array_id, array[index])) else: raise NotImplementedError( "The exporter only currently handles 1 and 2 Dimensional data" @@ -73,23 +95,25 @@ def store_array_to_database_alt(meas: Measurement, array: DataArray) -> int: dims = len(array.shape) assert array.array_id is not None if dims == 2: - outer_data = np.empty( - array.shape[1] # pyright: ignore[reportGeneralTypeIssues] - ) + setpoints_outer = array.set_arrays[0] + setpoints_inner = array.set_arrays[1] + outer_id = _array_id(setpoints_outer) + inner_id = _array_id(setpoints_inner) + outer_data = np.empty(array.shape[1]) with meas.run() as datasaver: - for index1, i in enumerate(array.set_arrays[0]): + for index1, i in enumerate(setpoints_outer): outer_data[:] = i datasaver.add_result( - (array.set_arrays[0].array_id, outer_data), - (array.set_arrays[1].array_id, array.set_arrays[1][index1, :]), + (outer_id, outer_data), + (inner_id, setpoints_inner[index1, :]), (array.array_id, array[index1, :]), ) elif dims == 1: + setpoints = array.set_arrays[0] + setpoints_id = _array_id(setpoints) with meas.run() as datasaver: - for index, i in enumerate(array.set_arrays[0]): - datasaver.add_result( - (array.set_arrays[0].array_id, i), (array.array_id, array[index]) - ) + for index, i in enumerate(setpoints): + datasaver.add_result((setpoints_id, i), (array.array_id, array[index])) else: raise NotImplementedError( "The exporter only currently handles 1 and 2 Dimensional data" From 11fabb8c5513bdc181b86400cd60d8554f5e2ec4 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 20 Aug 2026 06:59:20 +0200 Subject: [PATCH 06/76] Replace the CombinedParameter lambda hack with a dataclass self.parameter was a lambda with name, full_name, label and unit attached to it. A small dataclass expresses that directly and drops seven type checker suppressions. It is still marked as a hack: CombinedParameter does not inherit from Parameter or ParameterBase, so it has to fake the parts of their api that it is expected to provide. The object stays callable, returning None as the lambda did, in case external code relies on it. The units deprecation warning now runs before the object is built, which is safe because the class has no custom __repr__. --- docs/changes/newsfragments/8364.underthehood | 4 ++ src/qcodes/parameters/combined_parameter.py | 56 ++++++++++++++------ 2 files changed, 44 insertions(+), 16 deletions(-) create mode 100644 docs/changes/newsfragments/8364.underthehood diff --git a/docs/changes/newsfragments/8364.underthehood b/docs/changes/newsfragments/8364.underthehood new file mode 100644 index 00000000000..168eaff90ec --- /dev/null +++ b/docs/changes/newsfragments/8364.underthehood @@ -0,0 +1,4 @@ +``CombinedParameter.parameter`` is now a small dataclass holding the ``name``, +``full_name``, ``label`` and ``unit`` of the combined parameter, replacing a +lambda that had those attributes attached to it. It remains callable, and +calling it returns ``None`` as before. diff --git a/src/qcodes/parameters/combined_parameter.py b/src/qcodes/parameters/combined_parameter.py index 4a6adab034b..9c6ead6b292 100644 --- a/src/qcodes/parameters/combined_parameter.py +++ b/src/qcodes/parameters/combined_parameter.py @@ -3,6 +3,7 @@ import collections import logging from copy import copy +from dataclasses import dataclass from typing import TYPE_CHECKING, Any import numpy as np @@ -21,6 +22,30 @@ _LOG = logging.getLogger(__name__) +@dataclass +class _CombinedParameterInfo: + """ + The subset of the ``Parameter`` api that :class:`CombinedParameter` fakes. + + This exists because :class:`CombinedParameter` does not inherit from + :class:`.Parameter` or :class:`.ParameterBase`, yet is expected to carry the + identifying metadata of one so that it can be snapshotted like one. + """ + + name: str + full_name: str + label: str | None + unit: str | None + + def __call__(self) -> None: + """ + Do nothing. + + This used to be a lambda, so external code may be calling it. Calling it + has always returned ``None``. + """ + + def combine( *parameters: Parameter, name: str, @@ -77,10 +102,6 @@ def __init__( aggregator: Callable[..., Any] | None = None, ) -> None: super().__init__() - # TODO(giulioungaretti)temporary hack - # starthack - # this is a dummy parameter - # that mimicks the api that a normal parameter has if not name.isidentifier(): raise ValueError( f"Parameter name must be a valid identifier " @@ -89,13 +110,6 @@ def __init__( f"must not contain spaces or special characters" ) - self.parameter = lambda: None - # mypy will complain that a callable does not have these attributes - # but you can still create them here. - self.parameter.full_name = name # type: ignore[attr-defined] - self.parameter.name = name # type: ignore[attr-defined] - self.parameter.label = label # type: ignore[attr-defined] - if units is not None: _LOG.warning( f"`units` is deprecated for the " @@ -103,9 +117,19 @@ def __init__( ) if unit is None: unit = units - self.parameter.unit = unit # type: ignore[attr-defined] - self.setpoints: list[Any] = [] + + # TODO(giulioungaretti)temporary hack + # starthack + # this is a dummy parameter + # that mimicks the api that a normal parameter has. + # CombinedParameter does not inherit from Parameter or ParameterBase, + # so it has to fake the parts of their api that it is expected to + # provide. + self.parameter = _CombinedParameterInfo( + name=name, full_name=name, label=label, unit=unit + ) # endhack + self.setpoints: list[Any] = [] self.parameters = parameters self.sets = [parameter.set for parameter in self.parameters] self.dimensionality = len(self.sets) @@ -215,9 +239,9 @@ def snapshot_base( meta_data: dict[str, Any] = collections.OrderedDict() meta_data["__class__"] = full_class(self) param = self.parameter - meta_data["unit"] = param.unit # type: ignore[attr-defined] - meta_data["label"] = param.label # type: ignore[attr-defined] - meta_data["full_name"] = param.full_name # type: ignore[attr-defined] + meta_data["unit"] = param.unit + meta_data["label"] = param.label + meta_data["full_name"] = param.full_name meta_data["aggregator"] = repr(getattr(self, "f", None)) update = normalize_snapshot_update(update) for parameter in self.parameters: From 85daf4b4f5085c5833ee809622de5ac6c4069615 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 20 Aug 2026 07:24:12 +0200 Subject: [PATCH 07/76] Suppress ty false positive on forwarded parameter kwargs ty does not recognise a TypedDict that is generic over more than one type variable as a mapping when one of those type variables has a PEP 696 default, so it rejects re-expanding the kwargs with **. A TypedDict generic over a single such type variable is accepted, so this is a bug rather than something the code should work around. Suppress it at the five subclasses that forward their kwargs on, and document the reason once on ParameterBaseKWArgs. --- docs/changes/newsfragments/8365.underthehood | 5 +++++ src/qcodes/parameters/array_parameter.py | 3 ++- src/qcodes/parameters/delegate_parameter.py | 3 ++- src/qcodes/parameters/multi_parameter.py | 3 ++- src/qcodes/parameters/parameter.py | 3 ++- src/qcodes/parameters/parameter_base.py | 6 ++++++ src/qcodes/parameters/parameter_with_setpoints.py | 3 ++- 7 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 docs/changes/newsfragments/8365.underthehood diff --git a/docs/changes/newsfragments/8365.underthehood b/docs/changes/newsfragments/8365.underthehood new file mode 100644 index 00000000000..45e18b09569 --- /dev/null +++ b/docs/changes/newsfragments/8365.underthehood @@ -0,0 +1,5 @@ +Subclasses of ``ParameterBase`` that forward ``**kwargs`` on to their super +class now carry a ``ty: ignore[invalid-argument-type]``. ty does not recognise a +TypedDict that is generic over more than one type variable as a mapping when one +of those type variables has a PEP 696 default, so it rejects re-expanding the +kwargs with ``**``. The reason is documented on ``ParameterBaseKWArgs``. diff --git a/src/qcodes/parameters/array_parameter.py b/src/qcodes/parameters/array_parameter.py index d3565f83f02..5fb1df55f58 100644 --- a/src/qcodes/parameters/array_parameter.py +++ b/src/qcodes/parameters/array_parameter.py @@ -142,7 +142,8 @@ def __init__( kwargs.setdefault("snapshot_value", False) super().__init__( name, - **kwargs, + # see the note on ParameterBaseKWArgs + **kwargs, # ty: ignore[invalid-argument-type] ) if self.settable: diff --git a/src/qcodes/parameters/delegate_parameter.py b/src/qcodes/parameters/delegate_parameter.py index b948b8c568d..b63736f1928 100644 --- a/src/qcodes/parameters/delegate_parameter.py +++ b/src/qcodes/parameters/delegate_parameter.py @@ -210,7 +210,8 @@ def __init__( initial_cache_value = kwargs.pop("initial_cache_value", None) self.source = source - super().__init__(name, **kwargs) + # see the note on ParameterBaseKWArgs + super().__init__(name, **kwargs) # ty: ignore[invalid-argument-type] self.label = kwargs.get("label", None) self.unit = kwargs.get("unit", None) diff --git a/src/qcodes/parameters/multi_parameter.py b/src/qcodes/parameters/multi_parameter.py index 0f230b69334..80385ff6304 100644 --- a/src/qcodes/parameters/multi_parameter.py +++ b/src/qcodes/parameters/multi_parameter.py @@ -153,7 +153,8 @@ def __init__( kwargs.setdefault("snapshot_value", False) super().__init__( name, - **kwargs, + # see the note on ParameterBaseKWArgs + **kwargs, # ty: ignore[invalid-argument-type] ) self._meta_attrs.extend( diff --git a/src/qcodes/parameters/parameter.py b/src/qcodes/parameters/parameter.py index dbed818e008..567df9a8b1d 100644 --- a/src/qcodes/parameters/parameter.py +++ b/src/qcodes/parameters/parameter.py @@ -362,7 +362,8 @@ def _set_manual_parameter( super().__init__( name=name, - **kwargs, + # see the note on ParameterBaseKWArgs + **kwargs, # ty: ignore[invalid-argument-type] ) no_instrument_get = not self._implements_get_raw and ( diff --git a/src/qcodes/parameters/parameter_base.py b/src/qcodes/parameters/parameter_base.py index 16d96c8b08e..78e1183bc02 100644 --- a/src/qcodes/parameters/parameter_base.py +++ b/src/qcodes/parameters/parameter_base.py @@ -148,6 +148,12 @@ class ParameterBaseKWArgs( ``**kwargs: Unpack[ParameterBaseKWArgs]`` as input and forward this to the super class to ensure that it can accept all the arguments defined here. + + Note that forwarding the kwargs on requires a + ``ty: ignore[invalid-argument-type]``. ty does not recognise a TypedDict + that is generic over more than one type variable as a mapping when one of + those type variables has a PEP 696 default, so it rejects re-expanding the + kwargs with ``**``. """ instrument: NotRequired[InstrumentTypeVar_co] diff --git a/src/qcodes/parameters/parameter_with_setpoints.py b/src/qcodes/parameters/parameter_with_setpoints.py index 9653be69281..812b36f5a6c 100644 --- a/src/qcodes/parameters/parameter_with_setpoints.py +++ b/src/qcodes/parameters/parameter_with_setpoints.py @@ -74,7 +74,8 @@ def __init__( super().__init__( name=name, - **kwargs, + # see the note on ParameterBaseKWArgs + **kwargs, # ty: ignore[invalid-argument-type] ) if setpoints is None: self.setpoints = [] From fa5875a4a37f4faed82fb58507862cc4bbf83668 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 20 Aug 2026 08:47:55 +0200 Subject: [PATCH 08/76] Correct the description of the ty kwargs false positive Further reduction showed the trigger is not a TypedDict being generic over more than one type variable. One type parameter with any non-Any PEP 696 default is enough, and the problem is not specific to ** expansion: ty computes the upper bound of the synthesized Self as the default specialization, so every other specialization is rejected by the members that bind Self. --- docs/changes/newsfragments/8365.underthehood | 9 +++++---- src/qcodes/parameters/parameter_base.py | 12 ++++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/docs/changes/newsfragments/8365.underthehood b/docs/changes/newsfragments/8365.underthehood index 45e18b09569..492f371ee71 100644 --- a/docs/changes/newsfragments/8365.underthehood +++ b/docs/changes/newsfragments/8365.underthehood @@ -1,5 +1,6 @@ Subclasses of ``ParameterBase`` that forward ``**kwargs`` on to their super -class now carry a ``ty: ignore[invalid-argument-type]``. ty does not recognise a -TypedDict that is generic over more than one type variable as a mapping when one -of those type variables has a PEP 696 default, so it rejects re-expanding the -kwargs with ``**``. The reason is documented on ``ParameterBaseKWArgs``. +class now carry a ``ty: ignore[invalid-argument-type]``. When a generic TypedDict +declares a PEP 696 default for a type parameter, ty computes the upper bound of +the synthesized ``Self`` as the default specialization, so every other +specialization is rejected by the members that bind ``Self``, including expanding +with ``**``. The reason is documented on ``ParameterBaseKWArgs``. diff --git a/src/qcodes/parameters/parameter_base.py b/src/qcodes/parameters/parameter_base.py index 78e1183bc02..a0f19151dfe 100644 --- a/src/qcodes/parameters/parameter_base.py +++ b/src/qcodes/parameters/parameter_base.py @@ -150,10 +150,14 @@ class ParameterBaseKWArgs( defined here. Note that forwarding the kwargs on requires a - ``ty: ignore[invalid-argument-type]``. ty does not recognise a TypedDict - that is generic over more than one type variable as a mapping when one of - those type variables has a PEP 696 default, so it rejects re-expanding the - kwargs with ``**``. + ``ty: ignore[invalid-argument-type]``. When a generic TypedDict declares a + PEP 696 default for a type parameter, ty computes the upper bound of the + synthesized ``Self`` as the default specialization, so every other + specialization is rejected by the members that bind ``Self``. Expanding + with ``**`` is one of those, and reports the rather misleading + ``must be a mapping type``. ``InstrumentTypeVar_co`` defaults to + ``InstrumentBase | None`` and so triggers this; ``ParameterDataTypeVar`` + defaults to ``Any``, which happens to be the one default ty accepts. """ instrument: NotRequired[InstrumentTypeVar_co] From 87a28e459f92c6be93ae691b36d5b01ff9bfffb6 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:05:16 +0200 Subject: [PATCH 09/76] Use cast for the narrowing in the Infiniium driver The driver narrowed root_instrument and instrument to the concrete driver classes with an annotation plus a suppression, and worked around pyvisa typing the return of read_binary_values and query_binary_values as Sequence[float] regardless of the requested container. Spell both as cast instead, which all three type checkers accept and which drops five suppressions. --- docs/changes/newsfragments/8366.underthehood | 5 ++ .../instrument_drivers/Keysight/Infiniium.py | 46 +++++++++++-------- 2 files changed, 32 insertions(+), 19 deletions(-) create mode 100644 docs/changes/newsfragments/8366.underthehood diff --git a/docs/changes/newsfragments/8366.underthehood b/docs/changes/newsfragments/8366.underthehood new file mode 100644 index 00000000000..21ba0d20655 --- /dev/null +++ b/docs/changes/newsfragments/8366.underthehood @@ -0,0 +1,5 @@ +The Infiniium driver now uses ``cast`` where it narrows ``root_instrument`` and +``instrument`` to the concrete driver classes, and where ``pyvisa`` types the +return of ``read_binary_values``/``query_binary_values`` as a ``Sequence[float]`` +regardless of the requested ``container``. This replaces five type checker +suppressions and has no effect at runtime. diff --git a/src/qcodes/instrument_drivers/Keysight/Infiniium.py b/src/qcodes/instrument_drivers/Keysight/Infiniium.py index 8fdb6ee41ad..80ce22637bd 100644 --- a/src/qcodes/instrument_drivers/Keysight/Infiniium.py +++ b/src/qcodes/instrument_drivers/Keysight/Infiniium.py @@ -3,7 +3,7 @@ from io import BytesIO from os.path import splitext from pathlib import Path -from typing import TYPE_CHECKING, Any, ClassVar, Literal +from typing import TYPE_CHECKING, Any, ClassVar, Literal, cast import numpy as np import numpy.typing as npt @@ -134,8 +134,7 @@ def setpoints(self) -> "Sequence[ParameterBase]": """ instrument = self.instrument if isinstance(instrument, KeysightInfiniiumChannel): - root_instrument: KeysightInfiniium - root_instrument = self.root_instrument # type: ignore[assignment] + root_instrument = cast("KeysightInfiniium", self.root_instrument) cache_setpoints = root_instrument.cache_setpoints() if not cache_setpoints: self.update_setpoints() @@ -201,7 +200,8 @@ def update_fft_setpoints(self) -> None: """ Update waveform parameters for an FFT. """ - instrument: KeysightInfiniiumFunction = self.instrument # type: ignore[assignment] + # only reached for a function parameter, see the caller in ``setpoints`` + instrument = cast("KeysightInfiniiumFunction", self.instrument) instrument.write(f":WAV:SOUR {self._channel}") preamble = instrument.ask(":WAV:PRE?").strip().split(",") self.update_setpoints(preamble) @@ -215,7 +215,7 @@ def get_raw(self) -> npt.NDArray: """ if self.instrument is None: raise RuntimeError("Cannot get data without instrument") - root_instr: KeysightInfiniium = self.root_instrument # type: ignore[assignment] + root_instr = cast("KeysightInfiniium", self.root_instrument) # Check if we can use cached trace parameters if not root_instr.cache_setpoints(): self.update_setpoints() @@ -234,13 +234,16 @@ def get_raw(self) -> npt.NDArray: root_instr.write(":WAV:DATA?") # Ignore first two bytes, which should be "#0" _ = root_instr.visa_handle.read_bytes(2) - data: npt.NDArray - data = root_instr.visa_handle.read_binary_values( # type: ignore[assignment] - "h", - container=np.ndarray, - header_fmt="empty", - expect_termination=True, - data_points=self._points, + # pyvisa types the return as a Sequence[float] regardless of ``container`` + data = cast( + "npt.NDArray", + root_instr.visa_handle.read_binary_values( + "h", + container=np.ndarray, + header_fmt="empty", + expect_termination=True, + data_points=self._points, + ), ) data = data.astype(np.float64) data = (data * self._yincrement) + self._yoffset @@ -1275,15 +1278,20 @@ def screenshot( ) try: with open(img_path, "wb") as f: - screen_bytes = self.visa_handle.query_binary_values( - f":DISPlay:DATA? {img_type.upper()[1:]}", # without . - # https://docs.python.org/3/library/struct.html#format-characters - datatype="B", # Capitcal B for unsigned byte - container=bytes, + # pyvisa types the return as a Sequence[float] regardless of + # ``container`` + screen_bytes = cast( + "bytes", + self.visa_handle.query_binary_values( + f":DISPlay:DATA? {img_type.upper()[1:]}", # without . + # https://docs.python.org/3/library/struct.html#format-characters + datatype="B", # Capitcal B for unsigned byte + container=bytes, + ), ) - f.write(screen_bytes) # type: ignore[arg-type] + f.write(screen_bytes) print(f"Screen image written to {img_path}") - return np.asarray(pil_open(BytesIO(screen_bytes))) # type: ignore[arg-type] + return np.asarray(pil_open(BytesIO(screen_bytes))) except Exception as e: self.log.error(f"Failed to save screenshot, Error occurred: \n{e}") return None From 9fe4894ae24ac938fc6d0027ea04a1ebcfdc20ce Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:17:24 +0200 Subject: [PATCH 10/76] 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. --- docs/changes/newsfragments/8367.underthehood | 5 +++++ src/qcodes/dataset/data_set.py | 21 +++++++------------- 2 files changed, 12 insertions(+), 14 deletions(-) create mode 100644 docs/changes/newsfragments/8367.underthehood diff --git a/docs/changes/newsfragments/8367.underthehood b/docs/changes/newsfragments/8367.underthehood new file mode 100644 index 00000000000..81eef08c23d --- /dev/null +++ b/docs/changes/newsfragments/8367.underthehood @@ -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 f36ad008772..b8f53a1b9c3 100644 --- a/src/qcodes/dataset/data_set.py +++ b/src/qcodes/dataset/data_set.py @@ -1406,28 +1406,21 @@ def _finalize_res_dict_standalones( for param, value in result_dict.items(): if param.type == "text": if value.shape: - new_res: list[dict[str, VALUE]] = [ - {param.name: str(val)} for val in value - ] - res_list += new_res + res_list.extend({param.name: str(val)} for val in value) else: - new_res = [{param.name: str(value)}] - res_list += new_res + res_list.append({param.name: str(value)}) elif param.type == "numeric": if value.shape: - res_list += [{param.name: number} for number in value] + res_list.extend({param.name: number} for number in value) else: - new_res = [{param.name: float(value)}] - res_list += new_res + res_list.append({param.name: float(value)}) elif param.type == "complex": if value.shape: - res_list += [{param.name: number} for number in value] + res_list.extend({param.name: number} for number in value) else: - new_res = [{param.name: complex(value)}] - res_list += new_res + res_list.append({param.name: complex(value)}) else: - new_res = [{param.name: value}] - res_list += new_res + res_list.append({param.name: value}) return res_list From bae915b10645012f8f37de4d1559ba9a0a5cabdf Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:17:30 +0200 Subject: [PATCH 11/76] 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/8368.underthehood | 4 ++++ .../instrument_drivers/AlazarTech/dll_wrapper.py | 10 +++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 docs/changes/newsfragments/8368.underthehood diff --git a/docs/changes/newsfragments/8368.underthehood b/docs/changes/newsfragments/8368.underthehood new file mode 100644 index 00000000000..7cab22c1c78 --- /dev/null +++ b/docs/changes/newsfragments/8368.underthehood @@ -0,0 +1,4 @@ +The Alazar DLL wrapper no longer assumes that the callable handed to a ctypes +``errcheck`` has a ``__name__``, which a plain ``Callable`` does not guarantee. +The error message falls back to the repr of the callable instead. This only +affects the text of an error that should not occur in practice. diff --git a/src/qcodes/instrument_drivers/AlazarTech/dll_wrapper.py b/src/qcodes/instrument_drivers/AlazarTech/dll_wrapper.py index 425ab143f5e..2432ae04845 100644 --- a/src/qcodes/instrument_drivers/AlazarTech/dll_wrapper.py +++ b/src/qcodes/instrument_drivers/AlazarTech/dll_wrapper.py @@ -64,17 +64,21 @@ def _check_error_code( if len(argrepr) > 100: argrepr = argrepr[:96] + "...]" + # ``errcheck`` is always handed a ctypes foreign function, which has a + # ``__name__``, but a plain ``Callable`` is not guaranteed to. + func_name = getattr(func, "__name__", repr(func)) + logger.error( f"Alazar API returned code {return_code} from function " - f"{func.__name__} with args {argrepr}" + f"{func_name} with args {argrepr}" ) if return_code not in ERROR_CODES: raise RuntimeError( - f"unknown error {return_code} from function {func.__name__} with args: {argrepr}" + f"unknown error {return_code} from function {func_name} with args: {argrepr}" ) raise RuntimeError( - f"error {return_code}: {ERROR_CODES[ReturnCode(return_code)]} from function {func.__name__} with args: {argrepr}" + f"error {return_code}: {ERROR_CODES[ReturnCode(return_code)]} from function {func_name} with args: {argrepr}" ) return arguments From fff77b1564cd93a4010d054411144f034860f8cc Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:21:27 +0200 Subject: [PATCH 12/76] 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 c86ade62ad6..fb6ea81cfba 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 26f133821386a96d5dd2fc603104d28cc0cb1ce6 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:21:41 +0200 Subject: [PATCH 13/76] Suppress ty on the qcodes_abstractmethod marker The decorator tags the decorated function with a marker attribute that ParameterBase later reads. A Callable has no such attribute as far as the type system is concerned, so extend the existing mypy suppression to ty. --- src/qcodes/utils/abstractmethod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qcodes/utils/abstractmethod.py b/src/qcodes/utils/abstractmethod.py index 26d0ac3611f..ed6db0ecbf4 100644 --- a/src/qcodes/utils/abstractmethod.py +++ b/src/qcodes/utils/abstractmethod.py @@ -16,7 +16,7 @@ def qcodes_abstractmethod[**input, output]( instantiated and we will use this property to detect if the method is abstract and should be overwritten. """ - funcobj.__qcodes_is_abstract_method__ = True # type: ignore[attr-defined] + funcobj.__qcodes_is_abstract_method__ = True # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] return funcobj From 335ed2bddae080085eb61dc8aa8c7541a7f7d694 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:23:22 +0200 Subject: [PATCH 14/76] 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 93a382b97947e5d187763772f7d587665e230f28 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:24:47 +0200 Subject: [PATCH 15/76] Annotate the Keithley 7510 buffer data dictionary dict.fromkeys with no value is typed as dict[str, Any | None], so every read of the processed data had to be suppressed. The loop below assigns every key anyway, so start from an empty dict with the intended type and drop the two suppressions. --- .../instrument_drivers/Keithley/Keithley_7510.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/qcodes/instrument_drivers/Keithley/Keithley_7510.py b/src/qcodes/instrument_drivers/Keithley/Keithley_7510.py index b45b8750414..2929ce948c8 100644 --- a/src/qcodes/instrument_drivers/Keithley/Keithley_7510.py +++ b/src/qcodes/instrument_drivers/Keithley/Keithley_7510.py @@ -367,7 +367,8 @@ def _get_data(self) -> DataArray7510: n_elements = len(elements) units = tuple(elements_units[element] for element in elements) - processed_data = dict.fromkeys(elements) + # every element is filled in by the loop below + processed_data: dict[str, npt.NDArray] = {} for i, (element, unit) in enumerate(zip(elements, units)): if unit == "str": processed_data[element] = np.array(all_data[i::n_elements]) @@ -384,12 +385,9 @@ def _get_data(self) -> DataArray7510: setpoint_units=((self.setpoints.unit,),) * n_elements, setpoint_names=((self.setpoints.label,),) * n_elements, ) - data._data = tuple( - tuple(processed_data[element]) # type: ignore[arg-type] - for element in elements - ) + data._data = tuple(tuple(processed_data[element]) for element in elements) for i in range(len(data.names)): - setattr(data, data.names[i], tuple(processed_data[data.names[i]])) # type: ignore[arg-type] + setattr(data, data.names[i], tuple(processed_data[data.names[i]])) return data def clear_buffer(self) -> None: From 56614f9080ddc5ee64fca7409d1c6c7399d0d0c3 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:28:54 +0200 Subject: [PATCH 16/76] 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/8370.underthehood | 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/8370.underthehood diff --git a/docs/changes/newsfragments/8370.underthehood b/docs/changes/newsfragments/8370.underthehood new file mode 100644 index 00000000000..3317ec3768c --- /dev/null +++ b/docs/changes/newsfragments/8370.underthehood @@ -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 a0d17507bab..890a47d714b 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 379d40f0cfc3204b9fcbcead51242f6689320332 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:30:32 +0200 Subject: [PATCH 17/76] 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 8a965080cca..d455565b80e 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 fb2ee402b3f9cdeebe91226e5360ad04917a0364 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:30:55 +0200 Subject: [PATCH 18/76] Suppress ty on the IPToVisa base class conflict IPToVisa deliberately injects VisaInstrument ahead of IPInstrument in the MRO so that an IPInstrument can be driven by the pyvisa-sim backend, as the class docstring explains. The two bases declare set_address incompatibly, which already carried a mypy suppression, so extend it to ty. --- src/qcodes/instrument/ip_to_visa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qcodes/instrument/ip_to_visa.py b/src/qcodes/instrument/ip_to_visa.py index 67d61acb421..1183509a3f5 100644 --- a/src/qcodes/instrument/ip_to_visa.py +++ b/src/qcodes/instrument/ip_to_visa.py @@ -24,7 +24,7 @@ # Such a driver is just a two-line class definition. -class IPToVisa(VisaInstrument, IPInstrument): # type: ignore[misc] +class IPToVisa(VisaInstrument, IPInstrument): # type: ignore[misc] # ty: ignore[invalid-method-override] """ Class to inject an VisaInstrument like behaviour in an IPInstrument that we'd like to use as a VISAInstrument with the From 676466facf88ada9bf434446f2db73df8faa612a Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:31:17 +0200 Subject: [PATCH 19/76] 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 dd37c824468..ea07debefd8 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 d304597d090d503e9e7f613a8cbeec5e80ac0f90 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:32:25 +0200 Subject: [PATCH 20/76] 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/8371.underthehood | 5 +++++ src/qcodes/instrument_drivers/tektronix/AWG5014.py | 10 +++++----- 2 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 docs/changes/newsfragments/8371.underthehood diff --git a/docs/changes/newsfragments/8371.underthehood b/docs/changes/newsfragments/8371.underthehood new file mode 100644 index 00000000000..10becb126c8 --- /dev/null +++ b/docs/changes/newsfragments/8371.underthehood @@ -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 c0084abea47..604a7ade9f6 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 c8b2d463a9c6253813cc833a5a37eb65fd3167ee Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:32:51 +0200 Subject: [PATCH 21/76] 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 567df9a8b1d..74feae11f0f 100644 --- a/src/qcodes/parameters/parameter.py +++ b/src/qcodes/parameters/parameter.py @@ -555,7 +555,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 60cee39b25fc1287702ae7e4cc26c8bbba15bb5f Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:33:14 +0200 Subject: [PATCH 22/76] Suppress ty on the Lakeshore CHANNEL_CLASS default Assigning the class that matches the default of the covariant channel type variable is rejected by mypy and pyright already, and ty agrees. Extend the existing suppression and note that all three flag it. --- src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py b/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py index 1b8b9ff9d8e..2ad9601ae4f 100644 --- a/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py +++ b/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py @@ -690,9 +690,9 @@ class LakeshoreBase(VisaInstrument, Generic[ChanType_co]): # Define this in the model-specific class in case you want to use a # different class for sensor channels # type error. It's not clear to me why assigning a value that matches the - # default of the TypeVar is an error but both mypy and pyright - # flags it here. - CHANNEL_CLASS: type[ChanType_co] = LakeshoreBaseSensorChannel # type: ignore[assignment] + # default of the TypeVar is an error but mypy, pyright and ty all + # flag it here. + CHANNEL_CLASS: type[ChanType_co] = LakeshoreBaseSensorChannel # type: ignore[assignment] # ty: ignore[invalid-assignment] # This dict has channel name in the driver as keys, and channel "name" that # is used in instrument commands as values. For example, if channel called From eec10b00b633ff58b05efbd0da8f9538e7935edd Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:33:37 +0200 Subject: [PATCH 23/76] 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 343e6495fbe127981bac0bf5fc00e9164a887795 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:34:42 +0200 Subject: [PATCH 24/76] Suppress a ty narrowing imprecision in get_chain_links_of_type Filtering the parameter chain with isinstance against the type variable should narrow the elements to C. ty instead widens the result to a union of C with the unnarrowed parameter type, but only when the class being narrowed is generic. mypy and pyright both narrow it correctly. --- src/qcodes/extensions/infer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/qcodes/extensions/infer.py b/src/qcodes/extensions/infer.py index 3abc0d3ff0b..80c002857f5 100644 --- a/src/qcodes/extensions/infer.py +++ b/src/qcodes/extensions/infer.py @@ -226,7 +226,10 @@ def get_chain_links_of_type[C: ParameterBase]( link_param_type: type[C] | tuple[type[C], ...], parameter: Parameter ) -> tuple[C, ...]: """Gets all parameters in a chain of linked parameters that match a given type""" - chain_links: list[C] = [ + # ty does not narrow the element type to C here: for a generic parameter + # class it widens the isinstance narrowing to a union with the unnarrowed + # type. The equivalent non generic code narrows correctly. + chain_links: list[C] = [ # ty: ignore[invalid-assignment] param for param in get_parameter_chain(parameter) if isinstance(param, link_param_type) From caebd3bd9846ca11f35b09232c8d5ec5a4c2347d Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:35:03 +0200 Subject: [PATCH 25/76] Suppress ty on the run overview extra columns The keys of the extra columns are supplied by the caller at runtime, so they cannot be part of the closed RunOverviewDict definition, as the comment above already records. Extend the existing mypy suppression to ty. --- src/qcodes/dataset/sqlite/db_overview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qcodes/dataset/sqlite/db_overview.py b/src/qcodes/dataset/sqlite/db_overview.py index e596439f46a..590a94fb18a 100644 --- a/src/qcodes/dataset/sqlite/db_overview.py +++ b/src/qcodes/dataset/sqlite/db_overview.py @@ -253,7 +253,7 @@ def get_db_overview( # The keys of ``extra`` are only known at runtime (they are the # user-supplied ``extra_columns``), so they cannot be part of # the closed ``RunOverviewDict`` definition. - entry.update(extra) # type: ignore[typeddict-item] + entry.update(extra) # type: ignore[typeddict-item] # ty: ignore[invalid-argument-type] overview[run_id] = entry From 75b0dab3acf541a085e66ea98ec172fe58e0b638 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:35:55 +0200 Subject: [PATCH 26/76] 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 1dbc25deabc..fbf74b29057 100644 --- a/src/qcodes/instrument/channel.py +++ b/src/qcodes/instrument/channel.py @@ -1213,7 +1213,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 c5c72146b2d459a347737605a410a71fd21e2e59 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:36:25 +0200 Subject: [PATCH 27/76] 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 fbf74b29057..2c77792638e 100644 --- a/src/qcodes/instrument/channel.py +++ b/src/qcodes/instrument/channel.py @@ -731,9 +731,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 fa1633f51fbf27e44cdceb8992faee5b296f4121 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 07:37:12 +0200 Subject: [PATCH 28/76] Annotate the second Command exec mapping The two exec mappings are built in mutually exclusive branches but shared a name, and only the first carried an annotation. ty takes the inferred type of the second, whose keys are plain bool tuples, so looking up a key that may be the literal "multi" was an error. Give the second mapping its own name and the same annotation. --- src/qcodes/parameters/command.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/qcodes/parameters/command.py b/src/qcodes/parameters/command.py index e2da0b14d01..0561dab1dab 100644 --- a/src/qcodes/parameters/command.py +++ b/src/qcodes/parameters/command.py @@ -124,7 +124,10 @@ def __init__( elif is_function(cmd, arg_count): assert cmd is not None self._cmd = cmd - exec_mapping = { + cmd_exec_mapping: dict[ + tuple[bool | Literal["multi"], bool], + Callable[..., Output | ParsedOutput], + ] = { # (parse_input, parse_output) (False, False): cmd, (False, True): self.call_cmd_parsed_out, (True, False): self.call_cmd_parsed_in, @@ -132,7 +135,7 @@ def __init__( ("multi", False): self.call_cmd_parsed_in2, ("multi", True): self.call_cmd_parsed_in2_out, } - self.exec_function = exec_mapping[(parse_input, parse_output)] + self.exec_function = cmd_exec_mapping[(parse_input, parse_output)] elif cmd is None: if no_cmd_function is not None: From 6b4ddbfc872c81792af378675c75f04141f593d6 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Aug 2026 08:09:30 +0200 Subject: [PATCH 29/76] 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 890a47d714b..f3c9be83a0b 100644 --- a/src/qcodes/dataset/sqlite/database.py +++ b/src/qcodes/dataset/sqlite/database.py @@ -174,7 +174,10 @@ def connect( sqlite3.register_converter("numeric", _convert_numeric) - for numpy_float in (float, *numpy_floats): + # registered separately from the numpy floats below, so that the element + # type of the loop stays a numpy float rather than widening to object + sqlite3.register_adapter(float, _adapt_float) + for numpy_float in numpy_floats: sqlite3.register_adapter(numpy_float, _adapt_float) for complex_type in complex_types: From c813cae97988054512c50717bf02c2037041f80d Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 19:35:27 +0200 Subject: [PATCH 30/76] Suppress the get_ramp_values mismatch newly reported by ty 0.0.74 get_ramp_values works in numbers while the value being set has the generic parameter data type, which the comment above already records and which mypy has always reported. The constraint solver changes in 0.0.74 mean ty now reports it too, so extend the existing suppression. --- src/qcodes/parameters/parameter_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qcodes/parameters/parameter_base.py b/src/qcodes/parameters/parameter_base.py index a0f19151dfe..ddd9dd190f0 100644 --- a/src/qcodes/parameters/parameter_base.py +++ b/src/qcodes/parameters/parameter_base.py @@ -1033,7 +1033,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 d353b7b519ba826018b8b006bac75297f3b33810 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 19:42:57 +0200 Subject: [PATCH 31/76] Add drafts for the two remaining ty issues Both still reproduce on 0.0.74, unlike astral-sh/ty#4303 which that release fixes. Keeping the drafts alongside the suppressions they explain, so the repros stay with the code that needs them. --- ty-issue-1-typeddict-self-bound.md | 195 ++++++++++++++++++++++++++ ty-issue-2-typevar-default-context.md | 142 +++++++++++++++++++ 2 files changed, 337 insertions(+) create mode 100644 ty-issue-1-typeddict-self-bound.md create mode 100644 ty-issue-2-typevar-default-context.md diff --git a/ty-issue-1-typeddict-self-bound.md b/ty-issue-1-typeddict-self-bound.md new file mode 100644 index 00000000000..70ea4ed4858 --- /dev/null +++ b/ty-issue-1-typeddict-self-bound.md @@ -0,0 +1,195 @@ +# ty issue draft 1 + +**Title** + +> Generic `TypedDict` with a type parameter default: `Self`-bound methods and `**`-unpacking rejected for every non-default specialization + +**Labels to suggest:** `bug`, `generics`, `typeddict`, `constraint-solver` + +--- + +### Summary + +When a generic `TypedDict` declares a default for its type parameter, ty computes +the upper bound of the synthesized `Self` type variable as the *default* +specialization rather than the generic one. Every other specialization is then +rejected by any method that binds `Self`. + +```python +from typing import TypedDict + + +class Movie[T = int](TypedDict): + extra: T + + +def f(m: Movie[str]) -> None: + m.keys() +``` + +``` +error[invalid-argument-type]: Argument to bound method `TypedDictFallback.keys` is incorrect + --> repro.py:7:5 + | +7 | m.keys() + | ^^^^^^^^ Argument type `Movie[str]` does not satisfy upper bound `Movie[int]` of type variable `Self` +``` + +Removing the default (`class Movie[T](TypedDict)`) makes the error go away +without any other change, so the default is what introduces the bound. + +Note that `Movie[str]` here is an ordinary concrete specialization. No type +variable is unsolved at the call site, and nothing is being inferred. + +### Which specializations are affected + +Only the declared default is accepted: + +| annotation | result | +| --- | --- | +| `Movie[int]` (the default) | ok | +| `Movie` (bare, default applies) | ok | +| `Movie[str]` | error | +| `Movie[T]` for an enclosing type variable `T` | error | + +### Which members are affected + +Members whose signature binds `Self`: + +| member | result | +| --- | --- | +| `keys()` | error | +| `values()` | error | +| `items()` | error | +| `copy()` | error | +| `**` unpacking | error | +| `get()` | ok | +| `setdefault()` | ok | +| `pop()` | ok | +| `update()` | ok | + +Assignability is unaffected, which is consistent with the problem being the +`Self` bound rather than the type itself: + +```python +from typing import Mapping, TypedDict + + +class Movie[T = int](TypedDict): + extra: T + + +def f(m: Movie[str]) -> None: + ok: Mapping[str, object] = m # no error +``` + +### The `**` unpacking symptom + +`**`-unpacking reports a different and rather misleading message, which is how I +originally ran into this: + +```python +from typing import TypedDict + + +class Movie[T = int](TypedDict): + extra: T + + +def f(m: Movie[str]) -> None: + dict(**m) +``` + +``` +error[invalid-argument-type]: Argument expression after ** must be a mapping type + --> repro.py:7:12 + | +7 | dict(**m) + | ^ Found `Movie[str]` +``` + +A `TypedDict` is always a `Mapping[str, object]`, so this message points away +from the real cause. + +### Not specific to `TypedDict` syntax or version + +The legacy spelling behaves identically: + +```python +from typing import Generic, TypedDict, TypeVar + +T = TypeVar("T", default=int) + + +class Movie(TypedDict, Generic[T]): + extra: T + + +def f(m: Movie[str]) -> None: + m.copy() +``` + +A plain generic class with a type parameter default is **not** affected, so this +looks specific to the synthesized `TypedDictFallback` `Self`: + +```python +class WithDefault[T = int]: + def m(self) -> None: ... + + +def f[T](a: WithDefault[T]) -> None: + a.m() # no error +``` + +Any non-`Any` default triggers it. `Any` is the only default that is accepted, +which is probably why this has gone unnoticed: + +| type parameter | result | +| --- | --- | +| `class Movie[T](TypedDict)` | ok | +| `class Movie[T = Any](TypedDict)` | ok | +| `class Movie[T = int](TypedDict)` | error | +| `class Movie[T = None](TypedDict)` | error | +| `class Movie[T = int \| None](TypedDict)` | error | +| `class Movie[T = object](TypedDict)` | error | +| `class Movie[T: int \| None = int \| None](TypedDict)` | error | + +Reproduced on 0.0.72, 0.0.73 and 0.0.74. Checked with `--python-version 3.13` so +that the PEP 696 syntax is not itself reported as an error. mypy 2.3.1 and +pyright both accept all of the above. + +I searched existing issues for `"must be a mapping type"`, `TypedDict Unpack +default`, `"generic TypedDict"`, `"PEP 696"` and `Unpack kwargs` and did not +find a preexisting issue. #4255 is the closest but is about a union alias in a +stub leaking an unspecialized type variable. + +The error shape is reminiscent of #4303, which is also an upper bound on a type +variable being applied too strictly, though that one is about a `bound=` on a +class scoped type variable rather than a `default=` on `Self`. + +### Relation to the feature overview + +The type system feature overview in #1889 lists all of the following as +implemented: + +- Generics: `TypeVar` defaults (PEP 696) +- `TypedDict`: Inheritance, generic `TypedDict`s +- `TypedDict`: Structural assignability and equivalence +- `TypedDict`: Methods (`get`, `pop`, `setdefault`, `keys`, `values`, `copy`) + +This report sits at the intersection of those, so following the guidance at the +top of #1889 for features marked completed, it seemed worth reporting rather +than upvoting a tracking issue. + +It is worth stressing that this is **not** about `Unpack` for `**kwargs` typing, +which #1889 tracks separately in #1746. The lead repro contains no `Unpack` and +no `**` at all, just `Movie[str].keys()`. The `**` message is only how I +happened to notice it. + +Structural assignability also still works (`Mapping[str, object] = m` is +accepted), so this looks narrowly scoped to the upper bound computed for the +synthesized `Self`. + +### Version + +0.0.74 diff --git a/ty-issue-2-typevar-default-context.md b/ty-issue-2-typevar-default-context.md new file mode 100644 index 00000000000..62eec02d3d0 --- /dev/null +++ b/ty-issue-2-typevar-default-context.md @@ -0,0 +1,142 @@ +# ty issue draft 2 + +**Title** + +> Function scoped `TypeVar` default takes precedence over the declared type context, where an unsolved type variable would be accepted + +**Labels to suggest:** `bidirectional inference`, `constraint-solver`, `generics` + +--- + +### Summary + +When a function scoped type variable appears only in the return type and is not +constrained by any argument, ty leaves it unsolved as `Unknown`, which is +gradually compatible with whatever the result is assigned to. If that same type +variable declares a PEP 696 default, ty substitutes the default instead, which +is concrete and then conflicts with the declared type. + +```python +class Box[T]: + pass + + +def make[T = int](cls: type[T] | None = None) -> Box[T]: + raise NotImplementedError + + +def caller() -> None: + a: Box[str] = make() +``` + +``` +error[invalid-assignment]: Object of type `Box[int]` is not assignable to `Box[str]` + --> repro.py:8:19 + | +8 | a: Box[str] = make() + | ^^^^^^ +``` + +Removing the default makes ty accept it: + +```python +class Box[T]: + pass + + +def make[T](cls: type[T] | None = None) -> Box[T]: + raise NotImplementedError + + +def caller() -> None: + a: Box[str] = make() # ty: ok +``` + +`reveal_type` shows what is actually happening. The declared type is never used +to solve `T` in either case; the difference is only what fills the unsolved slot: + +| declaration | `reveal_type(make())` | `a: Box[str] = make()` | +| --- | --- | --- | +| `def make[T](...) -> Box[T]` | `Box[Unknown]` | accepted | +| `def make[T = int](...) -> Box[T]` | `Box[int]` | error | + +So adding a default is strictly worse than having no default at all, at every +call site that annotates its target. mypy 2.3.1 and pyright accept both forms. + +### The type context is available + +This is not a case of ty lacking the necessary context. Using the example from +#3933, the declared type of the assignment target clearly does reach the +constraint solver, since it widens the argument: + +```python +class Parent: ... + + +class Child(Parent): ... + + +def head[T](x: list[T]) -> T: + return x[0] + + +x: Parent = head(reveal_type([Child()])) # revealed: list[Parent] +``` + +I reproduced that on 0.0.74. So in `a: Box[str] = make()` the constraint +`Box[T] <: Box[str]` is available, but the default is applied in preference to +it. + +### Why this matters + +This pattern is common in factory functions, where the default exists to give a +sensible type to an unannotated call while still allowing the caller to ask for +something more specific (illustrative, from our codebase): + +```python +p = instrument.add_parameter("name") # want the default +q: Parameter[float, Self] = instrument.add_parameter("x") # want this instead +``` + +With ty's current behaviour the default wins in both cases, so the second form +is unusable and every annotated call site becomes an error. In our codebase this +produced 34 errors across instrument drivers from a single type variable +declaration. We ended up widening the default to a fully gradual type to work +around it, which loses the information the default was there to provide. + +### Relation to #3933 and the feature overview + +This looks like it may fall under #3933, constraint-set-aware bidirectional +inference. That issue is written in terms of constraints flowing into *argument* +inference, and all of its examples involve arguments that get eagerly +specialized or wrongly widened. The case here has no arguments at all, so the +symptom is different, but the underlying gap looks similar: the outer constraint +is not being unified with the specialization of the call. + +If the second approach in #3933 is taken, propagating constraints during +bidirectional inference rather than eagerly specializing, then `Box[T] <: +Box[str]` should presumably solve `T` to `str` before any default is considered, +which would fix this too. Filing separately in case that is not the intent, and +because the interaction with PEP 696 defaults is not mentioned there. + +The type system feature overview in #1889 lists "`TypeVar` defaults (PEP 696)" +as implemented under Generics. That section also has an open sub-item, "Solve +type variables in all cases" (#623), which may be the more appropriate home if +this is considered a solver limitation rather than a deliberate choice about +defaults. + +### Note on the spec + +I could not find wording in PEP 696 or the typing spec that settles whether the +declared type context should take precedence over a type variable default, so +this may be intentional. If it is, it would be helpful to say so explicitly, +since the natural reading of "the default is used when the type variable cannot +be solved" is that a solution derived from the type context counts as solving +it. The current behaviour also has the surprising property that adding a default +makes a call site fail that would otherwise have been accepted. + +Reproduced on 0.0.72, 0.0.73 and 0.0.74, checked with `--python-version 3.13`. + +### Version + +0.0.74 From 75d48981d4f1ab68a6f0038b20562c0e27715b6f Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 19:56:55 +0200 Subject: [PATCH 32/76] Document how mypy and ty suppression codes interact ty documents putting a ty rule into a mypy type: ignore comment by prefixing it with ty:. mypy does not recognise the prefixed code and reports it as unused when warn_unused_ignores is enabled, which we enable, so we use two comments on one line instead. Record the test case and the commands to run it, so the conclusion can be rechecked when either checker changes. --- suppression-codes-mypy-and-ty.md | 100 +++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 suppression-codes-mypy-and-ty.md diff --git a/suppression-codes-mypy-and-ty.md b/suppression-codes-mypy-and-ty.md new file mode 100644 index 00000000000..98128dc46ed --- /dev/null +++ b/suppression-codes-mypy-and-ty.md @@ -0,0 +1,100 @@ +# Combining mypy and ty suppression codes + +## Summary + +The [ty suppression docs](https://docs.astral.sh/ty/suppression/) document putting +a ty rule into a mypy `type: ignore` comment by prefixing it with `ty:`: + +```python +sum_three_numbers("one", 5, 2) # type: ignore[arg-type, ty:invalid-argument-type] +``` + +ty honours this. **mypy does not ignore the `ty:` prefixed code**, and reports it +as an unused suppression when `warn_unused_ignores` is enabled, which qcodes +enables in `pyproject.toml`. So the combined form cannot be used here. + +qcodes therefore uses two comments on the same line: + +```python +f("one") # type: ignore[arg-type] # ty: ignore[invalid-argument-type] +``` + +That is the only form of the three below that all three checkers accept. + +## Results + +| form | ty 0.0.74 | mypy 2.3.1 with `warn_unused_ignores` | mypy 2.3.1 without it | pyright 1.1.411 | +| --- | --- | --- | --- | --- | +| `# type: ignore[arg-type, ty:invalid-argument-type]` | suppressed | `Unused "type: ignore[ty:invalid-argument-type]" comment` | clean | suppressed | +| `# type: ignore[arg-type]` + `# ty: ignore[invalid-argument-type]` | suppressed | clean | clean | suppressed | +| `# type: ignore[ty:invalid-argument-type]` | suppressed | unused, and `arg-type` not covered | `arg-type` not covered | suppressed | + +Note that the `arg-type` half of the combined form *is* honoured by mypy. It is +only the `ty:` prefixed code that mypy does not recognise, and therefore reports +as unused. + +pyright honours a `# type: ignore` comment regardless of the codes in it, so it +accepts all three forms. That is also why removing a mypy suppression can +surface a pyright error on the same line. + +## Test case + +```python +def f(a: int) -> None: ... + + +# 1. combined form from the ty docs +f("one") # type: ignore[arg-type, ty:invalid-argument-type] + +# 2. the two comment form used in qcodes +f("one") # type: ignore[arg-type] # ty: ignore[invalid-argument-type] + +# 3. combined form, ty rule only +f("one") # type: ignore[ty:invalid-argument-type] + +# 4. control, expected to be reported by every checker +f("one") +``` + +Run from the repository root so that the mypy configuration in `pyproject.toml` +is picked up: + +``` +uv run ty check --output-format concise +uv run --extra test mypy +uv run --extra test mypy --no-warn-unused-ignores +uv run pyright +``` + +Only case 4 should be reported. Every checker reporting anything on cases 1 to 3 +tells you which form is currently supported. + +## Why we keep `warn_unused_ignores` + +Dropping `warn_unused_ignores` would make the combined form work, but that +setting is worth more than the shorter comments. It is what tells us when a +suppression has become obsolete. During the ty migration it caught: + +- the `issuperset` suppression becoming redundant once + [astral-sh/ty#4303](https://github.com/astral-sh/ty/issues/4303) was fixed in + ty 0.0.74 +- the two suppressions in the Keithley 7510 buffer becoming unnecessary once the + data dictionary was annotated +- several suppressions in `ParameterBase` becoming unnecessary once the duck + typed conversions were moved behind helpers + +## Suggested upstream change + +mypy could ignore codes carrying a `:` prefix in `type: ignore` comments, +rather than treating them as mypy codes that turned out to be unused. That would +make the form documented by ty usable in projects that run both checkers with +`warn_unused_ignores` enabled, and would generalise to any other checker that +wants to share the comment. + +Failing that, the ty documentation could note that the combined form conflicts +with mypy's `warn_unused_ignores`, and suggest the two comment form for projects +that run both. + +## Versions + +Measured with ty 0.0.74, mypy 2.3.1 and pyright 1.1.411. From 09a60d80321eef67915255247ce22b53e41351d4 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 20:03:08 +0200 Subject: [PATCH 33/76] Document how pyright reads suppression comments pyright honours mypy's type: ignore as a blanket suppression of its own rules, ignoring the codes in it, which is why removing a mypy suppression can surface a pyright error on the same line. It does not read ty: ignore at all. Also record why we cannot enable reportUnnecessaryTypeIgnoreComment: it calls a comment unnecessary whenever pyright itself has nothing to report, so every mypy only suppression would be flagged. --- suppression-codes-mypy-and-ty.md | 125 +++++++++++++++++++++++++++++-- 1 file changed, 118 insertions(+), 7 deletions(-) diff --git a/suppression-codes-mypy-and-ty.md b/suppression-codes-mypy-and-ty.md index 98128dc46ed..8479b7d269c 100644 --- a/suppression-codes-mypy-and-ty.md +++ b/suppression-codes-mypy-and-ty.md @@ -33,9 +33,88 @@ Note that the `arg-type` half of the combined form *is* honoured by mypy. It is only the `ty:` prefixed code that mypy does not recognise, and therefore reports as unused. -pyright honours a `# type: ignore` comment regardless of the codes in it, so it -accepts all three forms. That is also why removing a mypy suppression can -surface a pyright error on the same line. +## How pyright fits in + +pyright has its own suppression comment and also honours mypy's, which is why it +accepts all three forms above. + +| comment | pyright | +| --- | --- | +| `# type: ignore` | suppressed | +| `# type: ignore[arg-type]` | suppressed | +| `# type: ignore[arg-type, ty:invalid-argument-type]` | suppressed | +| `# pyright: ignore` | suppressed | +| `# pyright: ignore[reportArgumentType]` | suppressed | +| `# pyright: ignore[reportGeneralTypeIssues]` | **not** suppressed, wrong rule | +| `# ty: ignore[invalid-argument-type]` | **not** suppressed | + +Two things follow from this. + +**`# type: ignore` is a blanket suppression for pyright.** pyright does not parse +the codes in it, so `# type: ignore[arg-type]` silences *every* pyright rule on +that line, not just the argument type one. A consequence that came up repeatedly +during the ty migration: removing a mypy suppression can surface a pyright error +on the same line that was never visible before. `# pyright: ignore[rule]` is the +precise form, and unlike `# type: ignore` it only suppresses the rules listed. + +**A ty only suppression does not silence pyright.** `# ty: ignore[...]` is just a +comment as far as pyright is concerned. That is what makes the two comment form +safe: the mypy half keeps pyright quiet as a side effect, and the ty half is +inert for both of the others. + +## Unused suppression detection + +The three checkers differ in whether they tell you a suppression has gone stale. + +| checker | setting | default | reports unused | +| --- | --- | --- | --- | +| mypy | `warn_unused_ignores` | off | enabled in `pyproject.toml` | +| ty | `unused-ignore-comment` | on | yes, for `ty: ignore` directives | +| pyright | `reportUnnecessaryTypeIgnoreComment` | off | not enabled, see below | + +With the pyright setting enabled it reports all of these: + +```python +def g(a: int) -> None: ... + + +g(1) # type: ignore +g(1) # pyright: ignore +g(1) # pyright: ignore[reportArgumentType] +``` + +``` +Unnecessary "# type: ignore" comment +Unnecessary "# type: ignore" comment +Unnecessary "# pyright: ignore" rule: "reportArgumentType" +``` + +**We cannot enable it while we also run mypy.** Because pyright treats +`# type: ignore` as a blanket suppression of *its own* rules, it calls the +comment unnecessary whenever pyright itself has nothing to report on the line, +with no knowledge of whether mypy needed it. Every mypy only suppression in the +code base would be reported as unnecessary. For example: + +```python +from typing import Any + + +class A: + def m(self) -> None: ... + + +def make(a: A, replacement: Any) -> None: + # mypy reports method-assign here, pyright has no equivalent check + a.m = replacement # type: ignore[method-assign] +``` + +mypy needs that suppression: removing it gives +`error: Cannot assign to a method [method-assign]`. pyright with +`reportUnnecessaryTypeIgnoreComment` enabled reports the very same line as +`Unnecessary "# type: ignore" comment`. + +So mypy's `warn_unused_ignores` and ty's `unused-ignore-comment` are the two +stale suppression checks we can actually rely on. ## Test case @@ -56,6 +135,25 @@ f("one") # type: ignore[ty:invalid-argument-type] f("one") ``` +And for the pyright specific forms: + +```python +def h(a: int) -> None: ... + + +# 5. pyright: ignore, blanket +h("one") # pyright: ignore + +# 6. pyright: ignore with the matching rule +h("one") # pyright: ignore[reportArgumentType] + +# 7. pyright: ignore with a non matching rule +h("one") # pyright: ignore[reportGeneralTypeIssues] + +# 8. ty: ignore only +h("one") # ty: ignore[invalid-argument-type] +``` + Run from the repository root so that the mypy configuration in `pyproject.toml` is picked up: @@ -66,14 +164,27 @@ uv run --extra test mypy --no-warn-unused-ignores uv run pyright ``` -Only case 4 should be reported. Every checker reporting anything on cases 1 to 3 -tells you which form is currently supported. +Expected results: + +| block | ty | mypy | pyright | +| --- | --- | --- | --- | +| first, cases 1 to 4 | 4 | 1, 3, 4 and two unused directives | 4 | +| second, cases 5 to 8 | 5, 6, 7 | 5, 6, 7, 8 | 7, 8 | + +The second block deliberately exercises comments that only one checker +understands, so most cases are reported by the other two. That is the point: it +shows that `pyright: ignore` is inert for mypy and ty, and that `ty: ignore` is +inert for mypy and pyright. + +Any deviation from this table tells you that one of the checkers has changed how +it reads these comments. ## Why we keep `warn_unused_ignores` Dropping `warn_unused_ignores` would make the combined form work, but that -setting is worth more than the shorter comments. It is what tells us when a -suppression has become obsolete. During the ty migration it caught: +setting is worth more than the shorter comments. As shown above it is, together +with ty's `unused-ignore-comment`, one of only two stale suppression checks +available to us. During the ty migration it caught: - the `issuperset` suppression becoming redundant once [astral-sh/ty#4303](https://github.com/astral-sh/ty/issues/4303) was fixed in From fafefb31989d058921774ea350a00e0e39d9bcc1 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 21:08:32 +0200 Subject: [PATCH 34/76] 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/8373.underthehood | 5 +++++ src/qcodes/dataset/json_exporter.py | 8 ++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 docs/changes/newsfragments/8373.underthehood diff --git a/docs/changes/newsfragments/8373.underthehood b/docs/changes/newsfragments/8373.underthehood new file mode 100644 index 00000000000..e519a10b079 --- /dev/null +++ b/docs/changes/newsfragments/8373.underthehood @@ -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 a1f5adb535536ef926bff1c934dce2f52823eff2 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 21:09:48 +0200 Subject: [PATCH 35/76] 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/8374.improved | 6 ++++++ src/qcodes/dataset/data_set.py | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 docs/changes/newsfragments/8374.improved diff --git a/docs/changes/newsfragments/8374.improved b/docs/changes/newsfragments/8374.improved new file mode 100644 index 00000000000..e0c385c8e16 --- /dev/null +++ b/docs/changes/newsfragments/8374.improved @@ -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 b8f53a1b9c3..868eed6705d 100644 --- a/src/qcodes/dataset/data_set.py +++ b/src/qcodes/dataset/data_set.py @@ -1144,7 +1144,11 @@ def write_data_to_text_file( def subscribe( self, - callback: Callable[[Any, int, Any | None], None], + # ``Callable[..., None]`` rather than a three argument callable because + # ``callback_kwargs`` below is bound onto the callback with + # ``functools.partial``, so it may take further keyword arguments. This + # matches how ``_Subscriber`` types the same callback. + callback: Callable[..., None], min_wait: int = 0, min_count: int = 1, state: Any | None = None, From 73b2520949e23026bdd5f536288b2b91322bd891 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 21:13:04 +0200 Subject: [PATCH 36/76] 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/8375.improved | 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/8375.improved diff --git a/docs/changes/newsfragments/8375.improved b/docs/changes/newsfragments/8375.improved new file mode 100644 index 00000000000..b90716a9f22 --- /dev/null +++ b/docs/changes/newsfragments/8375.improved @@ -0,0 +1,7 @@ +``Keysight34980A.module`` is now a ``dict`` of +``Keysight34980ASwitchMatrixSubModule`` rather than one built with +``dict.fromkeys``, whose values were typed as possibly ``None``. ``scan_slots`` +puts an entry in for every slot, either the driver for the installed module or a +generic submodule, so the values were never ``None`` once the instrument was +constructed. Code using ``instrument.module[slot]`` no longer has to account for +a ``None`` that cannot occur. diff --git a/docs/examples/driver_examples/Qcodes example with Keysight 34980A Switch Mainframe and Modules.ipynb b/docs/examples/driver_examples/Qcodes example with Keysight 34980A Switch Mainframe and Modules.ipynb index 3726074aa3e..b1a64ba32a6 100644 --- a/docs/examples/driver_examples/Qcodes example with Keysight 34980A Switch Mainframe and Modules.ipynb +++ b/docs/examples/driver_examples/Qcodes example with Keysight 34980A Switch Mainframe and Modules.ipynb @@ -428,9 +428,10 @@ "metadata": {}, "outputs": [], "source": [ - "switch_matrix.module[\n", - " 2\n", - "]._is_locked = True # DO NOT perform this action in real situation" + "# DO NOT perform this action in a real situation. ``_is_locked`` is defined\n", + "# on the 34934A driver rather than on the shared submodule base class that\n", + "# ``module`` is typed as, hence the suppression.\n", + "switch_matrix.module[2]._is_locked = True # ty: ignore[unresolved-attribute]" ] }, { diff --git a/src/qcodes/instrument_drivers/Keysight/keysight_34980a.py b/src/qcodes/instrument_drivers/Keysight/keysight_34980a.py index 576b9888251..f93c08a149f 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysight_34980a.py +++ b/src/qcodes/instrument_drivers/Keysight/keysight_34980a.py @@ -70,7 +70,8 @@ def __init__( self._total_slot = 8 self._system_slots_info_dict: dict[int, dict[str, str]] | None = None - self.module = dict.fromkeys(self.system_slots_info.keys()) + # populated by scan_slots below, which puts an entry in for every slot + self.module: dict[int, Keysight34980ASwitchMatrixSubModule] = {} self.scan_slots() self.connect_message() @@ -132,7 +133,7 @@ def scan_slots(self) -> None: self.module[slot] = sub_module self.add_submodule(sub_module_name, sub_module) break - if self.module[slot] is None: + if slot not in self.module: sub_module_name = f"slot_{slot}_{model_string}_no_driver" sub_module_no_driver = Keysight34980ASwitchMatrixSubModule( self, sub_module_name, slot From 445f50d856fabab488be9e8a39f74dd665806337 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 21:18:50 +0200 Subject: [PATCH 37/76] 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/8376.improved | 6 ++++++ .../tektronix/AWGFileParser.py | 18 +++++++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 docs/changes/newsfragments/8376.improved diff --git a/docs/changes/newsfragments/8376.improved b/docs/changes/newsfragments/8376.improved new file mode 100644 index 00000000000..0437c9e88e2 --- /dev/null +++ b/docs/changes/newsfragments/8376.improved @@ -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 cf2808911fdc64f7f6b51d8cd688c12dfb7cfa59 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 24 Aug 2026 21:20:13 +0200 Subject: [PATCH 38/76] 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 392589d302773867c131cca5f378b356b871c245 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:00:21 +0200 Subject: [PATCH 39/76] 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. --- docs/changes/newsfragments/8377.underthehood | 3 +++ pyproject.toml | 14 ++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 docs/changes/newsfragments/8377.underthehood diff --git a/docs/changes/newsfragments/8377.underthehood b/docs/changes/newsfragments/8377.underthehood new file mode 100644 index 00000000000..73cf4961e3b --- /dev/null +++ b/docs/changes/newsfragments/8377.underthehood @@ -0,0 +1,3 @@ +``ty`` now also type checks the example notebooks in ``docs``. Unlike mypy and +pyright it understands Jupyter notebooks, so this is coverage that the other two +checkers do not provide. diff --git a/pyproject.toml b/pyproject.toml index 983f6bfa7ce..7b0055e36fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -300,8 +300,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", ] @@ -325,6 +326,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 07de95c639eb84fd73dc0bda49fe13849d592003 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:21:09 +0200 Subject: [PATCH 40/76] 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 dd68f4d9705a6a9eb6fdba814f2f787576d0e59a Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:21:38 +0200 Subject: [PATCH 41/76] 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/8381.improved | 6 ++++++ ...des example with Keysight B1500 Parameter Analyzer.ipynb | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 docs/changes/newsfragments/8381.improved diff --git a/docs/changes/newsfragments/8381.improved b/docs/changes/newsfragments/8381.improved new file mode 100644 index 00000000000..b124c324651 --- /dev/null +++ b/docs/changes/newsfragments/8381.improved @@ -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 5331519c1464cac7d3925b229b443a7aeeeef756 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:25:30 +0200 Subject: [PATCH 42/76] 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/8382.improved | 7 +++++++ .../instrument_drivers/Keysight/keysight_e4980a.py | 10 ++++++++++ 2 files changed, 17 insertions(+) create mode 100644 docs/changes/newsfragments/8382.improved diff --git a/docs/changes/newsfragments/8382.improved b/docs/changes/newsfragments/8382.improved new file mode 100644 index 00000000000..3727910a2a5 --- /dev/null +++ b/docs/changes/newsfragments/8382.improved @@ -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 d5fd9f090f30d6a0bbd8338ec60a8a5733ca8a53 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:33:51 +0200 Subject: [PATCH 43/76] 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/8384.improved | 4 ++++ ...e with Stanford SR86x with buffered readout.ipynb | 12 ++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 docs/changes/newsfragments/8384.improved diff --git a/docs/changes/newsfragments/8384.improved b/docs/changes/newsfragments/8384.improved new file mode 100644 index 00000000000..786b261bb83 --- /dev/null +++ b/docs/changes/newsfragments/8384.improved @@ -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 1e873b48fc40065c56540c3e4c18b791cd7bf926 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:38:11 +0200 Subject: [PATCH 44/76] 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/8385.improved | 6 ++++++ src/qcodes/instrument_drivers/Keysight/keysight_b220x.py | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 docs/changes/newsfragments/8385.improved diff --git a/docs/changes/newsfragments/8385.improved b/docs/changes/newsfragments/8385.improved new file mode 100644 index 00000000000..ffc16e01e34 --- /dev/null +++ b/docs/changes/newsfragments/8385.improved @@ -0,0 +1,6 @@ +``connect_paths``, ``disconnect_paths`` and ``to_channel_list`` on the Keysight +B220X switch matrix drivers now accept any iterable of paths rather than only a +``Sequence``. They iterate the paths once and do not index them, so passing a +set, as the example notebook does, is fine. Note that the order in which the +paths appear in the channel list then follows the iteration order of the +argument. diff --git a/src/qcodes/instrument_drivers/Keysight/keysight_b220x.py b/src/qcodes/instrument_drivers/Keysight/keysight_b220x.py index d6c4f9ee2c9..c8e01b51962 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysight_b220x.py +++ b/src/qcodes/instrument_drivers/Keysight/keysight_b220x.py @@ -7,7 +7,7 @@ from qcodes.validators import Enum, Ints, Lists, MultiType if TYPE_CHECKING: - from collections.abc import Callable, Sequence + from collections.abc import Callable, Iterable from typing import Concatenate, Unpack from qcodes.parameters import Parameter @@ -251,12 +251,12 @@ def connect(self, input_ch: int, output_ch: int) -> None: self.write(f":CLOS (@{self._card:01d}{input_ch:02d}{output_ch:02d})") @post_execution_status_poll - def connect_paths(self, paths: "Sequence[tuple[int, int]]") -> None: + def connect_paths(self, paths: "Iterable[tuple[int, int]]") -> None: channel_list_str = self.to_channel_list(paths) self.write(f":CLOS {channel_list_str}") @post_execution_status_poll - def disconnect_paths(self, paths: "Sequence[tuple[int, int]]") -> None: + def disconnect_paths(self, paths: "Iterable[tuple[int, int]]") -> None: channel_list_str = self.to_channel_list(paths) self.write(f":OPEN {channel_list_str}") @@ -424,7 +424,7 @@ def parse_channel_list(channel_list: str) -> set[tuple[int, int]]: for match in re.finditer(pattern, channel_list) } - def to_channel_list(self, paths: "Sequence[tuple[int, int]]") -> str: + def to_channel_list(self, paths: "Iterable[tuple[int, int]]") -> str: chan = [f"{self._card:01d}{i:02d}{o:02d}" for i, o in paths] channel_list = f"(@{','.join(chan)})" return channel_list From 32d355b7b26d0d87fab738f10b0477ebf79b86d8 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:40:55 +0200 Subject: [PATCH 45/76] 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/8386.improved | 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/8386.improved diff --git a/docs/changes/newsfragments/8386.improved b/docs/changes/newsfragments/8386.improved new file mode 100644 index 00000000000..60fc859b5b8 --- /dev/null +++ b/docs/changes/newsfragments/8386.improved @@ -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 47bbe2f38793d4491a6f4b4f92dc0178974e6d48 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 07:57:53 +0200 Subject: [PATCH 46/76] 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/8387.improved | 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/8387.improved diff --git a/docs/changes/newsfragments/8387.improved b/docs/changes/newsfragments/8387.improved new file mode 100644 index 00000000000..7d3920c0da1 --- /dev/null +++ b/docs/changes/newsfragments/8387.improved @@ -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 68895f9ccc4a85e0496556c9c385c68501039014 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 08:02:26 +0200 Subject: [PATCH 47/76] 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/8389.improved | 3 +++ .../DataSet/Offline Plotting Tutorial.ipynb | 18 ++++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 docs/changes/newsfragments/8389.improved diff --git a/docs/changes/newsfragments/8389.improved b/docs/changes/newsfragments/8389.improved new file mode 100644 index 00000000000..b2f6f471d3d --- /dev/null +++ b/docs/changes/newsfragments/8389.improved @@ -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 76ad021a450215c5a44e81c3421b6e7c97d6c9c1 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 08:07:51 +0200 Subject: [PATCH 48/76] 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/8390.improved | 5 +++++ src/qcodes/dataset/data_set_in_memory.py | 5 +++++ src/qcodes/dataset/data_set_protocol.py | 3 +++ tests/dataset/test_snapshot.py | 4 +--- 4 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 docs/changes/newsfragments/8390.improved diff --git a/docs/changes/newsfragments/8390.improved b/docs/changes/newsfragments/8390.improved new file mode 100644 index 00000000000..080e53462aa --- /dev/null +++ b/docs/changes/newsfragments/8390.improved @@ -0,0 +1,5 @@ +``snapshot_raw`` is now part of :class:`.DataSetProtocol` and is available on +:class:`.DataSetInMem` as well as on :class:`.DataSet`. It is documented as the +way to get the snapshot of a run as a JSON string, and is used as such in the +example notebooks, but it was only declared on one of the two dataset classes, +so reading it from a dataset returned by a measurement did not type check. diff --git a/src/qcodes/dataset/data_set_in_memory.py b/src/qcodes/dataset/data_set_in_memory.py index 241b789ba3b..5fe228c189f 100644 --- a/src/qcodes/dataset/data_set_in_memory.py +++ b/src/qcodes/dataset/data_set_in_memory.py @@ -595,6 +595,11 @@ def _snapshot_raw(self) -> str | None: """Snapshot of the run as a JSON-formatted string (or None).""" return self._snapshot_raw_data + @property + def snapshot_raw(self) -> str | None: + """Snapshot of the run as a JSON-formatted string (or None).""" + return self._snapshot_raw + def add_metadata(self, tag: str, metadata: Any) -> None: """ Adds metadata to the :class:`.DataSet`. diff --git a/src/qcodes/dataset/data_set_protocol.py b/src/qcodes/dataset/data_set_protocol.py index cd31082e880..339e11ab5a7 100644 --- a/src/qcodes/dataset/data_set_protocol.py +++ b/src/qcodes/dataset/data_set_protocol.py @@ -168,6 +168,9 @@ def add_snapshot(self, snapshot: str, overwrite: bool = False) -> None: ... @property def _snapshot_raw(self) -> str | None: ... + @property + def snapshot_raw(self) -> str | None: ... + def add_metadata(self, tag: str, metadata: Any) -> None: ... @property diff --git a/tests/dataset/test_snapshot.py b/tests/dataset/test_snapshot.py index b723033574e..1a72ec0c7e1 100644 --- a/tests/dataset/test_snapshot.py +++ b/tests/dataset/test_snapshot.py @@ -68,9 +68,7 @@ def test_station_snapshot_during_measurement( assert expected_snapshot == snapshot_from_dataset # 2. Test `snapshot_raw` property - # this is not part of the DatasetProtocol interface - # but we test it anyway - assert json_snapshot_from_dataset == data_saver.dataset.snapshot_raw # type: ignore[attr-defined] + assert json_snapshot_from_dataset == data_saver.dataset.snapshot_raw # 3. Test `snapshot` property From 989a50e64a04a09e04573bc64c202a87876fdb7b Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 25 Aug 2026 08:08:26 +0200 Subject: [PATCH 49/76] 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/8391.improved | 3 +++ docs/examples/DataSet/Working with snapshots.ipynb | 12 +++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 docs/changes/newsfragments/8391.improved diff --git a/docs/changes/newsfragments/8391.improved b/docs/changes/newsfragments/8391.improved new file mode 100644 index 00000000000..04a44aad1a7 --- /dev/null +++ b/docs/changes/newsfragments/8391.improved @@ -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 4ecac3dfad60073b737ee58f3c2511619902cf5b Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 27 Aug 2026 16:09:29 +0200 Subject: [PATCH 50/76] Use type safe dataset unpacking --- .../driver_examples/Qcodes example with AMI430.ipynb | 6 +++--- 1 file changed, 3 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..fc53d640775 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,8 @@ "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(sweep_1, sweep_2, dmm.v1, additional_setpoints=(time_p,), squeeze=False)\n", + " timed_datasets.append(dss[0])\n", " time.sleep(0.5)\n", " elapsed_time = time_p.get()" ] From 414111374557c952a0661b00e851e7ade0c68305 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 27 Aug 2026 16:25:05 +0200 Subject: [PATCH 51/76] Fix type checking in notebook --- ...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 d1405c3f72800093e9586dc44080aed89cfb7cb1 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 27 Aug 2026 16:32:20 +0200 Subject: [PATCH 52/76] Allow ParamSpecBase in get_parameter_data --- .../DataSet/Accessing-data-in-DataSet.ipynb | 24 ++----------------- src/qcodes/dataset/data_set.py | 2 +- src/qcodes/dataset/data_set_in_memory.py | 4 ++-- src/qcodes/dataset/data_set_protocol.py | 6 ++--- 4 files changed, 8 insertions(+), 28 deletions(-) diff --git a/docs/examples/DataSet/Accessing-data-in-DataSet.ipynb b/docs/examples/DataSet/Accessing-data-in-DataSet.ipynb index e3909e71bdc..15b33bcd9d9 100644 --- a/docs/examples/DataSet/Accessing-data-in-DataSet.ipynb +++ b/docs/examples/DataSet/Accessing-data-in-DataSet.ipynb @@ -710,26 +710,6 @@ "dataset.get_parameters()" ] }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'x,t,y,y2,q'" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "dataset.parameters" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -1047,7 +1027,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv", + "display_name": "qcodes", "language": "python", "name": "python3" }, @@ -1061,7 +1041,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.12" + "version": "3.14.7" }, "toc": { "base_numbering": 1, diff --git a/src/qcodes/dataset/data_set.py b/src/qcodes/dataset/data_set.py index 868eed6705d..96c88bad1d7 100644 --- a/src/qcodes/dataset/data_set.py +++ b/src/qcodes/dataset/data_set.py @@ -810,7 +810,7 @@ def _ensure_dataset_written(self) -> None: def get_parameter_data( self, - *params: str | ParamSpec | ParameterBase, + *params: str | ParamSpecBase | ParameterBase, start: int | None = None, end: int | None = None, callback: Callable[[float], None] | None = None, diff --git a/src/qcodes/dataset/data_set_in_memory.py b/src/qcodes/dataset/data_set_in_memory.py index 5fe228c189f..d42fac295d5 100644 --- a/src/qcodes/dataset/data_set_in_memory.py +++ b/src/qcodes/dataset/data_set_in_memory.py @@ -899,7 +899,7 @@ def to_pandas_dataframe( def get_parameter_data( self, - *params: str | ParamSpec | ParameterBase, + *params: str | ParamSpecBase | ParameterBase, start: int | None = None, end: int | None = None, callback: Callable[[float], None] | None = None, @@ -909,7 +909,7 @@ def get_parameter_data( @staticmethod def _warn_if_set( - *params: str | ParamSpec | ParameterBase, + *params: str | ParamSpecBase | ParameterBase, start: int | None = None, end: int | None, ) -> None: diff --git a/src/qcodes/dataset/data_set_protocol.py b/src/qcodes/dataset/data_set_protocol.py index 339e11ab5a7..62100c61703 100644 --- a/src/qcodes/dataset/data_set_protocol.py +++ b/src/qcodes/dataset/data_set_protocol.py @@ -207,7 +207,7 @@ def cache(self) -> DataSetCache[DataSetProtocol]: ... def get_parameter_data( self, - *params: str | ParamSpec | ParameterBase, + *params: str | ParamSpecBase | ParameterBase, start: int | None = None, end: int | None = None, callback: Callable[[float], None] | None = None, @@ -470,11 +470,11 @@ def _add_metadata_to_netcdf_if_nc_exported(self, tag: str, data: Any) -> None: ) @staticmethod - def _validate_parameters(*params: str | ParamSpec | ParameterBase) -> list[str]: + def _validate_parameters(*params: str | ParamSpecBase | ParameterBase) -> list[str]: """ Validate that the provided parameters have a name and return those names as a list. - The Parameters may be a mix of strings, ParamSpecs or ordinary + The Parameters may be a mix of strings, ParamSpecsBase or ordinary QCoDeS parameters. """ From 93a6d34bbb8ed45879a7fe0e9eb40ef91c0d426d Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 27 Aug 2026 16:34:48 +0200 Subject: [PATCH 53/76] Fix type checking in config notebook --- 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 01849552c76f98c145e8be8a294c3cf5bf73dc49 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 27 Aug 2026 16:37:12 +0200 Subject: [PATCH 54/76] Fix another notebook --- 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 e872aab7ba00c945bbdcda603c01b94fa6ca2fb5 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 27 Aug 2026 16:48:42 +0200 Subject: [PATCH 55/76] Fix type checking in parameter notebook --- ...nts-defined-on-a-different-instrument.ipynb | 18 +++++++++++++++--- ...ple-Example-of-ParameterWithSetpoints.ipynb | 16 +++++++++++++++- 2 files changed, 30 insertions(+), 4 deletions(-) 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 70d72c7c9346602394c0527c432b5e3ecf325708 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 27 Aug 2026 16:48:42 +0200 Subject: [PATCH 56/76] Fix type checking in parameter notebook --- .../DataSet/Threaded data acquisition.ipynb | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) 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", From 44923ece6475f7736de4a2b86e4eee2c64cbb975 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 28 Aug 2026 21:51:03 +0200 Subject: [PATCH 57/76] Fix ty typechecking of docs hack --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 9b78ed91a82..b4c9a301f9b 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 a0ee6dd1bc431d788383d7cc23f1cf0b54716389 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 28 Aug 2026 21:52:26 +0200 Subject: [PATCH 58/76] Fix remaining ty issues --- .../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..f600aca5494 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 ValueError(\"reloaded_ds must be an instance of DataSetInMem\")\n", + "\n", "reloaded_ds.set_netcdf_location(new_file_path)\n", "reloaded_ds.export_info" ] From 39a1a01b7edecd48f21034bd07c6c3c931328ff1 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 28 Aug 2026 22:06:27 +0200 Subject: [PATCH 59/76] 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/8392.improved | 5 +++++ src/qcodes/dataset/data_set_protocol.py | 1 + 2 files changed, 6 insertions(+) create mode 100644 docs/changes/newsfragments/8392.improved diff --git a/docs/changes/newsfragments/8392.improved b/docs/changes/newsfragments/8392.improved new file mode 100644 index 00000000000..3a3cbdae81b --- /dev/null +++ b/docs/changes/newsfragments/8392.improved @@ -0,0 +1,5 @@ +The type alias ``ValuesType``, used by ``DataSaver.add_result`` and related +methods, now also accepts a sequence of numpy arrays. This is what a +``MultiParameter`` measuring more than one array returns, so passing such a +result to ``DataSaver.add_result`` no longer produces a type checking error. +The behavior at runtime is unchanged since this was already supported. diff --git a/src/qcodes/dataset/data_set_protocol.py b/src/qcodes/dataset/data_set_protocol.py index 62100c61703..8e95a7d67e1 100644 --- a/src/qcodes/dataset/data_set_protocol.py +++ b/src/qcodes/dataset/data_set_protocol.py @@ -55,6 +55,7 @@ | npt.NDArray | Sequence[ScalarResTypes] | Sequence[Sequence[ScalarResTypes]] + | Sequence[npt.NDArray] ) type ResType = "tuple[ParameterBase | str, ValuesType]" type SetpointsType = "Sequence[str | ParameterBase]" From 03689766d54f20ab6eb7f05ffa0b3336d0cd0ad3 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 28 Aug 2026 22:07:09 +0200 Subject: [PATCH 60/76] Reformat file --- .../examples/driver_examples/Qcodes example with AMI430.ipynb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/examples/driver_examples/Qcodes example with AMI430.ipynb b/docs/examples/driver_examples/Qcodes example with AMI430.ipynb index fc53d640775..750ca3d212d 100644 --- a/docs/examples/driver_examples/Qcodes example with AMI430.ipynb +++ b/docs/examples/driver_examples/Qcodes example with AMI430.ipynb @@ -860,7 +860,9 @@ "time_p.reset_clock()\n", "\n", "while elapsed_time < 2:\n", - " dss, _, _ = dond(sweep_1, sweep_2, dmm.v1, additional_setpoints=(time_p,), squeeze=False)\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 c816beda329abb2a56a38577455d551d32dc3d42 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 28 Aug 2026 22:17:04 +0200 Subject: [PATCH 61/76] Run ty on src and docs in CI Add ty to the test extra and run it next to mypy and pyright. ty understands Jupyter notebooks so this covers the example notebooks in docs, which the other type checkers do not look at. The docs extra is now installed in that job since the notebooks import scipy, and the interpreter is passed explicitly because ty only auto discovers virtual environments. --- .github/workflows/pytest.yaml | 4 ++++ docs/changes/newsfragments/8393.underthehood | 4 ++++ pyproject.toml | 2 ++ requirements.txt | 2 ++ 4 files changed, 12 insertions(+) create mode 100644 docs/changes/newsfragments/8393.underthehood diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index c49f9286e62..e2337ce2fb3 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -103,6 +103,10 @@ jobs: run: mypy -p qcodes if: ${{ !matrix.min-version && always() }} id: mypy + - name: Run ty + run: ty check --output-format github src docs + 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/8393.underthehood b/docs/changes/newsfragments/8393.underthehood new file mode 100644 index 00000000000..782b81a23f2 --- /dev/null +++ b/docs/changes/newsfragments/8393.underthehood @@ -0,0 +1,4 @@ +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``. diff --git a/pyproject.toml b/pyproject.toml index 7b0055e36fd..37cf1b05947 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,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.75", "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 1f3e95d6555..d6b7a899fd5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -457,6 +457,8 @@ traitlets==5.16.1 # nbconvert # nbformat # nbsphinx +ty==0.0.75 + # via qcodes (pyproject.toml) types-jsonschema==4.26.0.20260518 # via qcodes (pyproject.toml) types-networkx==3.6.1.20260728 From 531515d2fb10c3536a99d7b722fd9752db55f3d8 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 07:45:00 +0200 Subject: [PATCH 62/76] Bump qcodes loop --- pyproject.toml | 4 ++-- requirements.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 37cf1b05947..80a74ec96a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ Changelog = "https://microsoft.github.io/Qcodes/changes/index.html" [project.optional-dependencies] zurichinstruments = ["zhinst-qcodes>=0.3"] minicircuits_usb_spdt = ["pythonnet>3.0.4"] -loop = ["qcodes_loop>=0.1.2"] +loop = ["qcodes_loop>=0.2.3"] test = [ "coverage[toml]>=7.10.5", "deepdiff>=8.6.1", @@ -86,7 +86,7 @@ test = [ "types-tabulate>=0.1.0", "types-tqdm>=4.64.6", "types_pywin32>=305.0.0.7", - "qcodes_loop>=0.1.1", + "qcodes_loop>=0.2.3", "zhinst.qcodes>=0.5", # typecheck zhinst driver alias "libcst>=1.2.0", # refactor tests ] diff --git a/requirements.txt b/requirements.txt index d6b7a899fd5..fe44fea7370 100644 --- a/requirements.txt +++ b/requirements.txt @@ -351,7 +351,7 @@ pyzmq==27.2.0 # via # ipykernel # jupyter-client -qcodes-loop==0.2.2 +qcodes-loop==0.2.3 # via qcodes (pyproject.toml) referencing==0.37.0 # via From d6ab3549fddf49d8cb74033bfee986e5f9e3de3e Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:06:22 +0200 Subject: [PATCH 63/76] Add intersphinx for testing --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 80a74ec96a0..2555ce08ea2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,6 +89,7 @@ test = [ "qcodes_loop>=0.2.3", "zhinst.qcodes>=0.5", # typecheck zhinst driver alias "libcst>=1.2.0", # refactor tests + "intersphinx_registry>=0.2603.16" # typecheck docs/conf.py ] docs = [ "autodocsumm>=0.2.9", From e34b92eb4a4430cd38fc081863bf9c95f6d4e01e Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:10:44 +0200 Subject: [PATCH 64/76] 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_connection.py | 4 +-- tests/dataset/test_string_data.py | 4 +-- .../b1500_driver_tests/test_b1517a_smu.py | 4 +-- tests/drivers/test_MercuryiPS.py | 2 +- tests/drivers/test_ami430_visa.py | 4 +-- tests/drivers/test_lakeshore_372.py | 10 +++--- 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_metadata.py | 4 +-- tests/test_station.py | 6 ++-- tests/utils/test_class_strings.py | 2 +- tests/utils/test_isfunction.py | 2 +- tests/validators/test_arrays.py | 10 +++--- tests/validators/test_basic.py | 4 +-- tests/validators/test_bool.py | 2 +- tests/validators/test_callable.py | 2 +- tests/validators/test_complex.py | 2 +- tests/validators/test_dict.py | 2 +- tests/validators/test_enum.py | 2 +- tests/validators/test_ints.py | 14 ++++---- tests/validators/test_lists.py | 4 +-- tests/validators/test_multi_type.py | 6 ++-- tests/validators/test_multi_type_and.py | 2 +- tests/validators/test_multi_type_or.py | 2 +- tests/validators/test_multiples.py | 4 +-- tests/validators/test_numbers.py | 2 +- tests/validators/test_sequence.py | 2 +- tests/validators/test_string.py | 16 +++++----- 61 files changed, 180 insertions(+), 180 deletions(-) diff --git a/tests/dataset/measurement/test_measurement_context_manager.py b/tests/dataset/measurement/test_measurement_context_manager.py index 412cf89c33a..cd76b48e133 100644 --- a/tests/dataset/measurement/test_measurement_context_manager.py +++ b/tests/dataset/measurement/test_measurement_context_manager.py @@ -85,10 +85,10 @@ def test_register_parameter_arg_types(DAC, DMM): meas.register_parameter(DMM.v1, setpoints="foo") with pytest.raises(TypeError): - meas.register_parameter(DMM.v1, basis=(DAC.ch1, 3)) # type: ignore[arg-type] + meas.register_parameter(DMM.v1, basis=(DAC.ch1, 3)) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises(TypeError): - meas.register_parameter(DMM.v1, setpoints=(DAC.ch1, 3)) # type: ignore[arg-type] + meas.register_parameter(DMM.v1, setpoints=(DAC.ch1, 3)) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_register_parameter_numbers(DAC, DMM) -> None: @@ -102,7 +102,7 @@ def test_register_parameter_numbers(DAC, DMM) -> None: for not_a_parameter in not_parameters: with pytest.raises(ValueError): - meas.register_parameter(not_a_parameter) # type: ignore[arg-type] + meas.register_parameter(not_a_parameter) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] my_param = DAC.ch1 meas.register_parameter(my_param) @@ -979,7 +979,7 @@ def test_datasaver_foul_input(bg_writing) -> None: with meas.run(bg_writing) as datasaver: for ft in foul_stuff: with pytest.raises(ValueError): - datasaver.add_result(("foul", ft)) # type: ignore[arg-type] + datasaver.add_result(("foul", ft)) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] @settings(max_examples=10, deadline=None) diff --git a/tests/dataset/test_converters.py b/tests/dataset/test_converters.py index a832f736963..3b1f397280b 100644 --- a/tests/dataset/test_converters.py +++ b/tests/dataset/test_converters.py @@ -164,11 +164,11 @@ def test_construct_current_rundescriber_from_fake_v4(some_interdeps) -> None: version=4, shapes=None, ) - v4["foobar"] = {"foo": ["bar"]} # type: ignore[typeddict-unknown-key] + v4["foobar"] = {"foo": ["bar"]} # type: ignore[typeddict-unknown-key] # ty: ignore[invalid-key] rds1 = RunDescriber._from_dict(v4) rds_upgraded = from_dict_to_current(v4) v3 = v4.copy() - v3.pop("foobar") # type: ignore[typeddict-item] + v3.pop("foobar") # type: ignore[typeddict-item] # ty: ignore[invalid-key] v3["version"] = 3 assert rds1._to_dict() == v3 assert rds_upgraded._to_dict() == v3 diff --git a/tests/dataset/test_datasaver.py b/tests/dataset/test_datasaver.py index 1a4f965adec..5545d98dc62 100644 --- a/tests/dataset/test_datasaver.py +++ b/tests/dataset/test_datasaver.py @@ -168,7 +168,7 @@ def test_saving_numeric_values_as_text(numeric_type, bg_writing) -> None: data_saver.add_result((p.name, value)) finally: data_saver.dataset.mark_completed() - data_saver.dataset.conn.close() # type: ignore[attr-defined] + data_saver.dataset.conn.close() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] @pytest.mark.usefixtures("experiment") @@ -200,4 +200,4 @@ def test_duplicated_parameter_raises() -> None: data_saver.add_result((p.name, 1), (p.name, 1)) finally: data_saver.dataset.mark_completed() - data_saver.dataset.conn.close() # type: ignore[attr-defined] + data_saver.dataset.conn.close() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] diff --git a/tests/dataset/test_dataset_basic.py b/tests/dataset/test_dataset_basic.py index d0ab9cf21eb..5f77c4573e6 100644 --- a/tests/dataset/test_dataset_basic.py +++ b/tests/dataset/test_dataset_basic.py @@ -312,7 +312,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) @@ -809,7 +809,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 c4deebc3223..06511f1fcb7 100644 --- a/tests/dataset/test_dependencies.py +++ b/tests/dataset/test_dependencies.py @@ -24,7 +24,7 @@ def test_wrong_input_raises() -> None: ["p1", ParamSpec("p2", paramtype="text")], ): with pytest.raises(ValueError): - InterDependencies(pspecs) # type: ignore[arg-type] + InterDependencies(pspecs) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_init(some_paramspecbases) -> None: @@ -78,18 +78,18 @@ def test_init_validation_raises(some_paramspecbases) -> None: for tree, cause in zip(invalid_trees, causes): with pytest.raises(ValueError, match="Invalid dependencies") as ei: - InterDependencies_(dependencies=tree, inferences={}) # type: ignore[arg-type] + InterDependencies_(dependencies=tree, inferences={}) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert error_caused_by(ei, cause=cause) for tree, cause in zip(invalid_trees, causes): with pytest.raises(ValueError, match="Invalid inferences") as ei: - InterDependencies_(dependencies={}, inferences=tree) # type: ignore[arg-type] + InterDependencies_(dependencies={}, inferences=tree) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert error_caused_by(ei, cause=cause) with pytest.raises(ValueError, match="Invalid standalones") as ei: - InterDependencies_(standalones=("ps1", "ps2")) # type: ignore[arg-type] + InterDependencies_(standalones=("ps1", "ps2")) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert error_caused_by(ei, cause="Standalones must be a sequence of ParamSpecs") @@ -101,8 +101,8 @@ def test_init_validation_raises(some_paramspecbases) -> None: for inv in invalid_trees_2: with pytest.raises(ValueError, match="already exists"): InterDependencies_( - dependencies=inv["deps"], # type: ignore[arg-type] - inferences=inv["inffs"], # type: ignore[arg-type] + dependencies=inv["deps"], # type: ignore[arg-type] # ty: ignore[invalid-argument-type] + inferences=inv["inffs"], # type: ignore[arg-type] # ty: ignore[invalid-argument-type] ) diff --git a/tests/dataset/test_descriptions.py b/tests/dataset/test_descriptions.py index 248c52ebfc9..ec03c7cfa01 100644 --- a/tests/dataset/test_descriptions.py +++ b/tests/dataset/test_descriptions.py @@ -12,7 +12,7 @@ def test_wrong_input_type_raises() -> None: for interdeps in ("interdeps", ["p1", "p2"], 0): with pytest.raises(ValueError): - RunDescriber(interdeps=interdeps) # type: ignore[arg-type] + RunDescriber(interdeps=interdeps) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_equality(some_paramspecbases) -> None: diff --git a/tests/dataset/test_parameter_with_setpoints_has_control.py b/tests/dataset/test_parameter_with_setpoints_has_control.py index 08e641a89bd..23abb9132f4 100644 --- a/tests/dataset/test_parameter_with_setpoints_has_control.py +++ b/tests/dataset/test_parameter_with_setpoints_has_control.py @@ -29,7 +29,7 @@ def unpack_self(self, value): # type: ignore[override] res.append((controlled, controlled())) return res - p = _ControlledSetpoints(name, **kwargs) # type: ignore[arg-type] + p = _ControlledSetpoints(name, **kwargs) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] p.has_control_of.add(controlled) return p diff --git a/tests/dataset/test_paramspec.py b/tests/dataset/test_paramspec.py index ed344a01203..fea4b0e957f 100644 --- a/tests/dataset/test_paramspec.py +++ b/tests/dataset/test_paramspec.py @@ -107,7 +107,7 @@ def test_creation(name, sp1, sp2, inff1, inff2, paramtype) -> None: invalid_types = ("np.array", "ndarray", "lala", "", Number, ndarray, 0, None) for inv_type in invalid_types: with pytest.raises(ValueError): - ParamSpec(name, inv_type) # type: ignore[arg-type] + ParamSpec(name, inv_type) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] if not inff1.isidentifier(): inff1 = "inff1" diff --git a/tests/dataset/test_snapshot.py b/tests/dataset/test_snapshot.py index 1a72ec0c7e1..058bcb92754 100644 --- a/tests/dataset/test_snapshot.py +++ b/tests/dataset/test_snapshot.py @@ -58,7 +58,7 @@ def test_station_snapshot_during_measurement( # 1. Test `get_metadata('snapshot')` method # this is not part of the DatasetProtocol interface # but we test it anyway - json_snapshot_from_dataset = data_saver.dataset.get_metadata("snapshot") # type: ignore[attr-defined] + json_snapshot_from_dataset = data_saver.dataset.get_metadata("snapshot") # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] snapshot_from_dataset = json.loads(json_snapshot_from_dataset) expected_snapshot = { diff --git a/tests/dataset/test_sqlite_connection.py b/tests/dataset/test_sqlite_connection.py index 62eb6e33b4b..30b0ef2a350 100644 --- a/tests/dataset/test_sqlite_connection.py +++ b/tests/dataset/test_sqlite_connection.py @@ -58,7 +58,7 @@ def test_atomic_raises_for_non_atomic_conn() -> None: "atomic context manager only accepts AtomicConnection " "database connection objects." ) - with pytest.raises(ValueError, match=match_str), atomic(sqlite_conn): # type: ignore[arg-type] + with pytest.raises(ValueError, match=match_str), atomic(sqlite_conn): # type: ignore[arg-type] # ty: ignore[invalid-argument-type] pass @@ -295,7 +295,7 @@ def test_atomic_transaction_on_sqlite3_connection_raises(tmp_path) -> None: ) with pytest.raises(ValueError, match=match_str): - atomic_transaction(conn, "whatever sql query") # type: ignore[arg-type] + atomic_transaction(conn, "whatever sql query") # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_connect() -> None: diff --git a/tests/dataset/test_string_data.py b/tests/dataset/test_string_data.py index 928ffbd7069..41cc2837502 100644 --- a/tests/dataset/test_string_data.py +++ b/tests/dataset/test_string_data.py @@ -138,7 +138,7 @@ def test_string_with_wrong_paramtype_via_datasaver() -> None: with pytest.raises(ValueError, match=msg): data_saver.add_result(("p", "some text")) finally: - data_saver.dataset.conn.close() # type: ignore[attr-defined] + data_saver.dataset.conn.close() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] @pytest.mark.usefixtures("experiment") @@ -194,7 +194,7 @@ def test_list_of_strings(experiment) -> None: try: np.testing.assert_array_equal(actual_data, expec_data) finally: - test_set.conn.close() # type: ignore[attr-defined] + test_set.conn.close() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] @settings(suppress_health_check=(HealthCheck.function_scoped_fixture,), deadline=None) diff --git a/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1517a_smu.py b/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1517a_smu.py index b66a514cceb..6555efdb002 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_MercuryiPS.py b/tests/drivers/test_MercuryiPS.py index f6fada36f90..3b6d94d9fad 100644 --- a/tests/drivers/test_MercuryiPS.py +++ b/tests/drivers/test_MercuryiPS.py @@ -92,7 +92,7 @@ def test_wrong_field_limit_raises() -> None: "mips", address="GPIB::1::INSTR", pyvisa_sim_file="MercuryiPS.yaml", - field_limits=0, # type: ignore[arg-type] + field_limits=0, # type: ignore[arg-type] # ty: ignore[invalid-argument-type] ) diff --git a/tests/drivers/test_ami430_visa.py b/tests/drivers/test_ami430_visa.py index 596c6a80da0..dbbca49f915 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, ) @@ -1867,7 +1867,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 b6085bc3b5f..739b42d0944 100644 --- a/tests/drivers/test_lakeshore_372.py +++ b/tests/drivers/test_lakeshore_372.py @@ -36,7 +36,7 @@ class MockVisaInstrument: def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - self.visa_log = get_instrument_logger(self, VISA_LOGGER) # type: ignore[arg-type] + self.visa_log = get_instrument_logger(self, VISA_LOGGER) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] # This base class mixin holds two dictionaries associated with the # pyvisa_instrument.write() @@ -74,7 +74,7 @@ def write_raw(self, cmd) -> None: self.visa_log.debug(f"Query: {cmd} for command {cmd_str} with args {args}") self.cmds[cmd_str](args) else: - super().write_raw(cmd) # type: ignore[misc] + super().write_raw(cmd) # type: ignore[misc] # ty: ignore[unresolved-attribute] def ask_raw(self, cmd) -> Any: query_parts = cmd.split(" ") @@ -88,12 +88,12 @@ def ask_raw(self, cmd) -> Any: self.visa_log.debug(f"Response: {response}") return response else: - return super().ask_raw(cmd) # type: ignore[misc] + return super().ask_raw(cmd) # type: ignore[misc] # ty: ignore[unresolved-attribute] def query(name: str) -> Callable[[Callable[P, T]], Callable[P, T]]: def wrapper(func: Callable[P, T]) -> Callable[P, T]: - func.query_name = name.upper() # type: ignore[attr-defined] + func.query_name = name.upper() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] return func return wrapper @@ -101,7 +101,7 @@ def wrapper(func: Callable[P, T]) -> Callable[P, T]: def command(name: str) -> Callable[[Callable[P, T]], Callable[P, T]]: def wrapper(func: Callable[P, T]) -> Callable[P, T]: - func.command_name = name.upper() # type: ignore[attr-defined] + func.command_name = name.upper() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] return func return wrapper diff --git a/tests/drivers/test_tektronix_AWG70000A.py b/tests/drivers/test_tektronix_AWG70000A.py index 99c955aefaf..badaceb8222 100644 --- a/tests/drivers/test_tektronix_AWG70000A.py +++ b/tests/drivers/test_tektronix_AWG70000A.py @@ -175,7 +175,7 @@ def test_seqxfilefromfs_failing(forged_sequence) -> None: forged_sequence, [1, 1, 1], seqname="dummyname", - channel_mapping={1: None, 3: None}, # type: ignore[dict-item] + channel_mapping={1: None, 3: None}, # type: ignore[dict-item] # ty: ignore[invalid-argument-type] ) # wrong channel mapping values diff --git a/tests/helpers/test_delegate_attribues.py b/tests/helpers/test_delegate_attribues.py index 69d54084cf1..300c4871054 100644 --- a/tests/helpers/test_delegate_attribues.py +++ b/tests/helpers/test_delegate_attribues.py @@ -20,7 +20,7 @@ class ToDict(DelegateAttributes): assert td.apples == "green" d = {"apples": "red", "oranges": "orange"} - td.d = d # type: ignore[attr-defined] + td.d = d # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] # you can get the whole dict still assert td.d == d @@ -52,7 +52,7 @@ class ToDicts(DelegateAttributes): td = ToDicts() e = {"cats": 12, "dogs": 3} - td.e = e # type: ignore[attr-defined] + td.e = e # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] # you can still access the second one when the first doesn't exist with pytest.raises(AttributeError): @@ -61,7 +61,7 @@ class ToDicts(DelegateAttributes): assert td.cats == 12 # the first beats out the second - td.d = {"cats": 42, "chickens": 1000} # type: ignore[attr-defined] + td.d = {"cats": 42, "chickens": 1000} # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert td.cats == 42 # but you can still access things only in the second @@ -89,14 +89,14 @@ class ToObject(DelegateAttributes): _ = to_obj.recipient assert to_obj.gray == "#888" - to_obj.recipient = recipient # type: ignore[attr-defined] + to_obj.recipient = recipient # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] # now you can access recipient through to_obj assert to_obj.black == "#000" # to_obj overrides but you can still access other recipient attributes # "soft" black - to_obj.black = "#444" # type: ignore[attr-defined] + to_obj.black = "#444" # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert to_obj.black == "#444" assert to_obj.white == "#fff" diff --git a/tests/helpers/test_strip_attrs.py b/tests/helpers/test_strip_attrs.py index 5dc8e39ddbd..35f8c6cf4d9 100644 --- a/tests/helpers/test_strip_attrs.py +++ b/tests/helpers/test_strip_attrs.py @@ -23,7 +23,7 @@ def __delitem__(self, item): def test_normal() -> None: a = A() a.x = 15 - a.z = 25 # type: ignore[attr-defined] + a.z = 25 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] strip_attrs(a) @@ -38,13 +38,13 @@ def test_pathological() -> None: a = A() a.__dict__ = BadKeysDict() - a.fruit = "mango" # type: ignore[attr-defined] + a.fruit = "mango" # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] with pytest.raises(RuntimeError): a.__dict__.keys() strip_attrs(a) # no error, but the attribute is still there - assert a.fruit == "mango" # type: ignore[attr-defined] + assert a.fruit == "mango" # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] a = A() a.__dict__ = NoDelDict() diff --git a/tests/parameter/test_array_parameter.py b/tests/parameter/test_array_parameter.py index 8d9ff7cc75e..5f6a0831d3b 100644 --- a/tests/parameter/test_array_parameter.py +++ b/tests/parameter/test_array_parameter.py @@ -167,13 +167,13 @@ def test_full_name() -> None: # this is not allowed since instrument # here is not actually an instrument # but useful for testing - p._instrument = instrument # type: ignore[assignment] + p._instrument = instrument # type: ignore[assignment] # ty: ignore[invalid-assignment] assert str(p) == "fred" assert p.setpoint_full_names == ("barney",) # and then an instrument that really has a name p = SimpleArrayParam([6, 7], "wilma", shape=(2,), setpoint_names=("betty",)) - p._instrument = named_instrument # type: ignore[assignment] + p._instrument = named_instrument # type: ignore[assignment] # ty: ignore[invalid-assignment] assert str(p) == "astro_wilma" assert p.setpoint_full_names == ("astro_betty",) @@ -181,7 +181,7 @@ def test_full_name() -> None: p = SimpleArrayParam( [[6, 7, 8], [1, 2, 3]], "wilma", shape=(3, 2), setpoint_names=("betty", None) ) - p._instrument = named_instrument # type: ignore[assignment] + p._instrument = named_instrument # type: ignore[assignment] # ty: ignore[invalid-assignment] assert p.setpoint_full_names == ("astro_betty", None) diff --git a/tests/parameter/test_delegate_parameter.py b/tests/parameter/test_delegate_parameter.py index 55278f3f9aa..e9aca5043e0 100644 --- a/tests/parameter/test_delegate_parameter.py +++ b/tests/parameter/test_delegate_parameter.py @@ -84,7 +84,7 @@ def get_cmd() -> Any: get_cmd=get_cmd, ) param = cast("ObservableParam", p) - param.get_instr_val = get_cmd # type: ignore[method-assign] + param.get_instr_val = get_cmd # type: ignore[method-assign] # ty: ignore[invalid-assignment] return param yield make_parameter @@ -142,7 +142,7 @@ def test_get_set_raises(simple_param: Parameter) -> None: """ for kwargs in ({"set_cmd": None}, {"get_cmd": None}): with pytest.raises(KeyError) as e: - DelegateParameter("test_delegate_parameter", source=simple_param, **kwargs) # type: ignore[arg-type] + DelegateParameter("test_delegate_parameter", source=simple_param, **kwargs) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert str(e.value).startswith("'It is not allowed to set") @@ -320,7 +320,7 @@ def test_delegate_parameter_get_and_snapshot_with_none_source() -> None: assert delegate_param.get() is None assert delegate_param.snapshot()["value"] is None - parameter = delegate_param.cache._parameter # type: ignore[attr-defined] + parameter = delegate_param.cache._parameter # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert parameter.source.cache is none_param.cache delegate_param.source = source_param assert delegate_param.get() == 1 diff --git a/tests/parameter/test_elapsed_time_parameter.py b/tests/parameter/test_elapsed_time_parameter.py index 3add38e4515..f5dc73174f3 100644 --- a/tests/parameter/test_elapsed_time_parameter.py +++ b/tests/parameter/test_elapsed_time_parameter.py @@ -50,4 +50,4 @@ def test_elapsed_time_parameter_forbidden_kwargs() -> None: for fb_kwarg in forbidden_kwargs: match = f'Can not set "{fb_kwarg}" for an ElapsedTimeParameter' with pytest.raises(ValueError, match=match): - ElapsedTimeParameter("time", **{fb_kwarg: None}) # type: ignore[arg-type] + ElapsedTimeParameter("time", **{fb_kwarg: None}) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] diff --git a/tests/parameter/test_get_latest.py b/tests/parameter/test_get_latest.py index 856f7443933..578b1088203 100644 --- a/tests/parameter/test_get_latest.py +++ b/tests/parameter/test_get_latest.py @@ -64,8 +64,8 @@ def test_get_latest_unknown() -> None: local_parameter = BetterGettableParam("test_param", set_cmd=None, get_cmd=None) # fake a parameter that has a value but never been get/set to mock # an instrument. - local_parameter.cache._value = value # type: ignore[attr-defined] - local_parameter.cache._raw_value = value # type: ignore[attr-defined] + local_parameter.cache._value = value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] + local_parameter.cache._raw_value = value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert local_parameter.get_latest.get_timestamp() is None before_get = datetime.now(UTC) assert local_parameter._get_count == 0 @@ -169,7 +169,7 @@ def test_no_get_max_val_age() -> None: class LocalParameter(ParameterBase): def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) - self.set_raw = lambda x: x # type: ignore[method-assign] + self.set_raw = lambda x: x # type: ignore[method-assign] # ty: ignore[invalid-assignment] self.set = self._wrap_set(self.set_raw) localparameter = LocalParameter("test_param", instrument=None, max_val_age=1) diff --git a/tests/parameter/test_get_set_wrapping.py b/tests/parameter/test_get_set_wrapping.py index 3bbe3416d5b..7938d72c0d7 100644 --- a/tests/parameter/test_get_set_wrapping.py +++ b/tests/parameter/test_get_set_wrapping.py @@ -69,7 +69,7 @@ def test_gettable_settable_attributes_with_get_set_raw( """Test that parameters that have get_raw,set_raw are listed as gettable/settable and reverse.""" - class GetSetParam(baseclass): # type: ignore[valid-type,misc] + class GetSetParam(baseclass): # type: ignore[valid-type,misc] # ty: ignore[unsupported-base] def __init__(self, *args: Any, initial_value: Any = None, **kwargs: Any): self._value = initial_value super().__init__(*args, **kwargs) diff --git a/tests/parameter/test_group_parameter.py b/tests/parameter/test_group_parameter.py index 9464b031e1c..931225489b7 100644 --- a/tests/parameter/test_group_parameter.py +++ b/tests/parameter/test_group_parameter.py @@ -112,7 +112,7 @@ def test_raise_on_get_set_cmd() -> None: kwarg = {arg: ""} with pytest.raises(ValueError) as e: - GroupParameter(name="a", **kwarg) # type: ignore[arg-type] + GroupParameter(name="a", **kwarg) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert ( str(e.value) == "A GroupParameter does not use 'set_cmd' or 'get_cmd' kwarg" diff --git a/tests/parameter/test_issequenceof.py b/tests/parameter/test_issequenceof.py index 7c710473dfe..12ea03866ac 100644 --- a/tests/parameter/test_issequenceof.py +++ b/tests/parameter/test_issequenceof.py @@ -36,9 +36,9 @@ def test_simple_bad(args) -> None: def test_examples_raises() -> None: with pytest.raises(TypeError): - is_sequence_of([1], 1) # type: ignore[arg-type] + is_sequence_of([1], 1) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises(TypeError): - is_sequence_of([1], (1, 2)) # type: ignore[arg-type] + is_sequence_of([1], (1, 2)) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] good_depth = [ diff --git a/tests/parameter/test_keyword_only_args.py b/tests/parameter/test_keyword_only_args.py index 3ae82f64cdb..3927d1962b6 100644 --- a/tests/parameter/test_keyword_only_args.py +++ b/tests/parameter/test_keyword_only_args.py @@ -42,7 +42,7 @@ def test_keyword_args_work(self) -> None: def test_positional_args_rejected(self) -> None: with pytest.raises(TypeError): - _ConcreteParameterBase("test", None) # type: ignore[misc] + _ConcreteParameterBase("test", None) # type: ignore[misc] # ty: ignore[too-many-positional-arguments] class TestParameterKeywordOnly: @@ -55,7 +55,7 @@ def test_keyword_args_work(self) -> None: def test_positional_args_rejected(self) -> None: with pytest.raises(TypeError): - Parameter("test", None, set_cmd=None) # type: ignore[misc] + Parameter("test", None, set_cmd=None) # type: ignore[misc] # ty: ignore[too-many-positional-arguments] # Minimal concrete subclass of ArrayParameter for testing @@ -74,11 +74,11 @@ def test_keyword_args_work(self) -> None: def test_positional_args_rejected(self) -> None: with pytest.raises(TypeError): - _ConcreteArrayParameter("test", (3,)) # type: ignore[misc] + _ConcreteArrayParameter("test", (3,)) # type: ignore[misc] # ty: ignore[missing-argument, too-many-positional-arguments] def test_missing_shape_raises(self) -> None: with pytest.raises(TypeError): - _ConcreteArrayParameter("test") # type: ignore[misc] + _ConcreteArrayParameter("test") # type: ignore[misc] # ty: ignore[missing-argument] class TestGroupParameterKeywordOnly: @@ -90,7 +90,7 @@ def test_keyword_args_work(self) -> None: def test_positional_args_rejected(self) -> None: with pytest.raises(TypeError): - GroupParameter("test", None) # type: ignore[misc] + GroupParameter("test", None) # type: ignore[misc] # ty: ignore[too-many-positional-arguments] def _make_delegate_group() -> DelegateGroup: @@ -112,11 +112,11 @@ def test_keyword_args_work(self) -> None: def test_positional_args_rejected(self) -> None: grp = _make_delegate_group() with pytest.raises(TypeError): - GroupedParameter("test", grp) # type: ignore[misc] + GroupedParameter("test", grp) # type: ignore[misc] # ty: ignore[missing-argument, too-many-positional-arguments] def test_missing_group_raises(self) -> None: with pytest.raises(TypeError): - GroupedParameter("test") # type: ignore[misc] + GroupedParameter("test") # type: ignore[misc] # ty: ignore[missing-argument] # Minimal concrete subclass of MultiParameter for testing @@ -135,15 +135,15 @@ def test_keyword_args_work(self) -> None: def test_positional_args_rejected(self) -> None: with pytest.raises(TypeError): - _ConcreteMultiParameter("test", ("a",), ((),)) # type: ignore[misc] + _ConcreteMultiParameter("test", ("a",), ((),)) # type: ignore[misc] # ty: ignore[missing-argument, too-many-positional-arguments] def test_missing_names_raises(self) -> None: with pytest.raises(TypeError): - _ConcreteMultiParameter("test", shapes=((),)) # type: ignore[misc] + _ConcreteMultiParameter("test", shapes=((),)) # type: ignore[misc] # ty: ignore[missing-argument] def test_missing_shapes_raises(self) -> None: with pytest.raises(TypeError): - _ConcreteMultiParameter("test", names=("a",)) # type: ignore[misc] + _ConcreteMultiParameter("test", names=("a",)) # type: ignore[misc] # ty: ignore[missing-argument] class TestMultiChannelInstrumentParameterKeywordOnly: @@ -167,13 +167,13 @@ def test_positional_args_rejected(self) -> None: def test_missing_channels_raises(self) -> None: with pytest.raises(TypeError): - MultiChannelInstrumentParameter( # type: ignore[call-arg] + MultiChannelInstrumentParameter( # type: ignore[call-arg] # ty: ignore[missing-argument] param_name="x", name="test", names=("a",), shapes=((),) ) def test_missing_param_name_raises(self) -> None: with pytest.raises(TypeError): - MultiChannelInstrumentParameter( # type: ignore[call-arg] + MultiChannelInstrumentParameter( # type: ignore[call-arg] # ty: ignore[missing-argument] channels=[], name="test", names=("a",), shapes=((),) ) @@ -188,7 +188,7 @@ def test_keyword_args_work(self) -> None: def test_positional_args_rejected(self) -> None: with pytest.raises(TypeError): - ElapsedTimeParameter("test", "My label") # type: ignore[misc] + ElapsedTimeParameter("test", "My label") # type: ignore[misc] # ty: ignore[too-many-positional-arguments] class TestInstrumentRefParameterKeywordOnly: @@ -201,4 +201,4 @@ def test_keyword_args_work(self) -> None: def test_positional_args_rejected(self) -> None: with pytest.raises(TypeError): - InstrumentRefParameter("test", None) # type: ignore[misc] + InstrumentRefParameter("test", None) # type: ignore[misc] # ty: ignore[too-many-positional-arguments] diff --git a/tests/parameter/test_manual_parameter.py b/tests/parameter/test_manual_parameter.py index f1e21c50543..c26d3da586a 100644 --- a/tests/parameter/test_manual_parameter.py +++ b/tests/parameter/test_manual_parameter.py @@ -27,4 +27,4 @@ def test_manual_parameter_forbidden_kwargs() -> None: for fk in forbidden_kwargs: match = f'It is not allowed to set "{fk}" for a ManualParameter' with pytest.raises(ValueError, match=match): - ManualParameter("test", **{fk: None}) # type: ignore[arg-type] + ManualParameter("test", **{fk: None}) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] diff --git a/tests/parameter/test_multi_parameter.py b/tests/parameter/test_multi_parameter.py index 5e60778a965..fd5580bc0ae 100644 --- a/tests/parameter/test_multi_parameter.py +++ b/tests/parameter/test_multi_parameter.py @@ -211,7 +211,7 @@ def test_full_name_s() -> None: shapes=shapes, setpoint_names=setpoint_names, ) - p._instrument = instrument # type: ignore[assignment] + p._instrument = instrument # type: ignore[assignment] # ty: ignore[invalid-assignment] assert str(p) == name assert p.full_names == names assert p.setpoint_full_names == ( @@ -228,7 +228,7 @@ def test_full_name_s() -> None: shapes=shapes, setpoint_names=setpoint_names, ) - p._instrument = named_instrument # type: ignore[assignment] + p._instrument = named_instrument # type: ignore[assignment] # ty: ignore[invalid-assignment] assert str(p) == "astro_mixed_dimensions" assert p.full_names == ("astro_0D", "astro_1D", "astro_2D") diff --git a/tests/parameter/test_on_off_mapping.py b/tests/parameter/test_on_off_mapping.py index 2bb4df4f8ed..a9839576cbe 100644 --- a/tests/parameter/test_on_off_mapping.py +++ b/tests/parameter/test_on_off_mapping.py @@ -37,14 +37,14 @@ def test_create_on_off_val_mapping_for( # this does not type check. However, hash(1) == hash(True) # so 1/0 behaves like True and False at runtime - assert val_mapping[1] is on_val # type: ignore[index] + assert val_mapping[1] is on_val # type: ignore[index] # ty: ignore[invalid-argument-type] assert val_mapping[True] is on_val assert val_mapping["1"] is on_val assert val_mapping["ON"] is on_val assert val_mapping["On"] is on_val assert val_mapping["on"] is on_val - assert val_mapping[0] is off_val # type: ignore[index] + assert val_mapping[0] is off_val # type: ignore[index] # ty: ignore[invalid-argument-type] assert val_mapping[False] is off_val assert val_mapping["0"] is off_val assert val_mapping["OFF"] is off_val diff --git a/tests/parameter/test_parameter_basics.py b/tests/parameter/test_parameter_basics.py index 18a5446e137..aa2356f049e 100644 --- a/tests/parameter/test_parameter_basics.py +++ b/tests/parameter/test_parameter_basics.py @@ -15,7 +15,7 @@ def test_no_name() -> None: with pytest.raises(TypeError): - Parameter() # type: ignore[call-arg] + Parameter() # type: ignore[call-arg] # ty: ignore[missing-argument] def test_default_attributes() -> None: @@ -155,12 +155,12 @@ def test_str_representation() -> None: # three cases where only name gets used for full_name for instrument in blank_instruments: p = Parameter(name="fred") - p._instrument = instrument # type: ignore[assignment] + p._instrument = instrument # type: ignore[assignment] # ty: ignore[invalid-assignment] assert str(p) == "fred" # and finally an instrument that really has a name p = Parameter(name="wilma") - p._instrument = named_instrument # type: ignore[assignment] + p._instrument = named_instrument # type: ignore[assignment] # ty: ignore[invalid-assignment] assert str(p) == "astro_wilma" @@ -196,14 +196,14 @@ def test_parameter_call() -> None: assert p() == 1 with pytest.raises(TypeError, match="takes 1 positional argument but 2 were given"): - p(1, 2) # type: ignore[call-overload] + p(1, 2) # type: ignore[call-overload] # ty: ignore[no-matching-overload] p(value=2) assert p() == 2 with pytest.raises(TypeError, match="got multiple values for argument"): - p(2, value=2) # type: ignore[call-overload] + p(2, value=2) # type: ignore[call-overload] # ty: ignore[no-matching-overload] def test_parameter_set_extra_kwargs() -> None: @@ -243,13 +243,13 @@ def test_unknown_args_to_baseparameter_raises() -> None: _ = ParameterBase( name="Foo", instrument=None, - snapshotable=False, # type: ignore[call-arg] + snapshotable=False, # type: ignore[call-arg] # ty: ignore[unknown-argument] ) def test_underlying_instrument_for_virtual_parameter() -> None: p = GettableParam("base_param", vals=vals.Numbers()) - p._instrument = named_instrument # type: ignore[assignment] + p._instrument = named_instrument # type: ignore[assignment] # ty: ignore[invalid-assignment] vp = VirtualParameter("test_param", param=p) assert vp.underlying_instrument is named_instrument # type: ignore[comparison-overlap] diff --git a/tests/parameter/test_parameter_cache.py b/tests/parameter/test_parameter_cache.py index 159cf3cbd67..78d215f7461 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 @@ -228,7 +228,7 @@ def test_no_get_max_val_age_runtime_error( class LocalParameter(ParameterBase): def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) - self.set_raw = lambda x: x # type: ignore[method-assign] + self.set_raw = lambda x: x # type: ignore[method-assign] # ty: ignore[invalid-assignment] self.set = self._wrap_set(self.set_raw) local_parameter = LocalParameter("test_param", instrument=None, max_val_age=1) @@ -268,13 +268,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 @@ -320,7 +320,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 @@ -341,7 +341,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 a735844e52d..4569f74d791 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 e2fabadcd73..3ca76e0c1b3 100644 --- a/tests/parameter/test_parameter_with_setpoints.py +++ b/tests/parameter/test_parameter_with_setpoints.py @@ -103,7 +103,7 @@ def test_setpoints_non_parameter_raises() -> None: param_with_setpoints_1 = ParameterWithSetpoints( "param_1", get_cmd=lambda: _rng.random(n_points_1()), - setpoints=(lambda x: x,), # type: ignore[arg-type] + setpoints=(lambda x: x,), # type: ignore[arg-type] # ty: ignore[invalid-argument-type] vals=vals.Arrays(shape=(n_points_1,)), ) @@ -114,7 +114,7 @@ def test_setpoints_non_parameter_raises() -> None: ) with pytest.raises(TypeError, match=err_msg): - param_with_setpoints_1.setpoints = (lambda x: x,) # type: ignore[assignment] + param_with_setpoints_1.setpoints = (lambda x: x,) # type: ignore[assignment] # ty: ignore[invalid-assignment] def test_validation_inconsistent_shape() -> None: @@ -322,7 +322,7 @@ def test_validation_one_dim_missing() -> None: "param_6", get_cmd=lambda: _rng.random(n_points_1()), setpoints=(setpoints_1,), - vals=vals.Arrays(shape=(n_points_1, None)), # type: ignore[arg-type] + vals=vals.Arrays(shape=(n_points_1, None)), # type: ignore[arg-type] # ty: ignore[invalid-argument-type] ) expected_err_msg = ( r"One or more dimensions have unknown shape " @@ -349,7 +349,7 @@ def test_validation_one_sp_dim_missing() -> None: setpoints_1 = Parameter( "setpoints_1", get_cmd=lambda: _rng.random(n_points_1()), - vals=vals.Arrays(shape=(n_points_1, None)), # type: ignore[arg-type] + vals=vals.Arrays(shape=(n_points_1, None)), # type: ignore[arg-type] # ty: ignore[invalid-argument-type] ) param_sp_without_shape = ParameterWithSetpoints( "param_6", diff --git a/tests/parameter/test_scaled_parameter.py b/tests/parameter/test_scaled_parameter.py index 9c9a0092eb2..2bcb9f6c550 100644 --- a/tests/parameter/test_scaled_parameter.py +++ b/tests/parameter/test_scaled_parameter.py @@ -24,7 +24,7 @@ def _make_instrument() -> "Generator[DummyInstrument, None, None]": get_cmd=None, set_cmd=None, ) - instrument.scaler = ScaledParameter( # type: ignore[attr-defined] + instrument.scaler = ScaledParameter( # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] instrument.target_parameter, division=1 ) yield instrument @@ -37,7 +37,7 @@ def test_constructor(instrument: DummyInstrument) -> None: # Require a wrapped parameter with pytest.raises(TypeError): - ScaledParameter() # type: ignore[call-arg] + ScaledParameter() # type: ignore[call-arg] # ty: ignore[missing-argument] # Require a scaling factor with pytest.raises(ValueError): diff --git a/tests/parameter/test_snapshot.py b/tests/parameter/test_snapshot.py index af3f69bf8e1..15c7ce4da31 100644 --- a/tests/parameter/test_snapshot.py +++ b/tests/parameter/test_snapshot.py @@ -58,13 +58,13 @@ def wrapped_func(*args: P.args, **kwargs: P.kwargs) -> T: call_count += 1 return get_func(*args, **kwargs) - wrapped_func.call_count = lambda: call_count # type: ignore[attr-defined] + wrapped_func.call_count = lambda: call_count # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] return wrapped_func p.get = wrap_in_call_counter(p.get) # pre-condition - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] else: # pre-condition assert not hasattr(p, "get") @@ -161,7 +161,7 @@ def test_snapshot_timestamp_for_valid_cache_depends_on_cache_update( # Hack cache's timestamp to simplify this test timestamp = p.cache.timestamp assert timestamp is not None - p.cache._timestamp = timestamp - timedelta(days=31) # type: ignore[attr-defined] + p.cache._timestamp = timestamp - timedelta(days=31) # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] tu = datetime.now(UTC) assert p.cache.timestamp is not None @@ -242,7 +242,7 @@ def test_snapshot_when_snapshot_value_is_false( assert "raw_value" not in s if get_cmd is not False: - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def test_snapshot_value_is_true_by_default( @@ -297,7 +297,7 @@ def test_snapshot_when_snapshot_get_is_false( assert s["raw_value"] is None if get_cmd is not False: - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def test_snapshot_of_non_gettable_parameter_mirrors_cache( @@ -348,15 +348,15 @@ def test_snapshot_of_gettable_parameter_depends_on_update( if should_get: assert s["value"] == 65 assert s["raw_value"] == 69 - assert p.get.call_count() == 1 # type: ignore[attr-defined] + assert p.get.call_count() == 1 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] elif cache_is_valid: assert s["value"] == 42 assert s["raw_value"] == 46 - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] else: assert s["value"] is None assert s["raw_value"] is None - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def test_snapshot_value() -> None: @@ -391,7 +391,7 @@ def test_normalize_snapshot_update_maps_to_canonical_values() -> None: def test_normalize_snapshot_update_rejects_unknown_string() -> None: with pytest.raises(ValueError, match="Invalid value for snapshot"): - normalize_snapshot_update("bogus") # type: ignore[arg-type] + normalize_snapshot_update("bogus") # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_snapshot_update_all_always_calls_get() -> None: @@ -403,7 +403,7 @@ def test_snapshot_update_all_always_calls_get() -> None: ) s = p.snapshot(update="All") assert s["value"] == 69 - assert p.get.call_count() == 1 # type: ignore[attr-defined] + assert p.get.call_count() == 1 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def test_snapshot_update_never_never_calls_get() -> None: @@ -415,7 +415,7 @@ def test_snapshot_update_never_never_calls_get() -> None: ) s = p.snapshot(update="Never") assert s["value"] is None - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def test_snapshot_update_only_invalid_calls_get_when_cache_invalid() -> None: @@ -427,7 +427,7 @@ def test_snapshot_update_only_invalid_calls_get_when_cache_invalid() -> None: ) s = p.snapshot(update="Only_invalid") assert s["value"] == 69 - assert p.get.call_count() == 1 # type: ignore[attr-defined] + assert p.get.call_count() == 1 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def test_snapshot_update_only_invalid_skips_get_when_cache_valid() -> None: @@ -440,7 +440,7 @@ def test_snapshot_update_only_invalid_skips_get_when_cache_valid() -> None: s = p.snapshot(update="Only_invalid") # the cached (set) value is used, ``get`` is not called assert s["value"] == 42 - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] @pytest.mark.parametrize( @@ -473,8 +473,8 @@ def test_snapshot_update_string_matches_legacy_value( assert s_legacy["value"] == s_string["value"] assert s_legacy["raw_value"] == s_string["raw_value"] assert ( - p_legacy.get.call_count() # type: ignore[attr-defined] - == p_string.get.call_count() # type: ignore[attr-defined] + p_legacy.get.call_count() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] + == p_string.get.call_count() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] ) @@ -486,4 +486,4 @@ def test_snapshot_rejects_unknown_update_value() -> None: cache_is_valid=True, ) with pytest.raises(ValueError, match="Invalid value for snapshot"): - p.snapshot(update="bogus") # type: ignore[arg-type] + p.snapshot(update="bogus") # type: ignore[arg-type] # ty: ignore[no-matching-overload] diff --git a/tests/test_channels.py b/tests/test_channels.py index 1f921b8b996..94b9781877b 100644 --- a/tests/test_channels.py +++ b/tests/test_channels.py @@ -156,7 +156,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] ) @@ -166,7 +166,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] ) @@ -728,7 +728,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: @@ -802,7 +802,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 6ed238b355a..83a2d9cbd66 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -11,25 +11,25 @@ class CustomError(Exception): def test_bad_calls() -> None: with pytest.raises(TypeError): - Command() # type: ignore[call-arg] + Command() # type: ignore[call-arg] # ty: ignore[missing-argument] with pytest.raises(TypeError): - Command(cmd="") # type: ignore[call-arg] + Command(cmd="") # type: ignore[call-arg] # ty: ignore[missing-argument] with pytest.raises(TypeError): - Command(0, "", output_parser=lambda: 1) # type: ignore[arg-type, misc] + Command(0, "", output_parser=lambda: 1) # type: ignore[arg-type, misc] # ty: ignore[invalid-argument-type] with pytest.raises(TypeError): Command(1, "", input_parser=lambda: 1) with pytest.raises(TypeError): - Command(0, cmd="", exec_str="not a function") # type: ignore[arg-type] + Command(0, cmd="", exec_str="not a function") # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises(TypeError): Command( 0, cmd=lambda: 1, - no_cmd_function="not a function", # type: ignore[arg-type] + no_cmd_function="not a function", # type: ignore[arg-type] # ty: ignore[invalid-argument-type] ) diff --git a/tests/test_import.py b/tests/test_import.py index 73d312b0720..b40392163fb 100644 --- a/tests/test_import.py +++ b/tests/test_import.py @@ -32,7 +32,7 @@ def test_top_level_parameter_shorthand_deprecated() -> None: with pytest.warns( QCoDeSDeprecationWarning, match=r"top level.*'qcodes\.parameters'" ): - obj = qcodes.Parameter # type: ignore[attr-defined] + obj = qcodes.Parameter # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert obj is qcodes.parameters.Parameter @@ -40,13 +40,13 @@ def test_top_level_combine_shorthand_deprecated() -> None: with pytest.warns( QCoDeSDeprecationWarning, match=r"top level.*'qcodes\.parameters'" ): - obj = qcodes.combine # type: ignore[attr-defined] + obj = qcodes.combine # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert obj is qcodes.parameters.combine def test_top_level_measurement_shorthand_deprecated() -> None: with pytest.warns(QCoDeSDeprecationWarning, match=r"top level.*'qcodes\.dataset'"): - obj = qcodes.Measurement # type: ignore[attr-defined] + obj = qcodes.Measurement # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert obj is qcodes.dataset.Measurement @@ -54,19 +54,19 @@ def test_top_level_instrument_shorthand_deprecated() -> None: with pytest.warns( QCoDeSDeprecationWarning, match=r"top level.*'qcodes\.instrument'" ): - obj = qcodes.Instrument # type: ignore[attr-defined] + obj = qcodes.Instrument # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert obj is qcodes.instrument.Instrument def test_top_level_monitor_shorthand_deprecated() -> None: with pytest.warns(QCoDeSDeprecationWarning, match=r"top level.*'qcodes\.monitor'"): - obj = qcodes.Monitor # type: ignore[attr-defined] + obj = qcodes.Monitor # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert obj is qcodes.monitor.Monitor def test_top_level_station_shorthand_deprecated() -> None: with pytest.warns(QCoDeSDeprecationWarning, match=r"top level.*'qcodes\.station'"): - obj = qcodes.Station # type: ignore[attr-defined] + obj = qcodes.Station # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert obj is qcodes.station.Station @@ -85,7 +85,7 @@ def test_top_level_submodule_access_not_deprecated() -> None: def test_top_level_unknown_attribute_raises() -> None: with pytest.raises(AttributeError, match="definitely_not_a_qcodes_attribute"): - _ = qcodes.definitely_not_a_qcodes_attribute # type: ignore[attr-defined] + _ = qcodes.definitely_not_a_qcodes_attribute # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def test_instrument_parameter_reexport_deprecated() -> None: @@ -94,7 +94,7 @@ def test_instrument_parameter_reexport_deprecated() -> None: with pytest.warns( QCoDeSDeprecationWarning, match=r"'ManualParameter'.*'qcodes\.parameters'" ): - obj = qcodes.instrument.ManualParameter # type: ignore[attr-defined] + obj = qcodes.instrument.ManualParameter # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert obj is qcodes.parameters.ManualParameter @@ -127,4 +127,4 @@ def test_instrument_parameter_reexport_deprecated_all(name: str) -> None: def test_instrument_unknown_attribute_raises() -> None: with pytest.raises(AttributeError, match="definitely_not_a_qcodes_attribute"): - _ = qcodes.instrument.definitely_not_a_qcodes_attribute # type: ignore[attr-defined] + _ = qcodes.instrument.definitely_not_a_qcodes_attribute # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] diff --git a/tests/test_instrument.py b/tests/test_instrument.py index 3b77fed4569..b23c6a68fde 100644 --- a/tests/test_instrument.py +++ b/tests/test_instrument.py @@ -436,7 +436,7 @@ def test_other_exception() -> None: # in order to raise an unexpected exception, and make sure it is # passed through the call stack, let's pass an empty dict instead # of a string with instrument name - _ = find_or_create_instrument(DummyInstrument, {}) # type: ignore[arg-type] + _ = find_or_create_instrument(DummyInstrument, {}) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] @pytest.mark.usefixtures("close_before_and_after") @@ -550,7 +550,7 @@ def test_close_all_no_log_by_default(caplog: pytest.LogCaptureFixture) -> None: def test_close_all_only_accepts_keyword_arguments() -> None: """The ``close_all`` options are keyword-only.""" with pytest.raises(TypeError): - Instrument.close_all(True) # type: ignore[misc] + Instrument.close_all(True) # type: ignore[misc] # ty: ignore[too-many-positional-arguments] def test_instrument_metadata(request: FixtureRequest) -> None: diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 87da1e55699..b4b1194d733 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -19,7 +19,7 @@ def snapshot_base(self, update=False, params_to_skip_update=None): class HasSnapshot(Metadatable): # Users shouldn't do this... but we'll test its behavior # for completeness - def snapshot(self, update: bool | None = False) -> Snapshot: # type: ignore[misc] + def snapshot(self, update: bool | None = False) -> Snapshot: # type: ignore[misc] # ty: ignore[invalid-method-override, override-of-final-method] return {"fruit": "kiwi"} @@ -58,7 +58,7 @@ def test_load() -> None: def test_init() -> None: with pytest.raises(TypeError): - Metadatable(metadata={"2": 3}, not_metadata={4: 5}) # type: ignore[call-arg] + Metadatable(metadata={"2": 3}, not_metadata={4: 5}) # type: ignore[call-arg] # ty: ignore[unknown-argument] m = Metadatable(metadata={"2": 3}) assert m.metadata == {"2": 3} diff --git a/tests/test_station.py b/tests/test_station.py index 4698ab5ab77..74b5c5ba905 100644 --- a/tests/test_station.py +++ b/tests/test_station.py @@ -114,13 +114,13 @@ def test_add_component_with_no_name() -> None: """ bob = {"name", "bob"} station = Station() - station.add_component(bob) # type: ignore[arg-type] + station.add_component(bob) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert ["component0"] == list(station.components.keys()) assert bob == station.components["component0"] # type: ignore[comparison-overlap] jay = {"name", "jay"} - station.add_component(jay) # type: ignore[arg-type] + station.add_component(jay) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert ["component0", "component1"] == list(station.components.keys()) assert jay == station.components["component1"] # type: ignore[comparison-overlap] @@ -1118,7 +1118,7 @@ def test_load_all_instruments_only_names(example_station) -> None: def test_load_all_instruments_without_config_raises() -> None: station = Station() with pytest.raises(ValueError, match="Station has no config"): - station.load_all_instruments() # type: ignore[call-overload] + station.load_all_instruments() # type: ignore[call-overload] # ty: ignore[no-matching-overload] def test_station_config_created_with_multiple_config_files() -> None: diff --git a/tests/utils/test_class_strings.py b/tests/utils/test_class_strings.py index 4badd3ca4d2..d059d3aa5e2 100644 --- a/tests/utils/test_class_strings.py +++ b/tests/utils/test_class_strings.py @@ -12,5 +12,5 @@ def test_full_class() -> None: def test_named_repr() -> None: j = json.JSONEncoder() id_ = id(j) - j.name = "Peppa" # type: ignore[attr-defined] + j.name = "Peppa" # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert named_repr(j) == f"" diff --git a/tests/utils/test_isfunction.py b/tests/utils/test_isfunction.py index 81bc0268ab8..45da5b8957e 100644 --- a/tests/utils/test_isfunction.py +++ b/tests/utils/test_isfunction.py @@ -36,7 +36,7 @@ def f2(a: object, b: object) -> NoReturn: # make sure we only accept valid arg_count with pytest.raises(TypeError): - is_function(f0, "lots") # type: ignore[arg-type] + is_function(f0, "lots") # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises(TypeError): is_function(f0, -1) diff --git a/tests/validators/test_arrays.py b/tests/validators/test_arrays.py index 02a0549f670..607b612fe95 100644 --- a/tests/validators/test_arrays.py +++ b/tests/validators/test_arrays.py @@ -18,7 +18,7 @@ def test_type() -> None: m = Arrays(min_value=0.0, max_value=3.2, shape=(2, 2)) for v in ["somestring", 4, 2, [[2, 0], [1, 2]]]: with pytest.raises(TypeError): - m.validate(v) # type: ignore[arg-type] + m.validate(v) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_complex_min_max_raises() -> None: @@ -31,14 +31,14 @@ def test_complex_min_max_raises() -> None: r" It is \(1\+1j\) of type " r"", ): - Arrays(min_value=1 + 1j) # type: ignore[arg-type] + Arrays(min_value=1 + 1j) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises( TypeError, match=r"max_value must be a real number. " r"It is \(1\+1j\) of type " r"", ): - Arrays(max_value=1 + 1j) # type: ignore[arg-type] + Arrays(max_value=1 + 1j) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises( TypeError, match=r"Setting min_value or max_value is " @@ -225,9 +225,9 @@ def test_valid_values() -> None: def test_shape_non_sequence_raises() -> None: with pytest.raises(ValueError): - _ = Arrays(shape=5) # type: ignore[arg-type] + _ = Arrays(shape=5) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises(ValueError): - _ = Arrays(shape=lambda: 10) # type: ignore[arg-type] + _ = Arrays(shape=lambda: 10) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_repr() -> None: diff --git a/tests/validators/test_basic.py b/tests/validators/test_basic.py index ee606a4205a..763e3d88ded 100644 --- a/tests/validators/test_basic.py +++ b/tests/validators/test_basic.py @@ -55,7 +55,7 @@ def test_real_anything() -> None: def test_failed_anything() -> None: with pytest.raises(TypeError): - Anything(1) # type: ignore[call-arg] + Anything(1) # type: ignore[call-arg] # ty: ignore[too-many-positional-arguments] with pytest.raises(TypeError): - Anything(values=[1, 2, 3]) # type: ignore[call-arg] + Anything(values=[1, 2, 3]) # type: ignore[call-arg] # ty: ignore[unknown-argument] diff --git a/tests/validators/test_bool.py b/tests/validators/test_bool.py index f41da91ee6a..3ec9739f615 100644 --- a/tests/validators/test_bool.py +++ b/tests/validators/test_bool.py @@ -54,7 +54,7 @@ def test_bool() -> None: for vv in NOTBOOLS: with pytest.raises(TypeError): - b.validate(vv) # type: ignore[arg-type] + b.validate(vv) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert repr(b) == "" diff --git a/tests/validators/test_callable.py b/tests/validators/test_callable.py index 02a75a59cb6..8a43ae2234e 100644 --- a/tests/validators/test_callable.py +++ b/tests/validators/test_callable.py @@ -12,7 +12,7 @@ def test_func() -> bool: c.validate(test_func) test_int = 5 with pytest.raises(TypeError): - c.validate(test_int) # type: ignore[arg-type] + c.validate(test_int) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_valid_values() -> None: diff --git a/tests/validators/test_complex.py b/tests/validators/test_complex.py index 2f0f9e45544..ff1e4995108 100644 --- a/tests/validators/test_complex.py +++ b/tests/validators/test_complex.py @@ -28,4 +28,4 @@ def test_complex_raises(val: float | str) -> None: n = ComplexNumbers() with pytest.raises(TypeError, match=r"is not complex;"): - n.validate(val) # type: ignore[arg-type] + n.validate(val) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] diff --git a/tests/validators/test_dict.py b/tests/validators/test_dict.py index 76746374ba7..e3cca0b6733 100644 --- a/tests/validators/test_dict.py +++ b/tests/validators/test_dict.py @@ -13,7 +13,7 @@ def test_dict() -> None: d.validate(my_dict) my_int = 5 with pytest.raises(TypeError): - d.validate(my_int) # type: ignore[arg-type] + d.validate(my_int) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_valid_values() -> None: diff --git a/tests/validators/test_enum.py b/tests/validators/test_enum.py index 72628bbc043..9496f63789d 100644 --- a/tests/validators/test_enum.py +++ b/tests/validators/test_enum.py @@ -37,7 +37,7 @@ def test_good() -> None: def test_bad() -> None: for enum in not_enums: with pytest.raises(TypeError): - vals.Enum(*enum) # type: ignore[arg-type] + vals.Enum(*enum) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_valid_values() -> None: diff --git a/tests/validators/test_ints.py b/tests/validators/test_ints.py index 92450f80baa..22f2ec76dec 100644 --- a/tests/validators/test_ints.py +++ b/tests/validators/test_ints.py @@ -91,7 +91,7 @@ def test_unlimited() -> None: for vv in not_ints: with pytest.raises(TypeError): - n.validate(vv) # type: ignore[arg-type] + n.validate(vv) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_min() -> None: @@ -109,7 +109,7 @@ def test_min_raises() -> None: n = Ints(min_value=ints[-1]) for v in not_ints: with pytest.raises(TypeError): - n.validate(v) # type: ignore[arg-type] + n.validate(v) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_max() -> None: @@ -127,7 +127,7 @@ def test_max_raises() -> None: n = Ints(max_value=ints[-1]) for v in not_ints: with pytest.raises(TypeError): - n.validate(v) # type: ignore[arg-type] + n.validate(v) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_range() -> None: @@ -142,7 +142,7 @@ def test_range() -> None: for vv in not_ints: with pytest.raises(TypeError): - n.validate(vv) # type: ignore[arg-type] + n.validate(vv) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert repr(n) == "" assert n.is_numeric @@ -150,17 +150,17 @@ def test_range() -> None: def test_failed_numbers() -> None: with pytest.raises(TypeError): - Ints(1, 2, 3) # type: ignore[call-arg] + Ints(1, 2, 3) # type: ignore[call-arg] # ty: ignore[too-many-positional-arguments] with pytest.raises(TypeError): Ints(1, 1) # min >= max for val in not_ints: with pytest.raises((TypeError, OverflowError)): - Ints(max_value=val) # type: ignore[arg-type] + Ints(max_value=val) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises((TypeError, OverflowError)): - Ints(min_value=val) # type: ignore[arg-type] + Ints(min_value=val) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_valid_values() -> None: diff --git a/tests/validators/test_lists.py b/tests/validators/test_lists.py index 92eb8311fa3..722e237facb 100644 --- a/tests/validators/test_lists.py +++ b/tests/validators/test_lists.py @@ -15,7 +15,7 @@ def test_type() -> None: v2 = 234 with pytest.raises(TypeError): - list_validator.validate(v2) # type: ignore[arg-type] + list_validator.validate(v2) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_elt_vals() -> None: @@ -25,7 +25,7 @@ def test_elt_vals() -> None: v2 = [0, 1, 11] with pytest.raises(ValueError): - list_validator.validate(v2) # type: ignore[arg-type] + list_validator.validate(v2) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_valid_values() -> None: diff --git a/tests/validators/test_multi_type.py b/tests/validators/test_multi_type.py index 334f2a6922c..a48a1f34673 100644 --- a/tests/validators/test_multi_type.py +++ b/tests/validators/test_multi_type.py @@ -87,15 +87,15 @@ def test_bad() -> None: # combiner == 'OR' for args in [[], [1], [Strings(), True]]: with pytest.raises(TypeError): - MultiType(*args) # type: ignore[misc] + MultiType(*args) # type: ignore[misc] # ty: ignore[invalid-argument-type] # combiner == 'OR' for args in [[], [1], [Strings(), True]]: with pytest.raises(TypeError): - MultiType(*args, combiner="OR") # type: ignore[misc] + MultiType(*args, combiner="OR") # type: ignore[misc] # ty: ignore[invalid-argument-type] # combiner == 'AND' for args in [[], [1], [Strings(), True]]: with pytest.raises(TypeError): - MultiType(*args, combiner="AND") # type: ignore[misc] + MultiType(*args, combiner="AND") # type: ignore[misc] # ty: ignore[invalid-argument-type] def test_valid_values() -> None: diff --git a/tests/validators/test_multi_type_and.py b/tests/validators/test_multi_type_and.py index 8d323ea1453..b943ed72334 100644 --- a/tests/validators/test_multi_type_and.py +++ b/tests/validators/test_multi_type_and.py @@ -44,4 +44,4 @@ def test_good() -> None: def test_bad() -> None: for args in ([], [1], [Strings(), True]): with pytest.raises(TypeError): - MultiTypeAnd(*args) # type: ignore[misc] + MultiTypeAnd(*args) # type: ignore[misc] # ty: ignore[invalid-argument-type] diff --git a/tests/validators/test_multi_type_or.py b/tests/validators/test_multi_type_or.py index 52ac0c1ced7..c21c9b2b453 100644 --- a/tests/validators/test_multi_type_or.py +++ b/tests/validators/test_multi_type_or.py @@ -38,7 +38,7 @@ def test_good() -> None: def test_bad() -> None: for args in ([], [1], [Strings(), True]): with pytest.raises(TypeError): - MultiTypeOr(*args) # type: ignore[misc] + MultiTypeOr(*args) # type: ignore[misc] # ty: ignore[invalid-argument-type] def test_valid_values() -> None: diff --git a/tests/validators/test_multiples.py b/tests/validators/test_multiples.py index cf3f894ca9e..6a79f5b3705 100644 --- a/tests/validators/test_multiples.py +++ b/tests/validators/test_multiples.py @@ -108,11 +108,11 @@ def test_divisors() -> None: for vvv in not_multiples: with pytest.raises(TypeError): - n.validate(vvv) # type:ignore[arg-type] + n.validate(vvv) # type:ignore[arg-type] # ty: ignore[invalid-argument-type] for dd in not_divisors: with pytest.raises(TypeError): - n = Multiples(divisor=dd) # type:ignore[arg-type] + n = Multiples(divisor=dd) # type:ignore[arg-type] # ty: ignore[invalid-argument-type] n = Multiples(divisor=3, min_value=1, max_value=10) assert repr(n) == "" diff --git a/tests/validators/test_numbers.py b/tests/validators/test_numbers.py index 79a1acf1929..0aca7efd874 100644 --- a/tests/validators/test_numbers.py +++ b/tests/validators/test_numbers.py @@ -139,7 +139,7 @@ def test_range() -> None: def test_failed_numbers() -> None: with pytest.raises(TypeError): - Numbers(1, 2, 3) # type: ignore[call-arg] + Numbers(1, 2, 3) # type: ignore[call-arg] # ty: ignore[too-many-positional-arguments] with pytest.raises(TypeError): Numbers(1, 1) # min >= max diff --git a/tests/validators/test_sequence.py b/tests/validators/test_sequence.py index 269460565dd..97d47092c56 100644 --- a/tests/validators/test_sequence.py +++ b/tests/validators/test_sequence.py @@ -12,7 +12,7 @@ def test_type() -> None: v2 = 234 with pytest.raises(TypeError): - sequence_validator.validate(v2) # type: ignore[arg-type] + sequence_validator.validate(v2) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_elt_vals() -> None: diff --git a/tests/validators/test_string.py b/tests/validators/test_string.py index f196afaf6f4..d7e0186110f 100644 --- a/tests/validators/test_string.py +++ b/tests/validators/test_string.py @@ -49,7 +49,7 @@ def test_unlimited() -> None: for vv in not_strings: with pytest.raises(TypeError): - s.validate(vv) # type: ignore[arg-type] + s.validate(vv) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert repr(s) == "" @@ -66,7 +66,7 @@ def test_min() -> None: for vv in not_strings: with pytest.raises(TypeError): - s.validate(vv) # type: ignore[arg-type] + s.validate(vv) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] s = Strings(min_length=100) assert repr(s) == "=100>" @@ -85,7 +85,7 @@ def test_max() -> None: s = Strings(max_length=100) for vv in not_strings: with pytest.raises(TypeError): - s.validate(vv) # type: ignore[arg-type] + s.validate(vv) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert repr(s) == "" @@ -102,7 +102,7 @@ def test_range() -> None: for vv in not_strings: with pytest.raises(TypeError): - s.validate(vv) # type: ignore[arg-type] + s.validate(vv) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert repr(s) == "" @@ -112,7 +112,7 @@ def test_range() -> None: def test_failed_strings() -> None: with pytest.raises(TypeError): - Strings(1, 2, 3) # type: ignore[call-arg] + Strings(1, 2, 3) # type: ignore[call-arg] # ty: ignore[too-many-positional-arguments] with pytest.raises(TypeError): Strings(10, 9) @@ -121,14 +121,14 @@ def test_failed_strings() -> None: Strings(max_length=0) with pytest.raises(TypeError): - Strings(min_length=1e12) # type: ignore[arg-type] + Strings(min_length=1e12) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] for length in [-1, 3.5, "2", None]: with pytest.raises(TypeError): - Strings(max_length=length) # type: ignore[arg-type] + Strings(max_length=length) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises(TypeError): - Strings(min_length=length) # type: ignore[arg-type] + Strings(min_length=length) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_valid_values() -> None: From 143954930177ad8e67c4f25a442fe5dd1752f856 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:16:01 +0200 Subject: [PATCH 65/76] Suppress ty errors where pyright is already suppressed These are calls that deliberately pass invalid arguments to check that they are rejected at runtime. They are only reported by pyright and ty since the enclosing test functions are untyped and therefore skipped by mypy. --- .../dataset/measurement/test_measurement_context_manager.py | 2 +- tests/dataset/test_dataset_export.py | 6 +++--- tests/dataset/test_measurement_extensions.py | 6 +++--- tests/dataset/test_string_data.py | 2 +- tests/parameter/test_keyword_only_args.py | 4 ++-- tests/sphinx_extension/test_parse_parameter_attr.py | 2 +- tests/test_channels.py | 2 +- tests/validators/test_enum.py | 2 +- tests/validators/test_literal.py | 4 ++-- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/dataset/measurement/test_measurement_context_manager.py b/tests/dataset/measurement/test_measurement_context_manager.py index cd76b48e133..dc30c10a478 100644 --- a/tests/dataset/measurement/test_measurement_context_manager.py +++ b/tests/dataset/measurement/test_measurement_context_manager.py @@ -300,7 +300,7 @@ def test_unregister_parameter(DAC, DMM) -> None: not_parameters = [DAC, DMM, 0.0, 1] for notparam in not_parameters: with pytest.raises(ValueError): - meas.unregister_parameter(notparam) # pyright: ignore[reportArgumentType] + meas.unregister_parameter(notparam) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] # unregistering something not registered should silently "succeed" meas.unregister_parameter("totes_not_registered") diff --git a/tests/dataset/test_dataset_export.py b/tests/dataset/test_dataset_export.py index bf7e2191168..9ece8058438 100644 --- a/tests/dataset/test_dataset_export.py +++ b/tests/dataset/test_dataset_export.py @@ -1648,13 +1648,13 @@ def test_multi_index_options_non_grid(mock_dataset_non_grid: DataSet) -> None: 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 56fb450cc39..0073164755f 100644 --- a/tests/dataset/test_measurement_extensions.py +++ b/tests/dataset/test_measurement_extensions.py @@ -334,7 +334,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, ) _ = datasaver.dataset @@ -355,8 +355,8 @@ def test_dond_into_fails_with_groups(default_params, default_database_and_experi dond_into( datasaver, sweep1, - [meas1], # pyright: ignore [reportArgumentType] - [meas2], # pyright: ignore [reportArgumentType] + [meas1], # pyright: ignore [reportArgumentType] # ty: ignore[invalid-argument-type] + [meas2], # pyright: ignore [reportArgumentType] # ty: ignore[invalid-argument-type] ) _ = datasaver.dataset diff --git a/tests/dataset/test_string_data.py b/tests/dataset/test_string_data.py index 41cc2837502..ffabb4412a3 100644 --- a/tests/dataset/test_string_data.py +++ b/tests/dataset/test_string_data.py @@ -199,7 +199,7 @@ def test_list_of_strings(experiment) -> None: @settings(suppress_health_check=(HealthCheck.function_scoped_fixture,), deadline=None) @given( - p_values=hypnumpy.arrays( + p_values=hypnumpy.arrays( # ty: ignore[no-matching-overload] dtype=hst.sampled_from( ( hypnumpy.unicode_string_dtypes(), diff --git a/tests/parameter/test_keyword_only_args.py b/tests/parameter/test_keyword_only_args.py index 3927d1962b6..99c2bffc552 100644 --- a/tests/parameter/test_keyword_only_args.py +++ b/tests/parameter/test_keyword_only_args.py @@ -157,8 +157,8 @@ def test_keyword_args_work(self) -> None: def test_positional_args_rejected(self) -> None: with pytest.raises(TypeError): - MultiChannelInstrumentParameter( - [], # pyright: ignore[reportCallIssue] + MultiChannelInstrumentParameter( # ty: ignore[missing-argument] + [], # pyright: ignore[reportCallIssue] # ty: ignore[too-many-positional-arguments] "x", name="test", names=("a",), diff --git a/tests/sphinx_extension/test_parse_parameter_attr.py b/tests/sphinx_extension/test_parse_parameter_attr.py index a19d457eb54..b424fb07caf 100644 --- a/tests/sphinx_extension/test_parse_parameter_attr.py +++ b/tests/sphinx_extension/test_parse_parameter_attr.py @@ -102,7 +102,7 @@ def test_decorated_init_func() -> None: def test_decorated_class() -> None: - attr = qcodes_parameter_attr_getter(DummyDecoratedClassTestClass, "other_attr") # pyright: ignore[reportDeprecated] + attr = qcodes_parameter_attr_getter(DummyDecoratedClassTestClass, "other_attr") # pyright: ignore[reportDeprecated] # ty: ignore[deprecated] assert isinstance(attr, ParameterProxy) assert repr(attr) == '"InstanceAttribute"' diff --git a/tests/test_channels.py b/tests/test_channels.py index 94b9781877b..ad85303c1c0 100644 --- a/tests/test_channels.py +++ b/tests/test_channels.py @@ -516,7 +516,7 @@ def test_access_channels_by_name_empty_raises(dci: DummyChannelInstrument) -> No def test_access_channel_by_name_empty_raises(dci: DummyChannelInstrument) -> None: with pytest.raises(TypeError, match="missing 1 required positional argument"): - dci.channels.get_channel_by_name() # pyright: ignore[reportCallIssue] + dci.channels.get_channel_by_name() # pyright: ignore[reportCallIssue] # ty: ignore[missing-argument] def test_delete_from_channel_list(dci_with_list: DCIWithList) -> None: diff --git a/tests/validators/test_enum.py b/tests/validators/test_enum.py index 9496f63789d..1c0c5220e52 100644 --- a/tests/validators/test_enum.py +++ b/tests/validators/test_enum.py @@ -25,7 +25,7 @@ def test_good() -> None: for v in [22, "bad data", [44, 55]]: with pytest.raises((ValueError, TypeError)): - e.validate(v) # pyright: ignore[reportArgumentType] + e.validate(v) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] assert repr(e) == f"" diff --git a/tests/validators/test_literal.py b/tests/validators/test_literal.py index c549e8003af..e001d461ea2 100644 --- a/tests/validators/test_literal.py +++ b/tests/validators/test_literal.py @@ -15,10 +15,10 @@ def test_literal_validator() -> None: a123_val.validate(1) with pytest.raises(ValueError, match="5 is not a member of "): - a123_val.validate(5, context="Outside range") # pyright: ignore[reportArgumentType] + a123_val.validate(5, context="Outside range") # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] with pytest.raises(ValueError, match="some_str is not a member of "): - a123_val.validate("some_str", context="Wrong type") # pyright: ignore[reportArgumentType] + a123_val.validate("some_str", context="Wrong type") # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] def test_literal_validator_repr() -> None: From f6028fc019fc9838f6032a2863fe12d4a974a2fd Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:16:45 +0200 Subject: [PATCH 66/76] 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/8394.improved | 5 +++++ src/qcodes/utils/types.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 docs/changes/newsfragments/8394.improved diff --git a/docs/changes/newsfragments/8394.improved b/docs/changes/newsfragments/8394.improved new file mode 100644 index 00000000000..1093e7f909c --- /dev/null +++ b/docs/changes/newsfragments/8394.improved @@ -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 a91b7be837e67a113711816933f12116d5148e3c Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:17:12 +0200 Subject: [PATCH 67/76] 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/8395.improved | 3 +++ src/qcodes/monitor/monitor.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 docs/changes/newsfragments/8395.improved diff --git a/docs/changes/newsfragments/8395.improved b/docs/changes/newsfragments/8395.improved new file mode 100644 index 00000000000..bec83574a15 --- /dev/null +++ b/docs/changes/newsfragments/8395.improved @@ -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 c84fe4780782622517fd4b4d330b7bbc93e992b5 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:18:19 +0200 Subject: [PATCH 68/76] 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 | 2 +- tests/dataset/test_measurement_extensions.py | 10 +++++----- tests/dataset/test_sqlite_base.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/dataset/test_dataset_basic.py b/tests/dataset/test_dataset_basic.py index 5f77c4573e6..d7c77a245a2 100644 --- a/tests/dataset/test_dataset_basic.py +++ b/tests/dataset/test_dataset_basic.py @@ -971,7 +971,7 @@ def test_get_array_parameter_data_no_nulls(array_dataset_with_nulls) -> None: expected_names = {} expected_names["val1"] = ["val1", "sp1", "sp2"] expected_names["val2"] = ["val2", "sp1"] - expected_shapes = {} + expected_shapes: dict[str, list[tuple[int, ...]]] = {} expected_values = {} if "array" in types: diff --git a/tests/dataset/test_measurement_extensions.py b/tests/dataset/test_measurement_extensions.py index 0073164755f..f9cc26a1c38 100644 --- a/tests/dataset/test_measurement_extensions.py +++ b/tests/dataset/test_measurement_extensions.py @@ -502,7 +502,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), } @@ -530,7 +530,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 = [ @@ -576,7 +576,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", @@ -614,8 +614,8 @@ def test_shapes_with_multiple_datasets(default_params, default_database_and_expe _ = default_database_and_experiment set1, set2, set3, meas1, _, meas3 = default_params - shapes_1 = {meas1.register_name: (11, 11)} - shapes_2 = {meas3.register_name: (11, 11)} + shapes_1: dict[str, tuple[int, ...]] = {meas1.register_name: (11, 11)} + shapes_2: dict[str, tuple[int, ...]] = {meas3.register_name: (11, 11)} dataset_definition = [ DataSetDefinition( name="dataset_1", diff --git a/tests/dataset/test_sqlite_base.py b/tests/dataset/test_sqlite_base.py index d01b40f4186..a6bf8865617 100644 --- a/tests/dataset/test_sqlite_base.py +++ b/tests/dataset/test_sqlite_base.py @@ -54,7 +54,7 @@ def _make_simple_run_describer(): t = ParamSpecBase("t", "numeric") y = ParamSpecBase("y", "numeric") - paramtree = {y: (x, t)} + paramtree: dict[ParamSpecBase, tuple[ParamSpecBase, ...]] = {y: (x, t)} interdependencies = InterDependencies_(dependencies=paramtree) rundescriber = RunDescriber(interdependencies) From 164432414429fe9f416e3d36e48965e5e366625e Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:18:59 +0200 Subject: [PATCH 69/76] Assert optional attributes are set in tests Parameter.step and DelegateParameter.source are optional and are read back through a property, so a type checker cannot know that the value assigned earlier in the test is still there. --- tests/parameter/test_delegate_parameter.py | 1 + tests/parameter/test_parameter_ramp.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/tests/parameter/test_delegate_parameter.py b/tests/parameter/test_delegate_parameter.py index e9aca5043e0..1fa997c1bb8 100644 --- a/tests/parameter/test_delegate_parameter.py +++ b/tests/parameter/test_delegate_parameter.py @@ -420,6 +420,7 @@ def test_delegate_parameter_with_changed_source_snapshot_matches_value( delegate_param.source = source_parameter calc_value = (value - offset) / scale assert delegate_param.cache.get(get_if_invalid=False) == calc_value + assert delegate_param.source is not None assert delegate_param.source.cache.get(get_if_invalid=False) == value snapshot = delegate_param.snapshot() # disregard timestamp that might be slightly different diff --git a/tests/parameter/test_parameter_ramp.py b/tests/parameter/test_parameter_ramp.py index a1d49af003e..450edcb5386 100644 --- a/tests/parameter/test_parameter_ramp.py +++ b/tests/parameter/test_parameter_ramp.py @@ -62,6 +62,7 @@ def test_ramp_scaled(scale, value) -> None: assert p.raw_value == first_step * scale # then check the generated steps. They should not be scaled as the # scaling happens when setting them + assert p.step is not None expected_steps = np.linspace(first_step + p.step, second_step, 90) actual_steps = p.get_ramp_values(second_step, p.step) np.testing.assert_allclose(np.array(actual_steps), expected_steps) @@ -104,6 +105,7 @@ def test_ramp_parser(value) -> None: assert p.raw_value == -first_step # then check the generated steps. They should not be parsed as the # scaling happens when setting them + assert p.step is not None expected_steps = np.linspace((first_step + p.step), second_step, 90) actual_steps = p.get_ramp_values(second_step, p.step) np.testing.assert_allclose(np.array(actual_steps), expected_steps) @@ -142,6 +144,7 @@ def test_ramp_parsed_scaled(scale, value) -> None: # these are parsed in the set_wrapper np.testing.assert_allclose(np.array(p.set_values), expected_raw_steps) assert p.raw_value == -scale * first_step + assert p.step is not None expected_steps = np.linspace(first_step + p.step, second_step, 90) actual_steps = p.get_ramp_values(10, p.step) np.testing.assert_allclose(np.array(actual_steps), expected_steps) From 0a47a1062f5ed552b586b1f49dc0e20b91cebac8 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:20:22 +0200 Subject: [PATCH 70/76] Suppress ty on TypeVars used as plain values The deprecation shim tests create TypeVar objects to hand to _make_deprecated_typevars_getattr and read a TypeVar that only the module level __getattr__ provides. Neither is something a type checker can follow, and mypy and pyright accept both without complaint. --- tests/utils/test_deprecate.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/utils/test_deprecate.py b/tests/utils/test_deprecate.py index 5fa3e58006e..164b1f4ecd2 100644 --- a/tests/utils/test_deprecate.py +++ b/tests/utils/test_deprecate.py @@ -17,9 +17,11 @@ @pytest.fixture def deprecated_getattr() -> _FixtureT: """Create a __getattr__ with two deprecated TypeVars.""" + # these TypeVars are values handed to the deprecation shim rather than + # type variables used to parametrize anything, which ty does not allow deprecated = { - "MyT": TypeVar("MyT"), - "MyK": TypeVar("MyK", bound=int), + "MyT": TypeVar("MyT"), # ty: ignore[invalid-legacy-type-variable] + "MyK": TypeVar("MyK", bound=int), # ty: ignore[invalid-legacy-type-variable] } getattr_fn = _make_deprecated_typevars_getattr("fake.module", deprecated) return deprecated, getattr_fn @@ -64,7 +66,7 @@ def test_repeated_access_returns_same_object( def test_fallback_is_called_for_unknown_names() -> None: - deprecated: dict[str, TypeVar] = {"X": TypeVar("X")} + deprecated: dict[str, TypeVar] = {"X": TypeVar("X")} # ty: ignore[invalid-legacy-type-variable] def fallback(name: str) -> str: return f"fallback:{name}" @@ -74,7 +76,7 @@ def fallback(name: str) -> str: def test_fallback_not_called_for_deprecated_names() -> None: - deprecated: dict[str, TypeVar] = {"X": TypeVar("X")} + deprecated: dict[str, TypeVar] = {"X": TypeVar("X")} # ty: ignore[invalid-legacy-type-variable] fallback_called = False def fallback(name: str) -> str: @@ -92,6 +94,8 @@ def test_real_module_import_triggers_warning() -> None: """Test that importing a deprecated TypeVar from an actual module works.""" mod = importlib.import_module("qcodes.utils.deep_update_utils") with pytest.warns(QCoDeSDeprecationWarning, match="'K'"): - k = mod.K + # K is only served by the module level __getattr__ that this test + # exercises, so it is invisible to a type checker + k = mod.K # ty: ignore[unresolved-attribute] assert isinstance(k, TypeVar) From 3ab55465c65edf5e04b44cf8e271ccd667547e7e Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:20:48 +0200 Subject: [PATCH 71/76] 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 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/parameter/test_parameter_override.py b/tests/parameter/test_parameter_override.py index 77fe787a85e..5837cf2b0f0 100644 --- a/tests/parameter/test_parameter_override.py +++ b/tests/parameter/test_parameter_override.py @@ -30,7 +30,7 @@ def __init__(self, name, **kwargs): This instrument errors because it tries to override an attribute with a parameter. """ super().__init__(name, **kwargs) - self.voltage = self.add_parameter("voltage", set_cmd=None, get_cmd=None) + self.voltage = self.add_parameter("voltage", set_cmd=None, get_cmd=None) # ty: ignore[invalid-assignment] def voltage(self): return 0 @@ -43,7 +43,7 @@ def __init__(self, name, **kwargs): to define an interface for the instrument. """ super().__init__(name, **kwargs) - self.add_parameter("voltage", set_cmd=None, get_cmd=None) # type: ignore + self.add_parameter("voltage", set_cmd=None, get_cmd=None) @property def voltage(self): From c7d8903980d435a7564da44d22cd8d63e4d9dad5 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:21:13 +0200 Subject: [PATCH 72/76] Annotate a test container that holds reshaped arrays The list is later filled with two dimensional arrays, which does not fit the one dimensional element type inferred from the initial content now that numpy arrays carry their shape in the type. --- tests/dataset/test_dataset_basic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/dataset/test_dataset_basic.py b/tests/dataset/test_dataset_basic.py index d7c77a245a2..284521cfe76 100644 --- a/tests/dataset/test_dataset_basic.py +++ b/tests/dataset/test_dataset_basic.py @@ -1010,7 +1010,7 @@ def test_get_array_parameter_data(array_dataset) -> None: expected_shapes: dict[str, list[tuple[int, ...]]] = {} expected_len = 5 expected_shapes[par_name] = [(expected_len,), (expected_len,)] - expected_values = {} + expected_values: dict[str, list[np.ndarray]] = {} expected_values[par_name] = [ np.ones(expected_len) + 1, np.linspace(5, 9, expected_len), From 33474269bc04f1c863cce8403157d7c0e6094d2e Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:21:37 +0200 Subject: [PATCH 73/76] Suppress ty on reading the name of a callable The wrapped callable is a function in practice but its declared type does not guarantee that. mypy and pyright both accept the attribute access. --- tests/common.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/common.py b/tests/common.py index 11d4df019b2..9c73b66b36b 100644 --- a/tests/common.py +++ b/tests/common.py @@ -87,7 +87,9 @@ def profile[**P, T](func: Callable[P, T]) -> Callable[P, T]: """ def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: - profile_filename = func.__name__ + ".prof" + # a Callable is not necessarily a function object, so ty rejects + # reading its name here. mypy and pyright both allow it. + profile_filename = func.__name__ + ".prof" # ty: ignore[unresolved-attribute] profiler = cProfile.Profile() result = profiler.runcall(func, *args, **kwargs) profiler.dump_stats(profile_filename) From c4afb309b27a5cd84ebd061749ac3ebaef54dc4d Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sat, 29 Aug 2026 08:29:33 +0200 Subject: [PATCH 74/76] Check the whole project with ty in CI The test suite is now free of ty diagnostics, so there is no longer a reason to limit the CI run to src and docs. The paths to check are configured in pyproject.toml. --- .github/workflows/pytest.yaml | 2 +- docs/changes/newsfragments/8393.underthehood | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index e2337ce2fb3..0125cb8fc94 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -104,7 +104,7 @@ jobs: if: ${{ !matrix.min-version && always() }} id: mypy - name: Run ty - run: ty check --output-format github src docs + run: ty check --output-format github if: ${{ !matrix.min-version && always() }} id: ty - name: Set pytest basetemp diff --git a/docs/changes/newsfragments/8393.underthehood b/docs/changes/newsfragments/8393.underthehood index 782b81a23f2..fc64d898b71 100644 --- a/docs/changes/newsfragments/8393.underthehood +++ b/docs/changes/newsfragments/8393.underthehood @@ -1,4 +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``. +example notebooks in ``docs`` in addition to the source code in ``src`` +and the test suite. From 52881065b06bd8930c71811b97abc3934fdc4f59 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 1 Sep 2026 07:41:39 +0200 Subject: [PATCH 75/76] Drop workarounds for a ty bug fixed in 0.0.77 Forwarding ParameterBaseKWArgs with ** was rejected because ty computed the upper bound of the synthesized Self of a generic TypedDict from the default specialization of its type parameter. That is fixed in ty 0.0.77 so the five suppressions and the note explaining them can go, and the minimum version is raised accordingly. --- docs/changes/newsfragments/8396.underthehood | 4 ++++ pyproject.toml | 2 +- requirements.txt | 2 +- src/qcodes/parameters/array_parameter.py | 3 +-- src/qcodes/parameters/delegate_parameter.py | 3 +-- src/qcodes/parameters/multi_parameter.py | 3 +-- src/qcodes/parameters/parameter.py | 3 +-- src/qcodes/parameters/parameter_base.py | 10 ---------- src/qcodes/parameters/parameter_with_setpoints.py | 3 +-- 9 files changed, 11 insertions(+), 22 deletions(-) create mode 100644 docs/changes/newsfragments/8396.underthehood diff --git a/docs/changes/newsfragments/8396.underthehood b/docs/changes/newsfragments/8396.underthehood new file mode 100644 index 00000000000..75e79dce770 --- /dev/null +++ b/docs/changes/newsfragments/8396.underthehood @@ -0,0 +1,4 @@ +The suppressions needed to forward ``**kwargs: Unpack[ParameterBaseKWArgs]`` +to ``ParameterBase`` have been removed along with the note documenting them. +They worked around a bug in ``ty`` that is fixed in 0.0.77, which is now the +minimum version used to type check QCoDeS. diff --git a/pyproject.toml b/pyproject.toml index 2555ce08ea2..646daf82730 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,7 @@ test = [ "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.75", + "ty>=0.0.77", "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 fe44fea7370..a9634dacc97 100644 --- a/requirements.txt +++ b/requirements.txt @@ -457,7 +457,7 @@ traitlets==5.16.1 # nbconvert # nbformat # nbsphinx -ty==0.0.75 +ty==0.0.77 # via qcodes (pyproject.toml) types-jsonschema==4.26.0.20260518 # via qcodes (pyproject.toml) diff --git a/src/qcodes/parameters/array_parameter.py b/src/qcodes/parameters/array_parameter.py index 5fb1df55f58..d3565f83f02 100644 --- a/src/qcodes/parameters/array_parameter.py +++ b/src/qcodes/parameters/array_parameter.py @@ -142,8 +142,7 @@ def __init__( kwargs.setdefault("snapshot_value", False) super().__init__( name, - # see the note on ParameterBaseKWArgs - **kwargs, # ty: ignore[invalid-argument-type] + **kwargs, ) if self.settable: diff --git a/src/qcodes/parameters/delegate_parameter.py b/src/qcodes/parameters/delegate_parameter.py index b63736f1928..b948b8c568d 100644 --- a/src/qcodes/parameters/delegate_parameter.py +++ b/src/qcodes/parameters/delegate_parameter.py @@ -210,8 +210,7 @@ def __init__( initial_cache_value = kwargs.pop("initial_cache_value", None) self.source = source - # see the note on ParameterBaseKWArgs - super().__init__(name, **kwargs) # ty: ignore[invalid-argument-type] + super().__init__(name, **kwargs) self.label = kwargs.get("label", None) self.unit = kwargs.get("unit", None) diff --git a/src/qcodes/parameters/multi_parameter.py b/src/qcodes/parameters/multi_parameter.py index 80385ff6304..0f230b69334 100644 --- a/src/qcodes/parameters/multi_parameter.py +++ b/src/qcodes/parameters/multi_parameter.py @@ -153,8 +153,7 @@ def __init__( kwargs.setdefault("snapshot_value", False) super().__init__( name, - # see the note on ParameterBaseKWArgs - **kwargs, # ty: ignore[invalid-argument-type] + **kwargs, ) self._meta_attrs.extend( diff --git a/src/qcodes/parameters/parameter.py b/src/qcodes/parameters/parameter.py index 74feae11f0f..04c3b8920f0 100644 --- a/src/qcodes/parameters/parameter.py +++ b/src/qcodes/parameters/parameter.py @@ -362,8 +362,7 @@ def _set_manual_parameter( super().__init__( name=name, - # see the note on ParameterBaseKWArgs - **kwargs, # ty: ignore[invalid-argument-type] + **kwargs, ) no_instrument_get = not self._implements_get_raw and ( diff --git a/src/qcodes/parameters/parameter_base.py b/src/qcodes/parameters/parameter_base.py index ddd9dd190f0..2169c173dcb 100644 --- a/src/qcodes/parameters/parameter_base.py +++ b/src/qcodes/parameters/parameter_base.py @@ -148,16 +148,6 @@ class ParameterBaseKWArgs( ``**kwargs: Unpack[ParameterBaseKWArgs]`` as input and forward this to the super class to ensure that it can accept all the arguments defined here. - - Note that forwarding the kwargs on requires a - ``ty: ignore[invalid-argument-type]``. When a generic TypedDict declares a - PEP 696 default for a type parameter, ty computes the upper bound of the - synthesized ``Self`` as the default specialization, so every other - specialization is rejected by the members that bind ``Self``. Expanding - with ``**`` is one of those, and reports the rather misleading - ``must be a mapping type``. ``InstrumentTypeVar_co`` defaults to - ``InstrumentBase | None`` and so triggers this; ``ParameterDataTypeVar`` - defaults to ``Any``, which happens to be the one default ty accepts. """ instrument: NotRequired[InstrumentTypeVar_co] diff --git a/src/qcodes/parameters/parameter_with_setpoints.py b/src/qcodes/parameters/parameter_with_setpoints.py index 812b36f5a6c..9653be69281 100644 --- a/src/qcodes/parameters/parameter_with_setpoints.py +++ b/src/qcodes/parameters/parameter_with_setpoints.py @@ -74,8 +74,7 @@ def __init__( super().__init__( name=name, - # see the note on ParameterBaseKWArgs - **kwargs, # ty: ignore[invalid-argument-type] + **kwargs, ) if setpoints is None: self.setpoints = [] From a809d70547e87aa022be6c9b3759bd0d52e02da5 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 1 Sep 2026 07:44:11 +0200 Subject: [PATCH 76/76] Delete draft for a ty bug fixed upstream The report was never filed and ty 0.0.77 no longer reproduces it. --- ty-issue-1-typeddict-self-bound.md | 195 ----------------------------- 1 file changed, 195 deletions(-) delete mode 100644 ty-issue-1-typeddict-self-bound.md diff --git a/ty-issue-1-typeddict-self-bound.md b/ty-issue-1-typeddict-self-bound.md deleted file mode 100644 index 70ea4ed4858..00000000000 --- a/ty-issue-1-typeddict-self-bound.md +++ /dev/null @@ -1,195 +0,0 @@ -# ty issue draft 1 - -**Title** - -> Generic `TypedDict` with a type parameter default: `Self`-bound methods and `**`-unpacking rejected for every non-default specialization - -**Labels to suggest:** `bug`, `generics`, `typeddict`, `constraint-solver` - ---- - -### Summary - -When a generic `TypedDict` declares a default for its type parameter, ty computes -the upper bound of the synthesized `Self` type variable as the *default* -specialization rather than the generic one. Every other specialization is then -rejected by any method that binds `Self`. - -```python -from typing import TypedDict - - -class Movie[T = int](TypedDict): - extra: T - - -def f(m: Movie[str]) -> None: - m.keys() -``` - -``` -error[invalid-argument-type]: Argument to bound method `TypedDictFallback.keys` is incorrect - --> repro.py:7:5 - | -7 | m.keys() - | ^^^^^^^^ Argument type `Movie[str]` does not satisfy upper bound `Movie[int]` of type variable `Self` -``` - -Removing the default (`class Movie[T](TypedDict)`) makes the error go away -without any other change, so the default is what introduces the bound. - -Note that `Movie[str]` here is an ordinary concrete specialization. No type -variable is unsolved at the call site, and nothing is being inferred. - -### Which specializations are affected - -Only the declared default is accepted: - -| annotation | result | -| --- | --- | -| `Movie[int]` (the default) | ok | -| `Movie` (bare, default applies) | ok | -| `Movie[str]` | error | -| `Movie[T]` for an enclosing type variable `T` | error | - -### Which members are affected - -Members whose signature binds `Self`: - -| member | result | -| --- | --- | -| `keys()` | error | -| `values()` | error | -| `items()` | error | -| `copy()` | error | -| `**` unpacking | error | -| `get()` | ok | -| `setdefault()` | ok | -| `pop()` | ok | -| `update()` | ok | - -Assignability is unaffected, which is consistent with the problem being the -`Self` bound rather than the type itself: - -```python -from typing import Mapping, TypedDict - - -class Movie[T = int](TypedDict): - extra: T - - -def f(m: Movie[str]) -> None: - ok: Mapping[str, object] = m # no error -``` - -### The `**` unpacking symptom - -`**`-unpacking reports a different and rather misleading message, which is how I -originally ran into this: - -```python -from typing import TypedDict - - -class Movie[T = int](TypedDict): - extra: T - - -def f(m: Movie[str]) -> None: - dict(**m) -``` - -``` -error[invalid-argument-type]: Argument expression after ** must be a mapping type - --> repro.py:7:12 - | -7 | dict(**m) - | ^ Found `Movie[str]` -``` - -A `TypedDict` is always a `Mapping[str, object]`, so this message points away -from the real cause. - -### Not specific to `TypedDict` syntax or version - -The legacy spelling behaves identically: - -```python -from typing import Generic, TypedDict, TypeVar - -T = TypeVar("T", default=int) - - -class Movie(TypedDict, Generic[T]): - extra: T - - -def f(m: Movie[str]) -> None: - m.copy() -``` - -A plain generic class with a type parameter default is **not** affected, so this -looks specific to the synthesized `TypedDictFallback` `Self`: - -```python -class WithDefault[T = int]: - def m(self) -> None: ... - - -def f[T](a: WithDefault[T]) -> None: - a.m() # no error -``` - -Any non-`Any` default triggers it. `Any` is the only default that is accepted, -which is probably why this has gone unnoticed: - -| type parameter | result | -| --- | --- | -| `class Movie[T](TypedDict)` | ok | -| `class Movie[T = Any](TypedDict)` | ok | -| `class Movie[T = int](TypedDict)` | error | -| `class Movie[T = None](TypedDict)` | error | -| `class Movie[T = int \| None](TypedDict)` | error | -| `class Movie[T = object](TypedDict)` | error | -| `class Movie[T: int \| None = int \| None](TypedDict)` | error | - -Reproduced on 0.0.72, 0.0.73 and 0.0.74. Checked with `--python-version 3.13` so -that the PEP 696 syntax is not itself reported as an error. mypy 2.3.1 and -pyright both accept all of the above. - -I searched existing issues for `"must be a mapping type"`, `TypedDict Unpack -default`, `"generic TypedDict"`, `"PEP 696"` and `Unpack kwargs` and did not -find a preexisting issue. #4255 is the closest but is about a union alias in a -stub leaking an unspecialized type variable. - -The error shape is reminiscent of #4303, which is also an upper bound on a type -variable being applied too strictly, though that one is about a `bound=` on a -class scoped type variable rather than a `default=` on `Self`. - -### Relation to the feature overview - -The type system feature overview in #1889 lists all of the following as -implemented: - -- Generics: `TypeVar` defaults (PEP 696) -- `TypedDict`: Inheritance, generic `TypedDict`s -- `TypedDict`: Structural assignability and equivalence -- `TypedDict`: Methods (`get`, `pop`, `setdefault`, `keys`, `values`, `copy`) - -This report sits at the intersection of those, so following the guidance at the -top of #1889 for features marked completed, it seemed worth reporting rather -than upvoting a tracking issue. - -It is worth stressing that this is **not** about `Unpack` for `**kwargs` typing, -which #1889 tracks separately in #1746. The lead repro contains no `Unpack` and -no `**` at all, just `Movie[str].keys()`. The `**` message is only how I -happened to notice it. - -Structural assignability also still works (`Mapping[str, object] = m` is -accepted), so this looks narrowly scoped to the upper bound computed for the -synthesized `Self`. - -### Version - -0.0.74