From 9eab1c66a135e93f4504df742cecd9867f2d43b0 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 4 May 2026 11:42:45 +0200 Subject: [PATCH 1/7] Convert to PEP 695 type parameters and deprecate removed TypeVars - Convert 22 classes/functions to PEP 695 type parameter syntax (UP046/UP047) - Add noqa: UP046 with comments for 14 cases that require PEP 696 (TypeVar defaults), which needs Python 3.13+ - Fix ruff autofix bug in Base_SPDT.py (TINSTR vs _TINSTR mismatch) - Clean up unused TypeVar definitions and imports across ~13 files - Add _make_deprecated_typevars_getattr helper in deprecate.py for module-level __getattr__ deprecation warnings - Add deprecation warnings (QCoDeSDeprecationWarning) for 9 modules with removed public TypeVars, guarded by if not TYPE_CHECKING so type checkers still flag removed imports as errors - Add tests for the deprecation helper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pyproject.toml | 2 +- src/qcodes/dataset/data_set_cache.py | 18 +++- src/qcodes/dataset/dond/sweeps.py | 18 +++- src/qcodes/extensions/_driver_test_case.py | 7 +- src/qcodes/extensions/infer.py | 22 +++-- src/qcodes/instrument/instrument.py | 2 +- .../instrument_drivers/AlazarTech/ATS.py | 6 +- .../AlazarTech/dll_wrapper.py | 15 ++- .../instrument_drivers/AlazarTech/utils.py | 5 +- .../Keysight/Keysight_N9030B.py | 5 +- .../instrument_drivers/Keysight/KtM960x.py | 5 +- .../keysightb1500/KeysightB1500_base.py | 4 +- .../Keysight/keysightb1500/KeysightB1517A.py | 15 ++- .../Keysight/keysightb1500/message_builder.py | 15 ++- .../Minicircuits/Base_SPDT.py | 10 +- .../instrument_drivers/tektronix/DPO7200xx.py | 4 +- src/qcodes/parameters/array_parameter.py | 4 +- src/qcodes/parameters/command.py | 19 ++-- src/qcodes/parameters/delegate_parameter.py | 4 +- .../multi_channel_instrument_parameter.py | 6 +- src/qcodes/parameters/multi_parameter.py | 4 +- src/qcodes/parameters/parameter.py | 8 +- src/qcodes/parameters/parameter_base.py | 15 ++- .../parameters/parameter_with_setpoints.py | 4 +- src/qcodes/parameters/val_mapping.py | 16 +++- src/qcodes/utils/deep_update_utils.py | 18 +++- src/qcodes/utils/deprecate.py | 46 ++++++++++ src/qcodes/utils/threading_utils.py | 4 +- src/qcodes/validators/validators.py | 4 +- tests/common.py | 2 +- tests/utils/test_deprecate.py | 92 +++++++++++++++++++ 31 files changed, 321 insertions(+), 78 deletions(-) create mode 100644 tests/utils/test_deprecate.py diff --git a/pyproject.toml b/pyproject.toml index 71df0457956..7ed11252b26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -259,7 +259,7 @@ select = [ # it may be worth fixing some or these in the future # PYI036 disable until https://github.com/astral-sh/ruff/issues/9794 is fixed # UP040, UP046, UP047: PEP 695 type param conversions deferred — TypeVars use default/covariant features -ignore = ["E501", "G004", "PLR2004", "PLR0913", "PLR0911", "PLR0912", "PLR0915", "PLW0602", "PLW0603", "PLW2901", "PYI036", "UP040", "UP046", "UP047"] +ignore = ["E501", "G004", "PLR2004", "PLR0913", "PLR0911", "PLR0912", "PLR0915", "PLW0602", "PLW0603", "PLW2901", "PYI036", "UP040"] # we want to explicitly use the micro symbol # not the greek letter diff --git a/src/qcodes/dataset/data_set_cache.py b/src/qcodes/dataset/data_set_cache.py index 210baac1d18..d3d361d2ce0 100644 --- a/src/qcodes/dataset/data_set_cache.py +++ b/src/qcodes/dataset/data_set_cache.py @@ -2,7 +2,7 @@ import logging from pathlib import Path -from typing import TYPE_CHECKING, Generic, Literal, TypeVar +from typing import TYPE_CHECKING, Literal, TypeVar import numpy as np import numpy.typing as npt @@ -34,12 +34,10 @@ from .data_set_in_memory import DataSetInMem from .data_set_protocol import DataSetProtocol, ParameterData -DatasetType_co = TypeVar("DatasetType_co", bound="DataSetProtocol", covariant=True) - log = logging.getLogger(__name__) -class DataSetCache(Generic[DatasetType_co]): +class DataSetCache[DatasetType_co: "DataSetProtocol"]: """ The DataSetCache contains a in memory representation of the data in this dataset as well a a method to progressively read data @@ -572,3 +570,15 @@ def load_data_from_db(self) -> None: ) if not data_not_read: self._live = False + + +if not TYPE_CHECKING: + from qcodes.utils.deprecate import _make_deprecated_typevars_getattr + + _deprecated_typevars: dict[str, TypeVar] = { + "DatasetType_co": TypeVar( + "DatasetType_co", bound="DataSetProtocol", covariant=True + ), + } + + __getattr__ = _make_deprecated_typevars_getattr(__name__, _deprecated_typevars) diff --git a/src/qcodes/dataset/dond/sweeps.py b/src/qcodes/dataset/dond/sweeps.py index efef85f1d97..7f3062fe810 100644 --- a/src/qcodes/dataset/dond/sweeps.py +++ b/src/qcodes/dataset/dond/sweeps.py @@ -1,7 +1,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Generic, TypeVar +from typing import TYPE_CHECKING, Any, TypeVar import numpy as np import numpy.typing as npt @@ -12,10 +12,8 @@ from qcodes.dataset.dond.do_nd_utils import ActionsT from qcodes.parameters import ParameterBase -T = TypeVar("T", bound=np.generic) - -class AbstractSweep(ABC, Generic[T]): +class AbstractSweep[T: np.generic](ABC): """ Abstract sweep class that defines an interface for concrete sweep classes. """ @@ -195,7 +193,7 @@ def get_after_set(self) -> bool: return self._get_after_set -class ArraySweep(AbstractSweep, Generic[T]): +class ArraySweep[T: np.generic](AbstractSweep): """ Sweep the values of a given array. @@ -281,3 +279,13 @@ def get_setpoints(self) -> Iterable: @property def num_points(self) -> int: return self.sweeps[0].num_points + + +if not TYPE_CHECKING: + from qcodes.utils.deprecate import _make_deprecated_typevars_getattr + + _deprecated_typevars: dict[str, TypeVar] = { + "T": TypeVar("T", bound=np.generic), + } + + __getattr__ = _make_deprecated_typevars_getattr(__name__, _deprecated_typevars) diff --git a/src/qcodes/extensions/_driver_test_case.py b/src/qcodes/extensions/_driver_test_case.py index 900ff16065f..48e63dc5908 100644 --- a/src/qcodes/extensions/_driver_test_case.py +++ b/src/qcodes/extensions/_driver_test_case.py @@ -1,7 +1,7 @@ from __future__ import annotations import unittest -from typing import TYPE_CHECKING, Generic, TypeVar +from typing import TYPE_CHECKING if TYPE_CHECKING: from qcodes.instrument import Instrument @@ -25,10 +25,7 @@ """ -T = TypeVar("T", bound="Instrument") - - -class DriverTestCase(unittest.TestCase, Generic[T]): +class DriverTestCase[T: "Instrument"](unittest.TestCase): # override this in a subclass driver: type[T] | None = None instrument: T diff --git a/src/qcodes/extensions/infer.py b/src/qcodes/extensions/infer.py index a88c98783d0..dc31e1a8ca5 100644 --- a/src/qcodes/extensions/infer.py +++ b/src/qcodes/extensions/infer.py @@ -11,9 +11,6 @@ DOES_NOT_EXIST = "Does not exist" -C = TypeVar("C", bound=ParameterBase) -TInstrument = TypeVar("TInstrument", bound=InstrumentBase) - class InferError(AttributeError): ... @@ -225,7 +222,7 @@ def _merge_user_and_class_attrs( return set.union(set(alt_source_attrs), set(InferAttrs.known_attrs())) -def get_chain_links_of_type( +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""" @@ -237,7 +234,7 @@ def get_chain_links_of_type( return tuple(chain_links) -def get_sole_chain_link_of_type( +def get_sole_chain_link_of_type[C: ParameterBase]( link_param_type: type[C] | tuple[type[C], ...], parameter: Parameter ) -> C: """Gets the one parameter in a chain of linked parameters that matches a given type""" @@ -256,7 +253,7 @@ def get_sole_chain_link_of_type( return chain_links[0] -def get_parent_instruments_from_chain_of_type( +def get_parent_instruments_from_chain_of_type[TInstrument: InstrumentBase]( instrument_type: type[TInstrument] | tuple[type[TInstrument], ...], parameter: Parameter, ) -> tuple[TInstrument, ...]: @@ -272,7 +269,7 @@ def get_parent_instruments_from_chain_of_type( ) -def get_sole_parent_instrument_from_chain_of_type( +def get_sole_parent_instrument_from_chain_of_type[TInstrument: InstrumentBase]( instrument_type: type[TInstrument] | tuple[type[TInstrument], ...], parameter: Parameter, ) -> TInstrument: @@ -289,3 +286,14 @@ def get_sole_parent_instrument_from_chain_of_type( raise ValueError(f"{error_msg_1} {[instr.name for instr in instruments]}") return instruments[0] + + +if not TYPE_CHECKING: + from qcodes.utils.deprecate import _make_deprecated_typevars_getattr + + _deprecated_typevars: dict[str, TypeVar] = { + "C": TypeVar("C", bound=ParameterBase), + "TInstrument": TypeVar("TInstrument", bound=InstrumentBase), + } + + __getattr__ = _make_deprecated_typevars_getattr(__name__, _deprecated_typevars) diff --git a/src/qcodes/instrument/instrument.py b/src/qcodes/instrument/instrument.py index f3ed65f62cc..e4cc68cb231 100644 --- a/src/qcodes/instrument/instrument.py +++ b/src/qcodes/instrument/instrument.py @@ -459,7 +459,7 @@ def ask_raw(self, cmd: str) -> str: ) -def find_or_create_instrument( +def find_or_create_instrument[T: "Instrument"]( instrument_class: type[T], name: str, *args: Any, diff --git a/src/qcodes/instrument_drivers/AlazarTech/ATS.py b/src/qcodes/instrument_drivers/AlazarTech/ATS.py index 760dfb4137c..31f614dd9fc 100644 --- a/src/qcodes/instrument_drivers/AlazarTech/ATS.py +++ b/src/qcodes/instrument_drivers/AlazarTech/ATS.py @@ -6,7 +6,7 @@ import time import warnings from contextlib import contextmanager -from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast +from typing import TYPE_CHECKING, Any, TypeVar, cast import numpy as np import numpy.typing as npt @@ -827,7 +827,7 @@ def __del__(self) -> None: ) -class AcquisitionInterface(Generic[OutputType]): +class AcquisitionInterface[OutputType]: """ This class represents all choices that the end-user has to make regarding the data-acquisition. this class should be subclassed to program these @@ -903,7 +903,7 @@ def buffer_done_callback(self, buffers_completed: int) -> None: pass -class AcquisitionController(Instrument, AcquisitionInterface[Any], Generic[OutputType]): +class AcquisitionController[OutputType](Instrument, AcquisitionInterface[Any]): """ Compatibility class. The methods of :class:`AcquisitionController` have been extracted. This class is the base class fro AcquisitionInterfaces diff --git a/src/qcodes/instrument_drivers/AlazarTech/dll_wrapper.py b/src/qcodes/instrument_drivers/AlazarTech/dll_wrapper.py index d7573bb5836..425ab143f5e 100644 --- a/src/qcodes/instrument_drivers/AlazarTech/dll_wrapper.py +++ b/src/qcodes/instrument_drivers/AlazarTech/dll_wrapper.py @@ -31,9 +31,6 @@ # FUNCTIONS # -T = TypeVar("T") - - def _api_call_task( lock: Lock, c_func: Callable[..., int], callback: Callable[[], None], *args: Any ) -> int: @@ -43,7 +40,7 @@ def _api_call_task( return retval -def _normalize_params(*args: T) -> list[T]: +def _normalize_params[T](*args: T) -> list[T]: args_out: list[T] = [] for arg in args: if isinstance(arg, ParameterBase): @@ -205,3 +202,13 @@ def _sync_dll_call(self, c_name: str, *args: Any) -> Any: *_normalize_params(*args), ) return future.result() + + +if not TYPE_CHECKING: + from qcodes.utils.deprecate import _make_deprecated_typevars_getattr + + _deprecated_typevars: dict[str, TypeVar] = { + "T": TypeVar("T"), + } + + __getattr__ = _make_deprecated_typevars_getattr(__name__, _deprecated_typevars) diff --git a/src/qcodes/instrument_drivers/AlazarTech/utils.py b/src/qcodes/instrument_drivers/AlazarTech/utils.py index 34904c0562e..305638369b4 100644 --- a/src/qcodes/instrument_drivers/AlazarTech/utils.py +++ b/src/qcodes/instrument_drivers/AlazarTech/utils.py @@ -16,7 +16,10 @@ class TraceParameter( - Parameter[ParameterDataTypeVar, "AlazarTechATS"], Generic[ParameterDataTypeVar] + Parameter[ParameterDataTypeVar, "AlazarTechATS"], + # Generic can be replaced with PEP 695 type params once Python 3.12 + # support is dropped (TypeVars use default= which requires PEP 696) + Generic[ParameterDataTypeVar], # noqa: UP046 ): """ A parameter that keeps track of if its value has been synced to diff --git a/src/qcodes/instrument_drivers/Keysight/Keysight_N9030B.py b/src/qcodes/instrument_drivers/Keysight/Keysight_N9030B.py index a6dd21a8a3f..78275f66144 100644 --- a/src/qcodes/instrument_drivers/Keysight/Keysight_N9030B.py +++ b/src/qcodes/instrument_drivers/Keysight/Keysight_N9030B.py @@ -62,7 +62,10 @@ def get_raw(self) -> npt.NDArray[np.float64]: class Trace( - ParameterWithSetpoints[ParameterDataTypeVar, _T], Generic[ParameterDataTypeVar, _T] + ParameterWithSetpoints[ParameterDataTypeVar, _T], + # Generic can be replaced with PEP 695 type params once Python 3.12 + # support is dropped (TypeVars use default= which requires PEP 696) + Generic[ParameterDataTypeVar, _T], # noqa: UP046 ): def __init__( self, diff --git a/src/qcodes/instrument_drivers/Keysight/KtM960x.py b/src/qcodes/instrument_drivers/Keysight/KtM960x.py index f3af627c2ec..896a0904e83 100644 --- a/src/qcodes/instrument_drivers/Keysight/KtM960x.py +++ b/src/qcodes/instrument_drivers/Keysight/KtM960x.py @@ -19,7 +19,10 @@ class Measure( - MultiParameter[ParameterDataTypeVar, "KeysightM960x"], Generic[ParameterDataTypeVar] + MultiParameter[ParameterDataTypeVar, "KeysightM960x"], + # Generic can be replaced with PEP 695 type params once Python 3.12 + # support is dropped (TypeVars use default= which requires PEP 696) + Generic[ParameterDataTypeVar], # noqa: UP046 ): def __init__(self, name: str, instrument: "KeysightM960x") -> None: super().__init__( diff --git a/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1500_base.py b/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1500_base.py index 1a91dc44cc7..55a38a3421f 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1500_base.py +++ b/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1500_base.py @@ -499,7 +499,9 @@ def enable_smu_filters( class IVSweepMeasurement( MultiParameter[ParameterDataTypeVar, KeysightB1500], StatusMixin, - Generic[ParameterDataTypeVar], + # Generic can be replaced with PEP 695 type params once Python 3.12 + # support is dropped (TypeVars use default= which requires PEP 696) + Generic[ParameterDataTypeVar], # noqa: UP046 ): """ IV sweep measurement outputs a list of measured current parameters diff --git a/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1517A.py b/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1517A.py index 3b08e2c361c..94e67aab13a 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1517A.py +++ b/src/qcodes/instrument_drivers/Keysight/keysightb1500/KeysightB1517A.py @@ -697,7 +697,10 @@ def _get_sweep_steps_parser(response: str) -> SweepSteps: class _ParameterWithStatus( - Parameter[ParameterDataTypeVar, "KeysightB1517A"], Generic[ParameterDataTypeVar] + Parameter[ParameterDataTypeVar, "KeysightB1517A"], + # Generic can be replaced with PEP 695 type params once Python 3.12 + # support is dropped (TypeVars use default= which requires PEP 696) + Generic[ParameterDataTypeVar], # noqa: UP046 ): def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) @@ -724,7 +727,10 @@ def snapshot_base( class _SpotMeasurementVoltageParameter( - _ParameterWithStatus[ParameterDataTypeVar], Generic[ParameterDataTypeVar] + _ParameterWithStatus[ParameterDataTypeVar], + # Generic can be replaced with PEP 695 type params once Python 3.12 + # support is dropped (TypeVars use default= which requires PEP 696) + Generic[ParameterDataTypeVar], # noqa: UP046 ): def set_raw(self, value: ParamRawDataType) -> None: smu = self.instrument @@ -767,7 +773,10 @@ def get_raw(self) -> ParamRawDataType: class _SpotMeasurementCurrentParameter( - _ParameterWithStatus[ParameterDataTypeVar], Generic[ParameterDataTypeVar] + _ParameterWithStatus[ParameterDataTypeVar], + # Generic can be replaced with PEP 695 type params once Python 3.12 + # support is dropped (TypeVars use default= which requires PEP 696) + Generic[ParameterDataTypeVar], # noqa: UP046 ): def set_raw(self, value: ParamRawDataType) -> None: smu = self.instrument diff --git a/src/qcodes/instrument_drivers/Keysight/keysightb1500/message_builder.py b/src/qcodes/instrument_drivers/Keysight/keysightb1500/message_builder.py index 5bf2cd0d07d..8162ada6834 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysightb1500/message_builder.py +++ b/src/qcodes/instrument_drivers/Keysight/keysightb1500/message_builder.py @@ -1,6 +1,6 @@ from functools import wraps from operator import xor -from typing import TYPE_CHECKING, Generic, ParamSpec, TypeVar +from typing import TYPE_CHECKING, ParamSpec, TypeVar from . import constants @@ -14,7 +14,6 @@ def as_csv(comps: "Iterable[object]", sep: str = ",") -> str: P = ParamSpec("P") -T = TypeVar("T") def final_command(f: "Callable[P, MessageBuilder]") -> "Callable[P, MessageBuilder]": @@ -28,7 +27,7 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> "MessageBuilder": return wrapper -class CommandList(list[T], Generic[T]): +class CommandList[T](list[T]): def __init__(self) -> None: super().__init__() self.is_final = False @@ -3984,3 +3983,13 @@ def xe(self) -> "MessageBuilder": self._msg.append(cmd) return self + + +if not TYPE_CHECKING: + from qcodes.utils.deprecate import _make_deprecated_typevars_getattr + + _deprecated_typevars: dict[str, TypeVar] = { + "T": TypeVar("T"), + } + + __getattr__ = _make_deprecated_typevars_getattr(__name__, _deprecated_typevars) diff --git a/src/qcodes/instrument_drivers/Minicircuits/Base_SPDT.py b/src/qcodes/instrument_drivers/Minicircuits/Base_SPDT.py index 8f00d6a9bd7..48770b966a1 100644 --- a/src/qcodes/instrument_drivers/Minicircuits/Base_SPDT.py +++ b/src/qcodes/instrument_drivers/Minicircuits/Base_SPDT.py @@ -2,7 +2,7 @@ import logging import re -from typing import TYPE_CHECKING, Generic, TypeVar +from typing import TYPE_CHECKING from qcodes.instrument import ( ChannelList, @@ -19,13 +19,13 @@ log = logging.getLogger(__name__) -_TINSTR = TypeVar("_TINSTR", bound="MiniCircuitsSPDTBase") - -class MiniCircuitsSPDTSwitchChannelBase(InstrumentChannel[_TINSTR], Generic[_TINSTR]): +class MiniCircuitsSPDTSwitchChannelBase[TINSTR: "MiniCircuitsSPDTBase"]( + InstrumentChannel[TINSTR] +): def __init__( self, - parent: _TINSTR, + parent: TINSTR, name: str, channel_letter: str, **kwargs: Unpack[InstrumentBaseKWArgs], diff --git a/src/qcodes/instrument_drivers/tektronix/DPO7200xx.py b/src/qcodes/instrument_drivers/tektronix/DPO7200xx.py index 41e5a5bbdc4..72a116bfe91 100644 --- a/src/qcodes/instrument_drivers/tektronix/DPO7200xx.py +++ b/src/qcodes/instrument_drivers/tektronix/DPO7200xx.py @@ -947,7 +947,9 @@ def _trigger_type(self, value: str) -> None: class TektronixDPOMeasurementParameter( Parameter[ParameterDataTypeVar, "TektronixDPOMeasurement"], - Generic[ParameterDataTypeVar], + # Generic can be replaced with PEP 695 type params once Python 3.12 + # support is dropped (TypeVars use default= which requires PEP 696) + Generic[ParameterDataTypeVar], # noqa: UP046 ): """ A measurement parameter does not only return the instantaneous value diff --git a/src/qcodes/parameters/array_parameter.py b/src/qcodes/parameters/array_parameter.py index 196fd600b01..d3565f83f02 100644 --- a/src/qcodes/parameters/array_parameter.py +++ b/src/qcodes/parameters/array_parameter.py @@ -48,7 +48,9 @@ class ArrayParameter( ParameterBase[ParameterDataTypeVar, InstrumentTypeVar_co], - Generic[ParameterDataTypeVar, InstrumentTypeVar_co], + # Generic can be replaced with PEP 695 type params once Python 3.12 + # support is dropped (TypeVars use default= which requires PEP 696) + Generic[ParameterDataTypeVar, InstrumentTypeVar_co], # noqa: UP046 ): """ A gettable parameter that returns an array of values. diff --git a/src/qcodes/parameters/command.py b/src/qcodes/parameters/command.py index 5a2ddf5287d..e2da0b14d01 100644 --- a/src/qcodes/parameters/command.py +++ b/src/qcodes/parameters/command.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar +from typing import TYPE_CHECKING, Any, Literal, TypeVar from qcodes.utils import is_function @@ -12,11 +12,7 @@ class NoCommandError(Exception): pass -Output = TypeVar("Output") -ParsedOutput = TypeVar("ParsedOutput") - - -class Command(Generic[Output, ParsedOutput]): +class Command[Output, ParsedOutput]: """ Create a callable command from a string or function. @@ -211,3 +207,14 @@ def __call__(self, *args: Any) -> Output | ParsedOutput: if len(args) != self.arg_count: raise TypeError(f"command takes exactly {self.arg_count} args") return self.exec_function(*args) + + +if not TYPE_CHECKING: + from qcodes.utils.deprecate import _make_deprecated_typevars_getattr + + _deprecated_typevars: dict[str, TypeVar] = { + "Output": TypeVar("Output"), + "ParsedOutput": TypeVar("ParsedOutput"), + } + + __getattr__ = _make_deprecated_typevars_getattr(__name__, _deprecated_typevars) diff --git a/src/qcodes/parameters/delegate_parameter.py b/src/qcodes/parameters/delegate_parameter.py index 566a42f22b1..c04b1521e0c 100644 --- a/src/qcodes/parameters/delegate_parameter.py +++ b/src/qcodes/parameters/delegate_parameter.py @@ -34,7 +34,9 @@ class DelegateParameter( Parameter[ParameterDataTypeVar, InstrumentTypeVar_co], - Generic[ParameterDataTypeVar, InstrumentTypeVar_co], + # Generic can be replaced with PEP 695 type params once Python 3.12 + # support is dropped (TypeVars use default= which requires PEP 696) + Generic[ParameterDataTypeVar, InstrumentTypeVar_co], # noqa: UP046 ): """ The :class:`.DelegateParameter` wraps a given `source` :class:`Parameter`. diff --git a/src/qcodes/parameters/multi_channel_instrument_parameter.py b/src/qcodes/parameters/multi_channel_instrument_parameter.py index d7055857480..a06f7f0e67b 100644 --- a/src/qcodes/parameters/multi_channel_instrument_parameter.py +++ b/src/qcodes/parameters/multi_channel_instrument_parameter.py @@ -1,7 +1,7 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Any, Generic, TypeVar +from typing import TYPE_CHECKING, Any, TypeVar from .multi_parameter import MultiParameter @@ -16,7 +16,9 @@ _LOG = logging.getLogger(__name__) -class MultiChannelInstrumentParameter(MultiParameter, Generic[InstrumentModuleType]): +class MultiChannelInstrumentParameter[InstrumentModuleType: "InstrumentModule"]( + MultiParameter +): """ Parameter to get or set multiple channels simultaneously. diff --git a/src/qcodes/parameters/multi_parameter.py b/src/qcodes/parameters/multi_parameter.py index 448534a5108..0f230b69334 100644 --- a/src/qcodes/parameters/multi_parameter.py +++ b/src/qcodes/parameters/multi_parameter.py @@ -57,7 +57,9 @@ def _is_nested_sequence_or_none( class MultiParameter( ParameterBase[ParameterDataTypeVar, InstrumentTypeVar_co], - Generic[ParameterDataTypeVar, InstrumentTypeVar_co], + # Generic can be replaced with PEP 695 type params once Python 3.12 + # support is dropped (TypeVars use default= which requires PEP 696) + Generic[ParameterDataTypeVar, InstrumentTypeVar_co], # noqa: UP046 ): """ A gettable parameter that returns multiple values with separate names, diff --git a/src/qcodes/parameters/parameter.py b/src/qcodes/parameters/parameter.py index 11882689cee..47626681be8 100644 --- a/src/qcodes/parameters/parameter.py +++ b/src/qcodes/parameters/parameter.py @@ -34,7 +34,9 @@ class ParameterKWArgs( TypedDict, - Generic[ParameterDataTypeVar, InstrumentTypeVar_co], + # Generic can be replaced with PEP 695 type params once Python 3.12 + # support is dropped (TypeVars use default= which requires PEP 696) + Generic[ParameterDataTypeVar, InstrumentTypeVar_co], # noqa: UP046 ): """ This TypedDict defines the type of the kwargs that can be passed to @@ -198,7 +200,9 @@ class ParameterKWArgs( class Parameter( ParameterBase[ParameterDataTypeVar, InstrumentTypeVar_co], - Generic[ParameterDataTypeVar, InstrumentTypeVar_co], + # Generic can be replaced with PEP 695 type params once Python 3.12 + # support is dropped (TypeVars use default= which requires PEP 696) + Generic[ParameterDataTypeVar, InstrumentTypeVar_co], # noqa: UP046 ): """ A parameter represents a single degree of freedom. Most often, diff --git a/src/qcodes/parameters/parameter_base.py b/src/qcodes/parameters/parameter_base.py index c009565b011..a14095a4fc5 100644 --- a/src/qcodes/parameters/parameter_base.py +++ b/src/qcodes/parameters/parameter_base.py @@ -1472,11 +1472,8 @@ def __call__(self) -> ParameterDataTypeVar: return self.cache() -P = TypeVar("P", bound=ParameterBase) - - # Does not implement __hash__, not clear it needs to -class ParameterSet(MutableSet[P], Generic[P]): # noqa: PLW1641 +class ParameterSet[P: ParameterBase](MutableSet[P]): # noqa: PLW1641 """A set-like container that preserves the insertion order of its parameters. This class implements the common set interface methods while maintaining @@ -1592,3 +1589,13 @@ def __ge__(self, other: object) -> bool: raise NotImplementedError( f">+ operation is not defined between ParameterSet and {type(other)}" ) + + +if not TYPE_CHECKING: + from qcodes.utils.deprecate import _make_deprecated_typevars_getattr + + _deprecated_typevars: dict[str, TypeVar] = { + "P": TypeVar("P", bound="ParameterBase"), + } + + __getattr__ = _make_deprecated_typevars_getattr(__name__, _deprecated_typevars) diff --git a/src/qcodes/parameters/parameter_with_setpoints.py b/src/qcodes/parameters/parameter_with_setpoints.py index 4b684c9acd8..d7897c58961 100644 --- a/src/qcodes/parameters/parameter_with_setpoints.py +++ b/src/qcodes/parameters/parameter_with_setpoints.py @@ -29,7 +29,9 @@ class ParameterWithSetpoints( Parameter[ParameterDataTypeVar, InstrumentTypeVar_co], - Generic[ParameterDataTypeVar, InstrumentTypeVar_co], + # Generic can be replaced with PEP 695 type params once Python 3.12 + # support is dropped (TypeVars use default= which requires PEP 696) + Generic[ParameterDataTypeVar, InstrumentTypeVar_co], # noqa: UP046 ): """ A parameter that has associated setpoints. The setpoints is nothing diff --git a/src/qcodes/parameters/val_mapping.py b/src/qcodes/parameters/val_mapping.py index d8076179616..9e5aad72c24 100644 --- a/src/qcodes/parameters/val_mapping.py +++ b/src/qcodes/parameters/val_mapping.py @@ -1,12 +1,10 @@ from __future__ import annotations from collections import OrderedDict -from typing import TypeVar +from typing import TYPE_CHECKING, TypeVar -T = TypeVar("T") - -def create_on_off_val_mapping( +def create_on_off_val_mapping[T]( on_val: T | bool = True, off_val: T | bool = False ) -> OrderedDict[str | bool, T | bool]: """ @@ -32,3 +30,13 @@ def create_on_off_val_mapping( offs = (*offs_, False) all_vals_tuples = [(on, on_val) for on in ons] + [(off, off_val) for off in offs] return OrderedDict(all_vals_tuples) + + +if not TYPE_CHECKING: + from qcodes.utils.deprecate import _make_deprecated_typevars_getattr + + _deprecated_typevars: dict[str, TypeVar] = { + "T": TypeVar("T"), + } + + __getattr__ = _make_deprecated_typevars_getattr(__name__, _deprecated_typevars) diff --git a/src/qcodes/utils/deep_update_utils.py b/src/qcodes/utils/deep_update_utils.py index 6090293e2c4..4ff5eac9342 100644 --- a/src/qcodes/utils/deep_update_utils.py +++ b/src/qcodes/utils/deep_update_utils.py @@ -1,13 +1,10 @@ from collections import abc from collections.abc import Hashable, Mapping, MutableMapping from copy import deepcopy -from typing import Any, TypeVar, cast +from typing import TYPE_CHECKING, Any, TypeVar, cast -K = TypeVar("K", bound=Hashable) -L = TypeVar("L", bound=Hashable) - -def deep_update( +def deep_update[K: Hashable, L: Hashable]( dest: MutableMapping[K, Any], update: Mapping[L, Any] ) -> MutableMapping[K | L, Any]: """ @@ -25,3 +22,14 @@ def deep_update( else: dest_int[k] = deepcopy(v_update) return dest_int + + +if not TYPE_CHECKING: + from qcodes.utils.deprecate import _make_deprecated_typevars_getattr + + _deprecated_typevars: dict[str, TypeVar] = { + "K": TypeVar("K", bound=Hashable), + "L": TypeVar("L", bound=Hashable), + } + + __getattr__ = _make_deprecated_typevars_getattr(__name__, _deprecated_typevars) diff --git a/src/qcodes/utils/deprecate.py b/src/qcodes/utils/deprecate.py index c5c6e8ac43d..2032254a591 100644 --- a/src/qcodes/utils/deprecate.py +++ b/src/qcodes/utils/deprecate.py @@ -1,5 +1,51 @@ +from __future__ import annotations + +import warnings +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from collections.abc import Callable + + class QCoDeSDeprecationWarning(RuntimeWarning): """ A DeprecationWarning used internally in QCoDeS. This fixes `DeprecationWarning` being suppressed by default. """ + + +def _make_deprecated_typevars_getattr( + module_name: str, + deprecated: dict[str, Any], + fallback: Callable[[str], Any] | None = None, +) -> Callable[[str], Any]: + """Return a module-level ``__getattr__`` that emits deprecation warnings + for removed TypeVar / type-alias names. + + Args: + module_name: The ``__name__`` of the calling module. + deprecated: Mapping of ``{name: object}`` for names that should + still be importable but trigger a warning. + fallback: Optional existing ``__getattr__`` to delegate to for + names not in *deprecated*. + + Returns: + A ``__getattr__`` function suitable for assignment at module level. + + """ + + def __getattr__(name: str) -> Any: + if name in deprecated: + warnings.warn( + f"Importing {name!r} from {module_name!r} is deprecated. " + f"This TypeVar is no longer used and will be removed in a " + f"future version.", + QCoDeSDeprecationWarning, + stacklevel=2, + ) + return deprecated[name] + if fallback is not None: + return fallback(name) + raise AttributeError(f"module {module_name!r} has no attribute {name!r}") + + return __getattr__ diff --git a/src/qcodes/utils/threading_utils.py b/src/qcodes/utils/threading_utils.py index 552c51e79a7..b67f5ed83ef 100644 --- a/src/qcodes/utils/threading_utils.py +++ b/src/qcodes/utils/threading_utils.py @@ -1,6 +1,6 @@ import logging import threading -from typing import TYPE_CHECKING, Any, Generic, Optional, TypeVar +from typing import TYPE_CHECKING, Any, Optional, TypeVar if TYPE_CHECKING: from collections.abc import Callable, Sequence @@ -10,7 +10,7 @@ T = TypeVar("T") -class RespondingThread(threading.Thread, Generic[T]): +class RespondingThread[T](threading.Thread): """ Thread subclass for parallelizing execution. Behaves like a regular thread but returns a value from target, and propagates diff --git a/src/qcodes/validators/validators.py b/src/qcodes/validators/validators.py index 37beb98fbf8..a3ac3e726d8 100644 --- a/src/qcodes/validators/validators.py +++ b/src/qcodes/validators/validators.py @@ -9,7 +9,7 @@ import typing from collections import abc from collections.abc import Hashable -from typing import Any, Generic, Literal, TypeVar, cast, get_args +from typing import Any, Literal, TypeVar, cast, get_args import numpy as np import numpy.typing as npt @@ -65,7 +65,7 @@ def range_str( T = TypeVar("T") -class Validator(Generic[T]): +class Validator[T]: """ Base class for all value validators each validator should implement: diff --git a/tests/common.py b/tests/common.py index 3d2ea62c3f1..0c2da5080b9 100644 --- a/tests/common.py +++ b/tests/common.py @@ -78,7 +78,7 @@ def func_retry(*args: P.args, **kwargs: P.kwargs) -> T: return retry_until_passes_decorator -def profile(func: Callable[P, T]) -> Callable[P, T]: +def profile[**P, T](func: Callable[P, T]) -> Callable[P, T]: """ Decorator that profiles the wrapped function with cProfile. diff --git a/tests/utils/test_deprecate.py b/tests/utils/test_deprecate.py new file mode 100644 index 00000000000..09f07ed606c --- /dev/null +++ b/tests/utils/test_deprecate.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from typing import TypeVar + +import pytest + +from qcodes.utils.deprecate import ( + QCoDeSDeprecationWarning, + _make_deprecated_typevars_getattr, +) + + +@pytest.fixture +def deprecated_getattr() -> tuple[dict[str, TypeVar], callable]: + """Create a __getattr__ with two deprecated TypeVars.""" + deprecated = { + "MyT": TypeVar("MyT"), + "MyK": TypeVar("MyK", bound=int), + } + getattr_fn = _make_deprecated_typevars_getattr("fake.module", deprecated) + return deprecated, getattr_fn + + +def test_returns_typevar_and_warns( + deprecated_getattr: tuple[dict[str, TypeVar], callable], +) -> None: + deprecated, getattr_fn = deprecated_getattr + with pytest.warns(QCoDeSDeprecationWarning, match="'MyT'.*'fake.module'"): + result = getattr_fn("MyT") + assert result is deprecated["MyT"] + + +def test_warns_for_each_deprecated_name( + deprecated_getattr: tuple[dict[str, TypeVar], callable], +) -> None: + deprecated, getattr_fn = deprecated_getattr + with pytest.warns(QCoDeSDeprecationWarning, match="'MyK'"): + result = getattr_fn("MyK") + assert result is deprecated["MyK"] + + +def test_unknown_name_raises_attribute_error( + deprecated_getattr: tuple[dict[str, TypeVar], callable], +) -> None: + _, getattr_fn = deprecated_getattr + with pytest.raises(AttributeError, match="fake.module"): + getattr_fn("DoesNotExist") + + +def test_repeated_access_returns_same_object( + deprecated_getattr: tuple[dict[str, TypeVar], callable], +) -> None: + deprecated, getattr_fn = deprecated_getattr + with pytest.warns(QCoDeSDeprecationWarning): + first = getattr_fn("MyT") + with pytest.warns(QCoDeSDeprecationWarning): + second = getattr_fn("MyT") + assert first is second + assert first is deprecated["MyT"] + + +def test_fallback_is_called_for_unknown_names() -> None: + deprecated: dict[str, TypeVar] = {"X": TypeVar("X")} + + def fallback(name: str) -> str: + return f"fallback:{name}" + + getattr_fn = _make_deprecated_typevars_getattr("mod", deprecated, fallback=fallback) + assert getattr_fn("something") == "fallback:something" + + +def test_fallback_not_called_for_deprecated_names() -> None: + deprecated: dict[str, TypeVar] = {"X": TypeVar("X")} + fallback_called = False + + def fallback(name: str) -> str: + nonlocal fallback_called + fallback_called = True + return name + + getattr_fn = _make_deprecated_typevars_getattr("mod", deprecated, fallback=fallback) + with pytest.warns(QCoDeSDeprecationWarning): + getattr_fn("X") + assert not fallback_called + + +def test_real_module_import_triggers_warning() -> None: + """Test that importing a deprecated TypeVar from an actual module works.""" + with pytest.warns(QCoDeSDeprecationWarning, match="'K'"): + from qcodes.utils.deep_update_utils import K + + assert isinstance(K, TypeVar) From 453c81c0bc845a90b33dd85347a19334ce0b5bca Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 4 May 2026 11:44:59 +0200 Subject: [PATCH 2/7] Add newsfragment for PR #8096 Also deprecate 3 additional pre-existing unused TypeVars in field_vector.py, threading.py, and channel.py. Fix pyright errors in test_deprecate.py. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/changes/newsfragments/8096.breaking | 3 ++ src/qcodes/dataset/measurements.py | 18 +++++++++--- src/qcodes/dataset/threading.py | 12 ++++++-- .../extensions/parameters/parameter_mixin.py | 21 ++++++++++---- src/qcodes/instrument/channel.py | 26 +++++++++++------ src/qcodes/instrument/instrument.py | 20 ++++++++++--- src/qcodes/instrument/instrument_base.py | 1 + .../instrument_drivers/AlazarTech/ATS.py | 19 ++++++++++--- .../Keithley/Keithley_6500.py | 19 ++++++++++--- .../Keysight/Keysight_N9030B.py | 1 + .../Keysight/keysight_34980a.py | 27 ++++++++++++------ .../Keysight/keysight_b220x.py | 28 +++++++++++++------ .../Lakeshore/lakeshore_base.py | 1 + .../QuantumDesign/DynaCoolPPMS/DynaCool.py | 7 ++--- .../american_magnetics/AMI430_visa.py | 26 ++++++++++++----- src/qcodes/math_utils/field_vector.py | 11 +++++++- src/qcodes/parameters/cache.py | 3 +- src/qcodes/parameters/delegate_parameter.py | 5 ++-- .../multi_channel_instrument_parameter.py | 18 ++++++++++-- src/qcodes/parameters/parameter_base.py | 2 ++ src/qcodes/utils/abstractmethod.py | 25 ++++++++++++----- src/qcodes/utils/snapshot_helpers.py | 19 ++++++++++--- src/qcodes/utils/threading_utils.py | 19 ++++++++++--- src/qcodes/validators/validators.py | 21 ++++++++++---- tests/utils/test_deprecate.py | 23 +++++++++------ 25 files changed, 279 insertions(+), 96 deletions(-) create mode 100644 docs/changes/newsfragments/8096.breaking diff --git a/docs/changes/newsfragments/8096.breaking b/docs/changes/newsfragments/8096.breaking new file mode 100644 index 00000000000..cc9c0924e7b --- /dev/null +++ b/docs/changes/newsfragments/8096.breaking @@ -0,0 +1,3 @@ +Several module-level ``TypeVar`` definitions that are no longer used internally have been +deprecated. Importing them will emit a ``QCoDeSDeprecationWarning``. They will be removed +in a future version. diff --git a/src/qcodes/dataset/measurements.py b/src/qcodes/dataset/measurements.py index cb11a9246af..fd94546d7e2 100644 --- a/src/qcodes/dataset/measurements.py +++ b/src/qcodes/dataset/measurements.py @@ -18,7 +18,7 @@ from itertools import chain from numbers import Number from time import perf_counter, perf_counter_ns -from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar, cast +from typing import TYPE_CHECKING, Any, TypeAlias, cast import numpy as np import numpy.typing as npt @@ -804,9 +804,6 @@ def __exit__( self._exit_stack.close() -T = TypeVar("T", bound="Measurement") - - class Measurement: """ Measurement procedure container. Note that multiple measurement @@ -1616,3 +1613,16 @@ def _numeric_values_are_equal( ): return False return True + + +if not TYPE_CHECKING: + from typing import TypeVar + + from qcodes.utils.deprecate import _make_deprecated_typevars_getattr + + __getattr__ = _make_deprecated_typevars_getattr( + __name__, + { + "T": TypeVar("T", bound="Measurement"), + }, + ) diff --git a/src/qcodes/dataset/threading.py b/src/qcodes/dataset/threading.py index 34a9d9be8cd..8300f01a11e 100644 --- a/src/qcodes/dataset/threading.py +++ b/src/qcodes/dataset/threading.py @@ -24,8 +24,6 @@ ParamMeasT: TypeAlias = "ParameterBase | Callable[[], None]" OutType: TypeAlias = "list[tuple[ParameterBase, ValuesType]]" -T = TypeVar("T") - _LOG = logging.getLogger(__name__) @@ -221,3 +219,13 @@ def __exit__( exc_tb: TracebackType | None, ) -> None: self._thread_pool.__exit__(exc_type, exc_val, exc_tb) + + +if not TYPE_CHECKING: + from qcodes.utils.deprecate import _make_deprecated_typevars_getattr + + _deprecated_typevars: dict[str, TypeVar] = { + "T": TypeVar("T"), + } + + __getattr__ = _make_deprecated_typevars_getattr(__name__, _deprecated_typevars) diff --git a/src/qcodes/extensions/parameters/parameter_mixin.py b/src/qcodes/extensions/parameters/parameter_mixin.py index c5e8eb6df58..c63765588a5 100644 --- a/src/qcodes/extensions/parameters/parameter_mixin.py +++ b/src/qcodes/extensions/parameters/parameter_mixin.py @@ -24,12 +24,10 @@ import logging import warnings -from typing import Any, ClassVar, TypeVar +from typing import TYPE_CHECKING, Any, ClassVar from qcodes.parameters import ParameterBase -T = TypeVar("T") - log = logging.getLogger(__name__) @@ -245,7 +243,7 @@ def _update_docstring( cls.__doc__ = (original_doc.strip() + "\n\n" + additional_doc).strip() @classmethod - def _get_leaf_classes( + def _get_leaf_classes[T]( cls, base_type: type[T], exclude_base_type: type | None = None ) -> list[type[T]]: """ @@ -289,7 +287,7 @@ def _get_leaf_classes( return leaf_classes @classmethod - def _get_mixin_classes( + def _get_mixin_classes[T]( cls, base_type: type[T], exclude_base_type: type | None = None ) -> list[type[T]]: """ @@ -321,3 +319,16 @@ def _get_mixin_classes( ] return mixin_classes + + +if not TYPE_CHECKING: + from typing import TypeVar + + from qcodes.utils.deprecate import _make_deprecated_typevars_getattr + + __getattr__ = _make_deprecated_typevars_getattr( + __name__, + { + "T": TypeVar("T"), + }, + ) diff --git a/src/qcodes/instrument/channel.py b/src/qcodes/instrument/channel.py index 8cd63ff7a3d..5d35d3c483d 100644 --- a/src/qcodes/instrument/channel.py +++ b/src/qcodes/instrument/channel.py @@ -15,7 +15,6 @@ MultiParameter, Parameter, ) -from qcodes.parameters.multi_channel_instrument_parameter import InstrumentModuleType from qcodes.utils import full_class from qcodes.validators import Validator @@ -27,6 +26,7 @@ from .instrument_base import InstrumentBaseKWArgs +# Cannot convert to PEP 695: uses default= and covariant= which require PEP 696 (Python 3.13+). _TIB_co = TypeVar( "_TIB_co", bound="InstrumentBase", default=InstrumentBase, covariant=True ) @@ -98,10 +98,7 @@ class InstrumentChannel(InstrumentModule[_TIB_co], Generic[_TIB_co]): pass -T = TypeVar("T", bound="ChannelTuple") - - -class ChannelTuple(MetadatableWithName, Sequence[InstrumentModuleType]): +class ChannelTuple[InstrumentModuleType: "InstrumentModule"](MetadatableWithName, Sequence[InstrumentModuleType]): """ Container for channelized parameters that allows for sweeps over all channels, as well as addressing of individual channels. @@ -628,7 +625,7 @@ def invalidate_cache(self) -> None: # in index method the parameter obj should be called value but that would # be an incompatible change -class ChannelList( # pyright: ignore[reportIncompatibleMethodOverride] +class ChannelList[InstrumentModuleType: "InstrumentModule"]( # pyright: ignore[reportIncompatibleMethodOverride] ChannelTuple[InstrumentModuleType], MutableSequence[InstrumentModuleType] ): """ @@ -1161,10 +1158,8 @@ def exists_on_instrument(self) -> bool: return self._exists_on_instrument -TAUTORELOADCHANNEL = TypeVar("TAUTORELOADCHANNEL", bound=AutoLoadableInstrumentChannel) - -class AutoLoadableChannelList(ChannelList[TAUTORELOADCHANNEL]): +class AutoLoadableChannelList[TAUTORELOADCHANNEL: AutoLoadableInstrumentChannel](ChannelList[TAUTORELOADCHANNEL]): """ Extends the QCoDeS :class:`ChannelList` class to add the following features: - Automatically create channel objects on initialization @@ -1243,3 +1238,16 @@ def add(self, **kwargs: Any) -> TAUTORELOADCHANNEL: self.append(new_channel) return new_channel + + +if not TYPE_CHECKING: + from qcodes.utils.deprecate import _make_deprecated_typevars_getattr + + _deprecated_typevars: dict[str, TypeVar] = { + "T": TypeVar("T", bound="ChannelTuple"), + "TAUTORELOADCHANNEL": TypeVar( + "TAUTORELOADCHANNEL", bound=AutoLoadableInstrumentChannel + ), + } + + __getattr__ = _make_deprecated_typevars_getattr(__name__, _deprecated_typevars) diff --git a/src/qcodes/instrument/instrument.py b/src/qcodes/instrument/instrument.py index e4cc68cb231..a0490427747 100644 --- a/src/qcodes/instrument/instrument.py +++ b/src/qcodes/instrument/instrument.py @@ -5,7 +5,7 @@ import logging import time import weakref -from typing import TYPE_CHECKING, Any, Protocol, TypeVar, overload +from typing import TYPE_CHECKING, Any, Protocol, overload from qcodes.utils import strip_attrs from qcodes.validators import Anything @@ -31,7 +31,6 @@ def ask(self, cmd: str) -> str: ... def write(self, cmd: str) -> None: ... -T = TypeVar("T", bound="Instrument") # a metaclass that overrides __call__ means that we lose # both the args and return type hints. @@ -280,10 +279,10 @@ def find_instrument( @overload @classmethod - def find_instrument(cls, name: str, instrument_class: type[T]) -> T: ... + def find_instrument[T: "Instrument"](cls, name: str, instrument_class: type[T]) -> T: ... @classmethod - def find_instrument( + def find_instrument[T: "Instrument"]( cls, name: str, instrument_class: type[T] | None = None ) -> T | Instrument: """ @@ -504,3 +503,16 @@ def find_or_create_instrument[T: "Instrument"]( instrument.connect_message() # prints the message return instrument + + +if not TYPE_CHECKING: + from typing import TypeVar + + from qcodes.utils.deprecate import _make_deprecated_typevars_getattr + + __getattr__ = _make_deprecated_typevars_getattr( + __name__, + { + "T": TypeVar("T", bound="Instrument"), + }, + ) diff --git a/src/qcodes/instrument/instrument_base.py b/src/qcodes/instrument/instrument_base.py index 52894a2e728..77a78602a76 100644 --- a/src/qcodes/instrument/instrument_base.py +++ b/src/qcodes/instrument/instrument_base.py @@ -27,6 +27,7 @@ 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") TSubmodule = TypeVar( "TSubmodule", bound="InstrumentModule | ChannelTuple", default="InstrumentModule" diff --git a/src/qcodes/instrument_drivers/AlazarTech/ATS.py b/src/qcodes/instrument_drivers/AlazarTech/ATS.py index 31f614dd9fc..90c4932d322 100644 --- a/src/qcodes/instrument_drivers/AlazarTech/ATS.py +++ b/src/qcodes/instrument_drivers/AlazarTech/ATS.py @@ -6,7 +6,7 @@ import time import warnings from contextlib import contextmanager -from typing import TYPE_CHECKING, Any, TypeVar, cast +from typing import TYPE_CHECKING, Any, cast import numpy as np import numpy.typing as npt @@ -24,8 +24,6 @@ logger = logging.getLogger(__name__) -OutputType = TypeVar("OutputType") - CtypesTypes = ( type[ctypes.c_uint8] | type[ctypes.c_uint16] @@ -308,7 +306,7 @@ def allocate_and_post_buffer( ) return buffer - def acquire( # noqa: D417 (missing args documentation) + def acquire[OutputType]( # noqa: D417 (missing args documentation) self, mode: str | None = None, samples_per_record: int | None = None, @@ -932,3 +930,16 @@ def _get_alazar(self) -> AlazarTechATS: :return: reference to the Alazar instrument """ return self._alazar + + +if not TYPE_CHECKING: + from typing import TypeVar + + from qcodes.utils.deprecate import _make_deprecated_typevars_getattr + + __getattr__ = _make_deprecated_typevars_getattr( + __name__, + { + "OutputType": TypeVar("OutputType"), + }, + ) diff --git a/src/qcodes/instrument_drivers/Keithley/Keithley_6500.py b/src/qcodes/instrument_drivers/Keithley/Keithley_6500.py index bccb7a49ec6..2c60b2e9922 100644 --- a/src/qcodes/instrument_drivers/Keithley/Keithley_6500.py +++ b/src/qcodes/instrument_drivers/Keithley/Keithley_6500.py @@ -1,5 +1,5 @@ from functools import partial -from typing import TYPE_CHECKING, TypeVar +from typing import TYPE_CHECKING from qcodes.instrument import VisaInstrument, VisaInstrumentKWArgs from qcodes.validators import Bool, Enum, Ints, MultiType, Numbers @@ -10,8 +10,6 @@ from qcodes.parameters import Parameter -T = TypeVar("T") - def _parse_output_string(string_value: str) -> str: """Parses and cleans string output of the multimeter. Removes the surrounding @@ -258,7 +256,7 @@ def reset(self) -> None: def _read_next_value(self) -> float: return float(self.ask("READ?")) - def _get_mode_param(self, parameter: str, parser: "Callable[[str], T]") -> T: + def _get_mode_param[T](self, parameter: str, parser: "Callable[[str], T]") -> T: """Reads the current mode of the multimeter and ask for the given parameter. Args: @@ -287,3 +285,16 @@ def _set_mode_param(self, parameter: str, value: str | float | bool) -> None: mode = _parse_output_string(self._mode_map[self.mode()]) cmd = f"{mode}:{parameter} {value}" self.write(cmd) + + +if not TYPE_CHECKING: + from typing import TypeVar + + from qcodes.utils.deprecate import _make_deprecated_typevars_getattr + + __getattr__ = _make_deprecated_typevars_getattr( + __name__, + { + "T": TypeVar("T"), + }, + ) diff --git a/src/qcodes/instrument_drivers/Keysight/Keysight_N9030B.py b/src/qcodes/instrument_drivers/Keysight/Keysight_N9030B.py index 78275f66144..a5f17ac885f 100644 --- a/src/qcodes/instrument_drivers/Keysight/Keysight_N9030B.py +++ b/src/qcodes/instrument_drivers/Keysight/Keysight_N9030B.py @@ -25,6 +25,7 @@ from collections.abc import Callable from typing import Unpack +# Cannot convert to PEP 695: uses default= which requires PEP 696 (Python 3.13+). _T = TypeVar( "_T", bound="KeysightN9030BSpectrumAnalyzerMode | KeysightN9030BPhaseNoiseMode", diff --git a/src/qcodes/instrument_drivers/Keysight/keysight_34980a.py b/src/qcodes/instrument_drivers/Keysight/keysight_34980a.py index c68dc9b796b..b278c20088a 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysight_34980a.py +++ b/src/qcodes/instrument_drivers/Keysight/keysight_34980a.py @@ -2,9 +2,7 @@ import re import warnings from functools import wraps -from typing import TYPE_CHECKING, TypeVar - -from typing_extensions import ParamSpec +from typing import TYPE_CHECKING from qcodes import validators as vals from qcodes.instrument import VisaInstrument, VisaInstrumentKWArgs @@ -18,12 +16,8 @@ KEYSIGHT_MODELS = {"34934A": Keysight34934A} -S = TypeVar("S", bound="Keysight34980A") -P = ParamSpec("P") -T = TypeVar("T") - -def post_execution_status_poll( +def post_execution_status_poll[S: "Keysight34980A", T, **P]( func: "Callable[Concatenate[S, P], T]", ) -> "Callable[Concatenate[S, P], T]": """ @@ -191,3 +185,20 @@ def disconnect_all(self, slot: int | None = None) -> None: else: vals.Ints(min_value=1, max_value=self._total_slot).validate(slot) self.write(f"ROUT:OPEN:ALL {slot}") + + +if not TYPE_CHECKING: + from typing import TypeVar + + from typing_extensions import ParamSpec + + from qcodes.utils.deprecate import _make_deprecated_typevars_getattr + + __getattr__ = _make_deprecated_typevars_getattr( + __name__, + { + "S": TypeVar("S", bound="Keysight34980A"), + "T": TypeVar("T"), + "P": ParamSpec("P"), + }, + ) diff --git a/src/qcodes/instrument_drivers/Keysight/keysight_b220x.py b/src/qcodes/instrument_drivers/Keysight/keysight_b220x.py index c6c8898552a..d6c4f9ee2c9 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysight_b220x.py +++ b/src/qcodes/instrument_drivers/Keysight/keysight_b220x.py @@ -1,9 +1,7 @@ import re import warnings from functools import wraps -from typing import TYPE_CHECKING, TypeVar - -from typing_extensions import ParamSpec +from typing import TYPE_CHECKING from qcodes.instrument import VisaInstrument, VisaInstrumentKWArgs from qcodes.validators import Enum, Ints, Lists, MultiType @@ -15,12 +13,7 @@ from qcodes.parameters import Parameter -S = TypeVar("S", bound="KeysightB220X") -P = ParamSpec("P") -T = TypeVar("T") - - -def post_execution_status_poll( +def post_execution_status_poll[S: "KeysightB220X", T, **P]( func: "Callable[Concatenate[S, P], T]", ) -> "Callable[Concatenate[S, P], T]": """ @@ -447,3 +440,20 @@ class KeysightB2201(KeysightB220X): """ QCodes driver for B2201 """ + + +if not TYPE_CHECKING: + from typing import TypeVar + + from typing_extensions import ParamSpec + + from qcodes.utils.deprecate import _make_deprecated_typevars_getattr + + __getattr__ = _make_deprecated_typevars_getattr( + __name__, + { + "S": TypeVar("S", bound="KeysightB220X"), + "T": TypeVar("T"), + "P": ParamSpec("P"), + }, + ) diff --git a/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py b/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py index bc518b3edb6..1b8b9ff9d8e 100644 --- a/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py +++ b/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py @@ -662,6 +662,7 @@ def _get_sum_terms(available_terms: "Sequence[int]", number: int) -> list[int]: return terms_in_number +# Cannot convert to PEP 695: uses default= and covariant= which require PEP 696 (Python 3.13+). ChanType_co = TypeVar( "ChanType_co", bound=LakeshoreBaseSensorChannel, diff --git a/src/qcodes/instrument_drivers/QuantumDesign/DynaCoolPPMS/DynaCool.py b/src/qcodes/instrument_drivers/QuantumDesign/DynaCoolPPMS/DynaCool.py index 4edafe7155c..f428dd42f35 100644 --- a/src/qcodes/instrument_drivers/QuantumDesign/DynaCoolPPMS/DynaCool.py +++ b/src/qcodes/instrument_drivers/QuantumDesign/DynaCoolPPMS/DynaCool.py @@ -1,7 +1,7 @@ import warnings from functools import partial from time import sleep -from typing import TYPE_CHECKING, ClassVar, Literal, TypeVar, cast +from typing import TYPE_CHECKING, ClassVar, Literal, cast import numpy as np from pyvisa import VisaIOError @@ -15,9 +15,6 @@ from qcodes.parameters import Parameter -_T = TypeVar("_T") - - class DynaCool(VisaInstrument): """ Class to represent the DynaCoolPPMS @@ -276,7 +273,7 @@ def error_code(self) -> int: return self._error_code @staticmethod - def _pick_one(which_one: int, parser: "Callable[[str], _T]", resp: str) -> _T: + def _pick_one[T](which_one: int, parser: "Callable[[str], T]", resp: str) -> T: """ Since most of the API calls return several values in a comma-separated string, here's a convenience function to pick out the substring of diff --git a/src/qcodes/instrument_drivers/american_magnetics/AMI430_visa.py b/src/qcodes/instrument_drivers/american_magnetics/AMI430_visa.py index e840d83e7f8..651b7682219 100644 --- a/src/qcodes/instrument_drivers/american_magnetics/AMI430_visa.py +++ b/src/qcodes/instrument_drivers/american_magnetics/AMI430_visa.py @@ -6,11 +6,10 @@ from collections.abc import Callable, Iterable, Sequence from contextlib import ExitStack from functools import partial -from typing import TYPE_CHECKING, Any, ClassVar, Concatenate, TypeVar, cast +from typing import TYPE_CHECKING, Any, ClassVar, Concatenate, cast import numpy as np from pyvisa import VisaIOError -from typing_extensions import ParamSpec from qcodes.instrument import ( Instrument, @@ -32,10 +31,6 @@ CartesianFieldLimitFunction = Callable[[float, float, float], bool] -S = TypeVar("S", bound="AMI430SwitchHeater") -T = TypeVar("T") -P = ParamSpec("P") - class AMI430Exception(Exception): pass @@ -48,7 +43,7 @@ class AMI430Warning(UserWarning): class AMI430SwitchHeater(InstrumentChannel["AMIModel430"]): class _Decorators: @classmethod - def check_enabled( + def check_enabled[S: "AMI430SwitchHeater", T, **P]( cls, f: Callable[Concatenate[S, P], T] ) -> Callable[Concatenate[S, P], T]: def check_enabled_decorator( @@ -1320,3 +1315,20 @@ def _set_setpoints(self, names: Sequence[str], values: Sequence[float]) -> None: self._adjust_child_instruments(setpoint_values) self._set_point = set_point + + +if not TYPE_CHECKING: + from typing import TypeVar + + from typing_extensions import ParamSpec + + from qcodes.utils.deprecate import _make_deprecated_typevars_getattr + + __getattr__ = _make_deprecated_typevars_getattr( + __name__, + { + "S": TypeVar("S", bound="AMI430SwitchHeater"), + "T": TypeVar("T"), + "P": ParamSpec("P"), + }, + ) diff --git a/src/qcodes/math_utils/field_vector.py b/src/qcodes/math_utils/field_vector.py index 032c60c60f8..284627d8929 100644 --- a/src/qcodes/math_utils/field_vector.py +++ b/src/qcodes/math_utils/field_vector.py @@ -18,7 +18,6 @@ AllCoordsType = tuple[float, float, float, float, float, float, float] NormOrder = Literal["fro", "nuc"] | None | float -T = TypeVar("T", bound="FieldVector") class FieldVector: @@ -424,3 +423,13 @@ def from_homogeneous(cls: type[Self], hvec: npt.NDArray) -> Self: # Thus, we start by rescaling such that s == 1. hvec /= hvec[-1] return cls(x=hvec[0], y=hvec[1], z=hvec[2]) + + +if not TYPE_CHECKING: + from qcodes.utils.deprecate import _make_deprecated_typevars_getattr + + _deprecated_typevars: dict[str, TypeVar] = { + "T": TypeVar("T", bound="FieldVector"), + } + + __getattr__ = _make_deprecated_typevars_getattr(__name__, _deprecated_typevars) diff --git a/src/qcodes/parameters/cache.py b/src/qcodes/parameters/cache.py index 28c8f55ae6b..c0fddffe3ba 100644 --- a/src/qcodes/parameters/cache.py +++ b/src/qcodes/parameters/cache.py @@ -5,7 +5,8 @@ from typing_extensions import TypeVar -# due to circular imports we cannot import the TypeVar from parameter_base +# Cannot convert to PEP 695: uses default= which requires PEP 696 (Python 3.13+). +# Due to circular imports we cannot import the TypeVar from parameter_base. ParameterDataTypeVar = TypeVar("ParameterDataTypeVar", default=Any) if TYPE_CHECKING: diff --git a/src/qcodes/parameters/delegate_parameter.py b/src/qcodes/parameters/delegate_parameter.py index c04b1521e0c..df8b2f6f262 100644 --- a/src/qcodes/parameters/delegate_parameter.py +++ b/src/qcodes/parameters/delegate_parameter.py @@ -20,8 +20,9 @@ ParamRawDataType, ) -# Generic type variables for inner cache class -# these need to be different variables such that both classes can be generic +# Cannot convert to PEP 695: uses default= which requires PEP 696 (Python 3.13+). +# Generic type variables for inner cache class — +# these need to be different variables such that both classes can be generic. _local_ParameterDataTypeVar = TypeVar("_local_ParameterDataTypeVar", default=Any) _local_InstrumentTypeVar_co = TypeVar( "_local_InstrumentTypeVar_co", diff --git a/src/qcodes/parameters/multi_channel_instrument_parameter.py b/src/qcodes/parameters/multi_channel_instrument_parameter.py index a06f7f0e67b..074c335935c 100644 --- a/src/qcodes/parameters/multi_channel_instrument_parameter.py +++ b/src/qcodes/parameters/multi_channel_instrument_parameter.py @@ -1,7 +1,7 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING, Any from .multi_parameter import MultiParameter @@ -12,7 +12,6 @@ from .parameter_base import ParamRawDataType -InstrumentModuleType = TypeVar("InstrumentModuleType", bound="InstrumentModule") _LOG = logging.getLogger(__name__) @@ -89,3 +88,18 @@ def full_names(self) -> tuple[str, ...]: """ return self.names + + +if not TYPE_CHECKING: + from typing import TypeVar + + from qcodes.utils.deprecate import _make_deprecated_typevars_getattr + + __getattr__ = _make_deprecated_typevars_getattr( + __name__, + { + "InstrumentModuleType": TypeVar( + "InstrumentModuleType", bound="InstrumentModule" + ), + }, + ) diff --git a/src/qcodes/parameters/parameter_base.py b/src/qcodes/parameters/parameter_base.py index a14095a4fc5..dcbaa6b60bd 100644 --- a/src/qcodes/parameters/parameter_base.py +++ b/src/qcodes/parameters/parameter_base.py @@ -49,7 +49,9 @@ from qcodes.dataset.data_set_protocol import ValuesType from qcodes.instrument import InstrumentBase from qcodes.logger.instrument_logger import InstrumentLoggerAdapter +# Cannot convert to PEP 695: uses default= which requires PEP 696 (Python 3.13+). ParameterDataTypeVar = TypeVar("ParameterDataTypeVar", default=Any) +# Cannot convert to PEP 695: uses default= and covariant= which require PEP 696 (Python 3.13+). # InstrumentTypeVar_co is a covariant type variable representing the instrument # type associated with the parameter. It needs to be covariant to allow passing # a Parameter bound to None or a specific instrument where the default is used in the type hint. diff --git a/src/qcodes/utils/abstractmethod.py b/src/qcodes/utils/abstractmethod.py index 1ba8c28bfb1..26d0ac3611f 100644 --- a/src/qcodes/utils/abstractmethod.py +++ b/src/qcodes/utils/abstractmethod.py @@ -1,15 +1,10 @@ -from typing import TYPE_CHECKING, TypeVar - -from typing_extensions import ParamSpec +from typing import TYPE_CHECKING if TYPE_CHECKING: from collections.abc import Callable -input = ParamSpec("input") -output = TypeVar("output") - -def qcodes_abstractmethod( +def qcodes_abstractmethod[**input, output]( funcobj: "Callable[input, output]", ) -> "Callable[input, output]": """ @@ -23,3 +18,19 @@ def qcodes_abstractmethod( """ funcobj.__qcodes_is_abstract_method__ = True # type: ignore[attr-defined] return funcobj + + +if not TYPE_CHECKING: + from typing import TypeVar + + from typing_extensions import ParamSpec + + from qcodes.utils.deprecate import _make_deprecated_typevars_getattr + + __getattr__ = _make_deprecated_typevars_getattr( + __name__, + { + "input": ParamSpec("input"), + "output": TypeVar("output"), + }, + ) diff --git a/src/qcodes/utils/snapshot_helpers.py b/src/qcodes/utils/snapshot_helpers.py index c334b1153f2..72299a5bf65 100644 --- a/src/qcodes/utils/snapshot_helpers.py +++ b/src/qcodes/utils/snapshot_helpers.py @@ -1,11 +1,9 @@ -from typing import Any, NamedTuple, TypeVar - -T = TypeVar("T") +from typing import TYPE_CHECKING, Any, NamedTuple # Unbound parameters or Instrument parameters ParameterKey = str | tuple[str, str] -ParameterDict = dict[ParameterKey, T] +type ParameterDict[T] = dict[ParameterKey, T] Snapshot = dict[str, Any] @@ -61,3 +59,16 @@ def diff_param_values( if left_params[key] != right_params[key] }, ) + + +if not TYPE_CHECKING: + from typing import TypeVar + + from qcodes.utils.deprecate import _make_deprecated_typevars_getattr + + __getattr__ = _make_deprecated_typevars_getattr( + __name__, + { + "T": TypeVar("T"), + }, + ) diff --git a/src/qcodes/utils/threading_utils.py b/src/qcodes/utils/threading_utils.py index b67f5ed83ef..ff207d2177d 100644 --- a/src/qcodes/utils/threading_utils.py +++ b/src/qcodes/utils/threading_utils.py @@ -1,14 +1,12 @@ import logging import threading -from typing import TYPE_CHECKING, Any, Optional, TypeVar +from typing import TYPE_CHECKING, Any, Optional if TYPE_CHECKING: from collections.abc import Callable, Sequence _LOG = logging.getLogger(__name__) -T = TypeVar("T") - class RespondingThread[T](threading.Thread): """ @@ -64,7 +62,7 @@ def output(self, timeout: float | None = None) -> T | None: return self._output -def thread_map( +def thread_map[T]( callables: "Sequence[Callable[..., T]]", args: Optional["Sequence[Sequence[Any]]"] = None, kwargs: Optional["Sequence[dict[str, Any]]"] = None, @@ -95,3 +93,16 @@ def thread_map( t.start() return [t.output() for t in threads] + + +if not TYPE_CHECKING: + from typing import TypeVar + + from qcodes.utils.deprecate import _make_deprecated_typevars_getattr + + __getattr__ = _make_deprecated_typevars_getattr( + __name__, + { + "T": TypeVar("T"), + }, + ) diff --git a/src/qcodes/validators/validators.py b/src/qcodes/validators/validators.py index a3ac3e726d8..b0dab79bb17 100644 --- a/src/qcodes/validators/validators.py +++ b/src/qcodes/validators/validators.py @@ -9,7 +9,7 @@ import typing from collections import abc from collections.abc import Hashable -from typing import Any, Literal, TypeVar, cast, get_args +from typing import TYPE_CHECKING, Any, Literal, cast, get_args import numpy as np import numpy.typing as npt @@ -62,8 +62,6 @@ def range_str( return "" -T = TypeVar("T") - class Validator[T]: """ @@ -501,7 +499,7 @@ def values(self) -> set[Hashable]: return self._values.copy() -class LiteralValidator(Validator[T]): +class LiteralValidator[T](Validator[T]): """ A validator that allows users to check that values supplied are in set of members @@ -1061,7 +1059,7 @@ def max_value(self) -> float | None: return float(self._max_value) if self._max_value is not None else None -class Lists(Validator[list[T]]): +class Lists[T](Validator[list[T]]): """ Validator for lists @@ -1253,3 +1251,16 @@ def allowed_keys(self) -> abc.Sequence[Hashable] | None: @allowed_keys.setter def allowed_keys(self, keys: abc.Sequence[Hashable] | None) -> None: self._allowed_keys = keys + + +if not TYPE_CHECKING: + from typing import TypeVar + + from qcodes.utils.deprecate import _make_deprecated_typevars_getattr + + __getattr__ = _make_deprecated_typevars_getattr( + __name__, + { + "T": TypeVar("T"), + }, + ) diff --git a/tests/utils/test_deprecate.py b/tests/utils/test_deprecate.py index 09f07ed606c..5fa3e58006e 100644 --- a/tests/utils/test_deprecate.py +++ b/tests/utils/test_deprecate.py @@ -1,6 +1,8 @@ from __future__ import annotations -from typing import TypeVar +import importlib +from collections.abc import Callable +from typing import Any, TypeVar import pytest @@ -9,9 +11,11 @@ _make_deprecated_typevars_getattr, ) +_FixtureT = tuple[dict[str, TypeVar], Callable[[str], Any]] + @pytest.fixture -def deprecated_getattr() -> tuple[dict[str, TypeVar], callable]: +def deprecated_getattr() -> _FixtureT: """Create a __getattr__ with two deprecated TypeVars.""" deprecated = { "MyT": TypeVar("MyT"), @@ -22,7 +26,7 @@ def deprecated_getattr() -> tuple[dict[str, TypeVar], callable]: def test_returns_typevar_and_warns( - deprecated_getattr: tuple[dict[str, TypeVar], callable], + deprecated_getattr: _FixtureT, ) -> None: deprecated, getattr_fn = deprecated_getattr with pytest.warns(QCoDeSDeprecationWarning, match="'MyT'.*'fake.module'"): @@ -31,7 +35,7 @@ def test_returns_typevar_and_warns( def test_warns_for_each_deprecated_name( - deprecated_getattr: tuple[dict[str, TypeVar], callable], + deprecated_getattr: _FixtureT, ) -> None: deprecated, getattr_fn = deprecated_getattr with pytest.warns(QCoDeSDeprecationWarning, match="'MyK'"): @@ -40,15 +44,15 @@ def test_warns_for_each_deprecated_name( def test_unknown_name_raises_attribute_error( - deprecated_getattr: tuple[dict[str, TypeVar], callable], + deprecated_getattr: _FixtureT, ) -> None: _, getattr_fn = deprecated_getattr - with pytest.raises(AttributeError, match="fake.module"): + with pytest.raises(AttributeError, match=r"fake\.module"): getattr_fn("DoesNotExist") def test_repeated_access_returns_same_object( - deprecated_getattr: tuple[dict[str, TypeVar], callable], + deprecated_getattr: _FixtureT, ) -> None: deprecated, getattr_fn = deprecated_getattr with pytest.warns(QCoDeSDeprecationWarning): @@ -86,7 +90,8 @@ def fallback(name: str) -> str: 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'"): - from qcodes.utils.deep_update_utils import K + k = mod.K - assert isinstance(K, TypeVar) + assert isinstance(k, TypeVar) From 08764549356d78e50bffc344a7b19629b7ea522d Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 4 May 2026 13:18:42 +0200 Subject: [PATCH 3/7] Rerun precommit hooks --- src/qcodes/instrument/channel.py | 9 ++++++--- src/qcodes/instrument/instrument.py | 5 +++-- .../QuantumDesign/DynaCoolPPMS/DynaCool.py | 1 + src/qcodes/validators/validators.py | 1 - 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/qcodes/instrument/channel.py b/src/qcodes/instrument/channel.py index 5d35d3c483d..0b51adab55d 100644 --- a/src/qcodes/instrument/channel.py +++ b/src/qcodes/instrument/channel.py @@ -98,7 +98,9 @@ class InstrumentChannel(InstrumentModule[_TIB_co], Generic[_TIB_co]): pass -class ChannelTuple[InstrumentModuleType: "InstrumentModule"](MetadatableWithName, Sequence[InstrumentModuleType]): +class ChannelTuple[InstrumentModuleType: "InstrumentModule"]( + MetadatableWithName, Sequence[InstrumentModuleType] +): """ Container for channelized parameters that allows for sweeps over all channels, as well as addressing of individual channels. @@ -1158,8 +1160,9 @@ def exists_on_instrument(self) -> bool: return self._exists_on_instrument - -class AutoLoadableChannelList[TAUTORELOADCHANNEL: AutoLoadableInstrumentChannel](ChannelList[TAUTORELOADCHANNEL]): +class AutoLoadableChannelList[TAUTORELOADCHANNEL: AutoLoadableInstrumentChannel]( + ChannelList[TAUTORELOADCHANNEL] +): """ Extends the QCoDeS :class:`ChannelList` class to add the following features: - Automatically create channel objects on initialization diff --git a/src/qcodes/instrument/instrument.py b/src/qcodes/instrument/instrument.py index a0490427747..70ea6d203c3 100644 --- a/src/qcodes/instrument/instrument.py +++ b/src/qcodes/instrument/instrument.py @@ -31,7 +31,6 @@ def ask(self, cmd: str) -> str: ... def write(self, cmd: str) -> None: ... - # a metaclass that overrides __call__ means that we lose # both the args and return type hints. # Since our metaclass does not modify the signature @@ -279,7 +278,9 @@ def find_instrument( @overload @classmethod - def find_instrument[T: "Instrument"](cls, name: str, instrument_class: type[T]) -> T: ... + def find_instrument[T: "Instrument"]( + cls, name: str, instrument_class: type[T] + ) -> T: ... @classmethod def find_instrument[T: "Instrument"]( diff --git a/src/qcodes/instrument_drivers/QuantumDesign/DynaCoolPPMS/DynaCool.py b/src/qcodes/instrument_drivers/QuantumDesign/DynaCoolPPMS/DynaCool.py index f428dd42f35..d004a60c6f4 100644 --- a/src/qcodes/instrument_drivers/QuantumDesign/DynaCoolPPMS/DynaCool.py +++ b/src/qcodes/instrument_drivers/QuantumDesign/DynaCoolPPMS/DynaCool.py @@ -15,6 +15,7 @@ from qcodes.parameters import Parameter + class DynaCool(VisaInstrument): """ Class to represent the DynaCoolPPMS diff --git a/src/qcodes/validators/validators.py b/src/qcodes/validators/validators.py index b0dab79bb17..ce1410cf532 100644 --- a/src/qcodes/validators/validators.py +++ b/src/qcodes/validators/validators.py @@ -62,7 +62,6 @@ def range_str( return "" - class Validator[T]: """ Base class for all value validators From d6c4247f1970fbf9a43f4b65d3333236beeec425 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 5 May 2026 06:58:49 +0200 Subject: [PATCH 4/7] Implement UP040 --- pyproject.toml | 2 +- src/qcodes/dataset/data_set_protocol.py | 16 +++++++--------- src/qcodes/dataset/measurements.py | 6 +++--- src/qcodes/dataset/threading.py | 6 +++--- .../instrument_drivers/AlazarTech/ats_api.py | 6 +++--- .../private/Keysight_344xxA_submodules.py | 4 ++-- src/qcodes/utils/types.py | 2 +- 7 files changed, 20 insertions(+), 22 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7ed11252b26..cbe55cb89be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -259,7 +259,7 @@ select = [ # it may be worth fixing some or these in the future # PYI036 disable until https://github.com/astral-sh/ruff/issues/9794 is fixed # UP040, UP046, UP047: PEP 695 type param conversions deferred — TypeVars use default/covariant features -ignore = ["E501", "G004", "PLR2004", "PLR0913", "PLR0911", "PLR0912", "PLR0915", "PLW0602", "PLW0603", "PLW2901", "PYI036", "UP040"] +ignore = ["E501", "G004", "PLR2004", "PLR0913", "PLR0911", "PLR0912", "PLR0915", "PLW0602", "PLW0603", "PLW2901", "PYI036"] # we want to explicitly use the micro symbol # not the greek letter diff --git a/src/qcodes/dataset/data_set_protocol.py b/src/qcodes/dataset/data_set_protocol.py index 1098f472e95..d1ba5ecc9b5 100644 --- a/src/qcodes/dataset/data_set_protocol.py +++ b/src/qcodes/dataset/data_set_protocol.py @@ -51,17 +51,15 @@ # twice here convert to set to ensure no duplication. _EXPORT_CALLBACKS = set(entry_points(group="qcodes.dataset.on_export")) -ScalarResTypes: TypeAlias = ( - str | complex | np.integer | np.floating | np.complexfloating -) -ValuesType: TypeAlias = ( +type ScalarResTypes = str | complex | np.integer | np.floating | np.complexfloating +type ValuesType = ( ScalarResTypes | npt.NDArray | Sequence[ScalarResTypes] | Sequence[Sequence[ScalarResTypes]] ) -ResType: TypeAlias = "tuple[ParameterBase | str, ValuesType]" -SetpointsType: TypeAlias = "Sequence[str | ParameterBase]" +type ResType = "tuple[ParameterBase | str, ValuesType]" +type SetpointsType = "Sequence[str | ParameterBase]" # deprecated alias left for backwards compatibility array_like_types = (tuple, list, npt.NDArray) @@ -71,12 +69,12 @@ setpoints_type: TypeAlias = SetpointsType # noqa PYI042 -SPECS: TypeAlias = list[ParamSpec] +type SPECS = list[ParamSpec] # Transition period type: SpecsOrInterDeps. We will allow both as input to # the DataSet constructor for a while, then deprecate SPECS and finally remove # the ParamSpec class -SpecsOrInterDeps: TypeAlias = SPECS | InterDependencies_ -ParameterData: TypeAlias = dict[str, dict[str, npt.NDArray]] +type SpecsOrInterDeps = SPECS | InterDependencies_ +type ParameterData = dict[str, dict[str, npt.NDArray]] LOG = logging.getLogger(__name__) diff --git a/src/qcodes/dataset/measurements.py b/src/qcodes/dataset/measurements.py index fd94546d7e2..3437bbb8a7c 100644 --- a/src/qcodes/dataset/measurements.py +++ b/src/qcodes/dataset/measurements.py @@ -18,7 +18,7 @@ from itertools import chain from numbers import Number from time import perf_counter, perf_counter_ns -from typing import TYPE_CHECKING, Any, TypeAlias, cast +from typing import TYPE_CHECKING, Any, cast import numpy as np import numpy.typing as npt @@ -74,8 +74,8 @@ Callable[..., Any], MutableSequence[Any] | MutableMapping[Any, Any] ] -ParameterResultType: TypeAlias = tuple[ParameterBase, ValuesType] -DatasetResultDict: TypeAlias = dict[ParamSpecBase, npt.NDArray] +type ParameterResultType = tuple[ParameterBase, ValuesType] +type DatasetResultDict = dict[ParamSpecBase, npt.NDArray] class ParameterTypeError(Exception): diff --git a/src/qcodes/dataset/threading.py b/src/qcodes/dataset/threading.py index 8300f01a11e..606018a76dd 100644 --- a/src/qcodes/dataset/threading.py +++ b/src/qcodes/dataset/threading.py @@ -9,7 +9,7 @@ import logging from collections import defaultdict from functools import partial -from typing import TYPE_CHECKING, Protocol, TypeAlias, TypeVar +from typing import TYPE_CHECKING, Protocol, TypeVar from qcodes.utils import RespondingThread @@ -21,8 +21,8 @@ from qcodes.dataset.data_set_protocol import ValuesType from qcodes.parameters import ParamDataType, ParameterBase -ParamMeasT: TypeAlias = "ParameterBase | Callable[[], None]" -OutType: TypeAlias = "list[tuple[ParameterBase, ValuesType]]" +type ParamMeasT = "ParameterBase | Callable[[], None]" +type OutType = "list[tuple[ParameterBase, ValuesType]]" _LOG = logging.getLogger(__name__) diff --git a/src/qcodes/instrument_drivers/AlazarTech/ats_api.py b/src/qcodes/instrument_drivers/AlazarTech/ats_api.py index ca0f814256d..dacd569ab31 100644 --- a/src/qcodes/instrument_drivers/AlazarTech/ats_api.py +++ b/src/qcodes/instrument_drivers/AlazarTech/ats_api.py @@ -6,7 +6,7 @@ import ctypes from ctypes import POINTER -from typing import TYPE_CHECKING, Any, ClassVar, TypeAlias +from typing import TYPE_CHECKING, Any, ClassVar # `ParameterBase` is needed because users may pass instrument parameters # that originate from `Instrument.parameters` dictionary which is typed @@ -30,9 +30,9 @@ POINTER_c_long = Any -IntOrParam: TypeAlias = "int | Parameter" +type IntOrParam = "int | Parameter" # deprecated alias for backwards compatibility -int_or_param: TypeAlias = IntOrParam # noqa: PYI042 +type int_or_param = IntOrParam # noqa: PYI042 class AlazarATSAPI(WrappedDll): diff --git a/src/qcodes/instrument_drivers/Keysight/private/Keysight_344xxA_submodules.py b/src/qcodes/instrument_drivers/Keysight/private/Keysight_344xxA_submodules.py index 7bc60e43577..e35c0930b65 100644 --- a/src/qcodes/instrument_drivers/Keysight/private/Keysight_344xxA_submodules.py +++ b/src/qcodes/instrument_drivers/Keysight/private/Keysight_344xxA_submodules.py @@ -2,7 +2,7 @@ from bisect import bisect_left from contextlib import ExitStack from functools import partial -from typing import TYPE_CHECKING, Any, Literal, TypeAlias, get_args +from typing import TYPE_CHECKING, Any, Literal, get_args import numpy as np import numpy.typing as npt @@ -27,7 +27,7 @@ from collections.abc import Callable, Sequence from typing import Unpack -NumericScpiMnemonic: TypeAlias = Literal["MIN", "MAX", "DEF"] +type NumericScpiMnemonic = Literal["MIN", "MAX", "DEF"] class Keysight344xxATrigger(InstrumentChannel["Keysight344xxA"]): diff --git a/src/qcodes/utils/types.py b/src/qcodes/utils/types.py index 0f0fb0fd5b5..01fe50e3a2e 100644 --- a/src/qcodes/utils/types.py +++ b/src/qcodes/utils/types.py @@ -85,7 +85,7 @@ concrete_complex_types = (*numpy_concrete_complex, complex) complex_types = (*numpy_concrete_complex, complex) -NumberType: TypeAlias = int | float | np.integer | np.floating +NumberType: TypeAlias = int | float | np.integer | np.floating # noqa: UP040 # this is used in isinstance checks. """ Python or NumPy real number. """ From 168aa29ff0c17e64939c44a505ac8f114bbb197d Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 5 May 2026 07:05:47 +0200 Subject: [PATCH 5/7] Deprecate unused type aliases with runtime warnings Deprecate soft-deprecated type aliases in data_set_protocol.py (scalar_res_types, values_type, res_type, setpoints_type, array_like_types) and ats_api.py (int_or_param) using the same __getattr__-based deprecation pattern used for TypeVars. Also update the deprecation warning message to say 'name' instead of 'TypeVar' since the helper is now used for type aliases too. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/changes/newsfragments/8096.breaking | 6 ++--- src/qcodes/dataset/data_set_protocol.py | 24 ++++++++++++------- .../instrument_drivers/AlazarTech/ats_api.py | 13 ++++++++-- src/qcodes/utils/deprecate.py | 4 ++-- 4 files changed, 31 insertions(+), 16 deletions(-) diff --git a/docs/changes/newsfragments/8096.breaking b/docs/changes/newsfragments/8096.breaking index cc9c0924e7b..a3776e7d7d6 100644 --- a/docs/changes/newsfragments/8096.breaking +++ b/docs/changes/newsfragments/8096.breaking @@ -1,3 +1,3 @@ -Several module-level ``TypeVar`` definitions that are no longer used internally have been -deprecated. Importing them will emit a ``QCoDeSDeprecationWarning``. They will be removed -in a future version. +Several module-level ``TypeVar`` definitions and type aliases that are no longer used +internally have been deprecated. Importing them will emit a ``QCoDeSDeprecationWarning``. +They will be removed in a future version. diff --git a/src/qcodes/dataset/data_set_protocol.py b/src/qcodes/dataset/data_set_protocol.py index d1ba5ecc9b5..c12dbc3d495 100644 --- a/src/qcodes/dataset/data_set_protocol.py +++ b/src/qcodes/dataset/data_set_protocol.py @@ -34,8 +34,6 @@ from .sqlite.queries import raw_time_to_str_time if TYPE_CHECKING: - from typing import TypeAlias - import pandas as pd import xarray as xr @@ -61,13 +59,6 @@ type ResType = "tuple[ParameterBase | str, ValuesType]" type SetpointsType = "Sequence[str | ParameterBase]" -# deprecated alias left for backwards compatibility -array_like_types = (tuple, list, npt.NDArray) -scalar_res_types: TypeAlias = ScalarResTypes # noqa PYI042 -values_type: TypeAlias = ValuesType # noqa PYI042 -res_type: TypeAlias = ResType # noqa PYI042 -setpoints_type: TypeAlias = SetpointsType # noqa PYI042 - type SPECS = list[ParamSpec] # Transition period type: SpecsOrInterDeps. We will allow both as input to @@ -548,3 +539,18 @@ def dependent_parameters(self) -> tuple[ParamSpecBase, ...]: class DataSetType(StrEnum): DataSet = "DataSet" DataSetInMem = "DataSetInMem" + + +if not TYPE_CHECKING: + from qcodes.utils.deprecate import _make_deprecated_typevars_getattr + + __getattr__ = _make_deprecated_typevars_getattr( + __name__, + { + "array_like_types": (tuple, list, npt.NDArray), + "scalar_res_types": ScalarResTypes, + "values_type": ValuesType, + "res_type": ResType, + "setpoints_type": SetpointsType, + }, + ) diff --git a/src/qcodes/instrument_drivers/AlazarTech/ats_api.py b/src/qcodes/instrument_drivers/AlazarTech/ats_api.py index dacd569ab31..d61ffb07a46 100644 --- a/src/qcodes/instrument_drivers/AlazarTech/ats_api.py +++ b/src/qcodes/instrument_drivers/AlazarTech/ats_api.py @@ -31,8 +31,6 @@ type IntOrParam = "int | Parameter" -# deprecated alias for backwards compatibility -type int_or_param = IntOrParam # noqa: PYI042 class AlazarATSAPI(WrappedDll): @@ -667,3 +665,14 @@ def write_register_(self, handle: int, offset: int, value: int) -> None: """ self.write_register(handle, offset, value, REGISTER_ACCESS_PASSWORD) + + +if not TYPE_CHECKING: + from qcodes.utils.deprecate import _make_deprecated_typevars_getattr + + __getattr__ = _make_deprecated_typevars_getattr( + __name__, + { + "int_or_param": IntOrParam, + }, + ) diff --git a/src/qcodes/utils/deprecate.py b/src/qcodes/utils/deprecate.py index 2032254a591..76b9c3ef7ed 100644 --- a/src/qcodes/utils/deprecate.py +++ b/src/qcodes/utils/deprecate.py @@ -20,7 +20,7 @@ def _make_deprecated_typevars_getattr( fallback: Callable[[str], Any] | None = None, ) -> Callable[[str], Any]: """Return a module-level ``__getattr__`` that emits deprecation warnings - for removed TypeVar / type-alias names. + for removed TypeVar / type-alias / other names. Args: module_name: The ``__name__`` of the calling module. @@ -38,7 +38,7 @@ def __getattr__(name: str) -> Any: if name in deprecated: warnings.warn( f"Importing {name!r} from {module_name!r} is deprecated. " - f"This TypeVar is no longer used and will be removed in a " + f"This name is no longer used and will be removed in a " f"future version.", QCoDeSDeprecationWarning, stacklevel=2, From f007066e3c2d5500c9615b0eebc326240574daa7 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 5 May 2026 07:10:01 +0200 Subject: [PATCH 6/7] Remove comment --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cbe55cb89be..0d2b8d622a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -258,7 +258,6 @@ select = [ # PLxxxx are pylint lints that generate a fair amount of warnings # it may be worth fixing some or these in the future # PYI036 disable until https://github.com/astral-sh/ruff/issues/9794 is fixed -# UP040, UP046, UP047: PEP 695 type param conversions deferred — TypeVars use default/covariant features ignore = ["E501", "G004", "PLR2004", "PLR0913", "PLR0911", "PLR0912", "PLR0915", "PLW0602", "PLW0603", "PLW2901", "PYI036"] # we want to explicitly use the micro symbol From c567894cea32f548f4bb14425a09ea0ee807662b Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Tue, 5 May 2026 11:19:24 +0200 Subject: [PATCH 7/7] Remove Typevar from test util --- tests/common.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/common.py b/tests/common.py index 0c2da5080b9..11d4df019b2 100644 --- a/tests/common.py +++ b/tests/common.py @@ -4,10 +4,9 @@ import os from functools import wraps from time import sleep -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING, Any import pytest -from typing_extensions import ParamSpec from qcodes.metadatable import MetadatableWithName @@ -18,11 +17,7 @@ from pytest import ExceptionInfo -T = TypeVar("T") -P = ParamSpec("P") - - -def retry_until_does_not_throw( +def retry_until_does_not_throw[**P, T]( exception_class_to_expect: type[Exception] = AssertionError, tries: int = 5, delay: float = 0.1,