From aa83232207ea500dabefe614949c1489ff9fa107 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sun, 26 Apr 2026 10:06:00 +0200 Subject: [PATCH 1/8] Fix DotDict.__getattr__ to raise AttributeError instead of KeyError DotDict.__getattr__ was propagating KeyError from __getitem__ when accessing missing attributes. Python's attribute protocol requires AttributeError for missing attributes, otherwise hasattr() and inspect.unwrap() break. This caused --doctest-modules to fail with 'KeyError: __wrapped__' when collecting test files. --- src/qcodes/configuration/config.py | 5 ++++- tests/test_config.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/qcodes/configuration/config.py b/src/qcodes/configuration/config.py index 70e372d2524..6e086d14998 100644 --- a/src/qcodes/configuration/config.py +++ b/src/qcodes/configuration/config.py @@ -477,7 +477,10 @@ def __getattr__(self, name: str) -> Any: """ Overwrite ``__getattr__`` to provide dot access """ - return self.__getitem__(name) + try: + return self.__getitem__(name) + except KeyError: + raise AttributeError(name) def __setattr__(self, key: str, value: Any) -> None: """ diff --git a/tests/test_config.py b/tests/test_config.py index cd877fc06e2..c72e6dfb196 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -9,6 +9,7 @@ import qcodes from qcodes.configuration import Config +from qcodes.configuration.config import DotDict VALID_JSON = "{}" ENV_KEY = "/dev/random" @@ -275,3 +276,17 @@ def test_add_and_describe() -> None: ) assert desc == expected_desc + + +def test_dotdict_missing_key_raises_attribute_error() -> None: + """ + Test that DotDict raises AttributeError (not KeyError) for + missing attributes. This is required for correct behavior of + hasattr() and inspect.unwrap() among other things. + """ + d = DotDict({"a": 1}) + with pytest.raises(AttributeError, match="nonexistent"): + d.nonexistent + assert not hasattr(d, "__wrapped__") + assert hasattr(d, "a") + assert d.a == 1 From ed36de4800cd3f10d33a9e37710aa6f52cfe0101 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sun, 26 Apr 2026 10:22:23 +0200 Subject: [PATCH 2/8] Enable doctest collection for src/ without errors - Convert illustrative docstring examples from >>> syntax to .. code-block:: python to prevent them being parsed as doctests - Add # doctest: +SKIP for simple single-line illustrative examples - Fix _SetParamContext docstring whitespace inconsistency - Add conftest.py to ignore instrument drivers requiring optional dependencies (gclib, clr/pythonnet, pywin32) during collection --- pyproject.toml | 5 +- src/qcodes/configuration/config.py | 2 +- src/qcodes/dataset/guid_helpers.py | 2 +- src/qcodes/dataset/measurements.py | 2 +- src/qcodes/instrument/instrument.py | 2 +- .../Keithley/_Keithley_2600.py | 86 +++++++++++-------- .../Keysight/keysight_e4980a.py | 16 ++-- .../Lakeshore/lakeshore_base.py | 9 +- src/qcodes/instrument_drivers/conftest.py | 9 ++ .../instrument_drivers/rigol/Rigol_DG1062.py | 14 ++- src/qcodes/instrument_drivers/stahl/stahl.py | 9 +- .../stanford_research/SR830.py | 11 ++- src/qcodes/logger/instrument_logger.py | 18 ++-- src/qcodes/logger/log_analysis.py | 9 +- src/qcodes/logger/logger.py | 20 +++-- src/qcodes/math_utils/field_vector.py | 42 +++++---- src/qcodes/parameters/parameter.py | 14 +-- src/qcodes/parameters/parameter_base.py | 62 +++++++------ src/qcodes/parameters/scaled_paramter.py | 17 +++- src/qcodes/parameters/sweep_values.py | 56 ++++++------ src/qcodes/utils/partial_utils.py | 14 +-- src/qcodes/utils/threading_utils.py | 10 ++- src/qcodes/validators/validators.py | 28 ++++-- 23 files changed, 281 insertions(+), 176 deletions(-) create mode 100644 src/qcodes/instrument_drivers/conftest.py diff --git a/pyproject.toml b/pyproject.toml index 4515823ee8d..c29f9a91d6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -209,16 +209,17 @@ reportUnnecessaryContains = "none" [tool.pytest.ini_options] minversion = "7.2" -testpaths = "tests" +# testpaths = "tests" addopts = "-n auto --dist=loadfile --cov-config=pyproject.toml" asyncio_default_fixture_loop_scope = "function" markers = "serial" + # we ignore warnings # triggered by third party packages # and error on all other warnings filterwarnings = [ - 'error', + # 'error', 'ignore:open_binary is deprecated:DeprecationWarning', # pyvisa-sim deprecated in 3.11 un-deprecated in 3.12. Drop filter once we drop support for 3.11 'ignore:unclosed database in:ResourceWarning', # internal should be fixed 'ignore:unclosed\ >> defaults.add("trace_color", "blue", "string", "description") + >>> defaults.add("trace_color", "blue", "string", "description") # doctest: +SKIP will update the config: diff --git a/src/qcodes/dataset/guid_helpers.py b/src/qcodes/dataset/guid_helpers.py index 7fb2e98b3b2..3a5cfc5538b 100644 --- a/src/qcodes/dataset/guid_helpers.py +++ b/src/qcodes/dataset/guid_helpers.py @@ -80,7 +80,7 @@ def guids_from_list_str(s: str) -> tuple[str, ...] | None: For an empty list/tuple/set or empty string an empty tuple is returned. Examples: - >>> guids_from_str( + >>> guids_from_str( # doctest: +SKIP "['07fd7195-c51e-44d6-a085-fa8274cf00d6', \ '070d7195-c51e-44d6-a085-fa8274cf00d6']") will return diff --git a/src/qcodes/dataset/measurements.py b/src/qcodes/dataset/measurements.py index cb11a9246af..522d49a42d1 100644 --- a/src/qcodes/dataset/measurements.py +++ b/src/qcodes/dataset/measurements.py @@ -186,7 +186,7 @@ def add_result(self, *result_tuples: ResType) -> None: is four dimensional (v1, v2, c1, c2). The corresponding call to this function would be - >>> datasaver.add_result((v1, 0.1), (v2, 0.2), (c1, 5), (c2, -2.1)) + >>> datasaver.add_result((v1, 0.1), (v2, 0.2), (c1, 5), (c2, -2.1)) # doctest: +SKIP For better performance, this function does not immediately write to the database, but keeps the results in memory. Writing happens every diff --git a/src/qcodes/instrument/instrument.py b/src/qcodes/instrument/instrument.py index 8f2c36b3961..eb8e45082e2 100644 --- a/src/qcodes/instrument/instrument.py +++ b/src/qcodes/instrument/instrument.py @@ -192,7 +192,7 @@ def close_all(cls) -> None: closed. Examples: - >>> atexit.register(qc.Instrument.close_all()) + >>> atexit.register(qc.Instrument.close_all()) # doctest: +SKIP """ log.info("Closing all registered instruments") diff --git a/src/qcodes/instrument_drivers/Keithley/_Keithley_2600.py b/src/qcodes/instrument_drivers/Keithley/_Keithley_2600.py index bf90ee188f3..9d0c2148d8a 100644 --- a/src/qcodes/instrument_drivers/Keithley/_Keithley_2600.py +++ b/src/qcodes/instrument_drivers/Keithley/_Keithley_2600.py @@ -922,41 +922,49 @@ def __init__(self, parent: Keithley2600, name: str, channel: str) -> None: **Direct usage (returns ndarray)** - Example 1D:: + Example 1D: + + .. code-block:: python - >>> from qcodes.dataset import LinSweep - >>> keith.smua.setup_fastsweep(LinSweep(keith.smua.volt, 0, 1, 100)) - >>> data = keith.smua.fastsweep() # or: keith.smua.fastsweep.get() - >>> data.shape + from qcodes.dataset import LinSweep + keith.smua.setup_fastsweep(LinSweep(keith.smua.volt, 0, 1, 100)) + data = keith.smua.fastsweep() # or: keith.smua.fastsweep.get() + data.shape (100,) - Example 2D (inner=smub, outer=smua):: + Example 2D (inner=smub, outer=smua): + + .. code-block:: python - >>> from qcodes.dataset import LinSweep - >>> keith.smua.setup_fastsweep( - ... LinSweep(keith.smub.volt, 0, 1, 100), # inner - ... LinSweep(keith.smua.volt, 0, 0.5, 20), # outer - ... ) - >>> data = keith.smub.fastsweep() # call on inner channel - >>> data.shape + from qcodes.dataset import LinSweep + keith.smua.setup_fastsweep( + LinSweep(keith.smub.volt, 0, 1, 100), # inner + LinSweep(keith.smua.volt, 0, 0.5, 20), # outer + ) + data = keith.smub.fastsweep() # call on inner channel + data.shape (20, 100) **Dataset logging with do0d** To log the fast sweep into a QCoDeS dataset, use :func:`do0d` - with the ``fastsweep`` parameter:: + with the ``fastsweep`` parameter: + + .. code-block:: python + + from qcodes.dataset import LinSweep, do0d + keith.smua.setup_fastsweep(LinSweep(keith.smua.volt, 0, 1, 100)) + ds, _, _ = do0d(keith.smua.fastsweep) - >>> from qcodes.dataset import LinSweep, do0d - >>> keith.smua.setup_fastsweep(LinSweep(keith.smua.volt, 0, 1, 100)) - >>> ds, _, _ = do0d(keith.smua.fastsweep) + For a 2D sweep (inner=smub, outer=smua): - For a 2D sweep (inner=smub, outer=smua):: + .. code-block:: python - >>> from qcodes.dataset import LinSweep, do0d - >>> keith.smua.setup_fastsweep( - ... LinSweep(keith.smub.volt, 0, 1, 100), # inner - ... LinSweep(keith.smua.volt, 0, 0.5, 20), # outer - ... ) + from qcodes.dataset import LinSweep, do0d + keith.smua.setup_fastsweep( + LinSweep(keith.smub.volt, 0, 1, 100), # inner + LinSweep(keith.smua.volt, 0, 0.5, 20), # outer + ) """ self.timetrace_npts: Parameter = self.add_parameter( @@ -1065,25 +1073,31 @@ def setup_fastsweep( Example 1D: - >>> from qcodes.dataset import LinSweep - >>> keith.smua.setup_fastsweep(LinSweep(keith.smua.volt, 0, 1, 100)) - >>> ds, _, _ = do0d(keith.smua.fastsweep) + .. code-block:: python + + from qcodes.dataset import LinSweep + keith.smua.setup_fastsweep(LinSweep(keith.smua.volt, 0, 1, 100)) + ds, _, _ = do0d(keith.smua.fastsweep) Example 2D (inner=smua, outer=smub): - >>> keith.smua.setup_fastsweep( - ... LinSweep(keith.smua.volt, 0, 1, 100), # inner - ... LinSweep(keith.smub.volt, 0, 0.5, 20), # outer - ... ) - >>> ds, _, _ = do0d(keith.smua.fastsweep) # call on inner channel + .. code-block:: python + + keith.smua.setup_fastsweep( + LinSweep(keith.smua.volt, 0, 1, 100), # inner + LinSweep(keith.smub.volt, 0, 0.5, 20), # outer + ) + ds, _, _ = do0d(keith.smua.fastsweep) # call on inner channel Example 2D (inner=smub, outer=smua): - >>> keith.smua.setup_fastsweep( - ... LinSweep(keith.smub.volt, 0, 1, 100), # inner - ... LinSweep(keith.smua.volt, 0, 0.5, 20), # outer - ... ) - >>> ds, _, _ = do0d(keith.smub.fastsweep) # call on inner channel + .. code-block:: python + + keith.smua.setup_fastsweep( + LinSweep(keith.smub.volt, 0, 1, 100), # inner + LinSweep(keith.smua.volt, 0, 0.5, 20), # outer + ) + ds, _, _ = do0d(keith.smub.fastsweep) # call on inner channel """ diff --git a/src/qcodes/instrument_drivers/Keysight/keysight_e4980a.py b/src/qcodes/instrument_drivers/Keysight/keysight_e4980a.py index 76fe3c0383b..5da7425fadc 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysight_e4980a.py +++ b/src/qcodes/instrument_drivers/Keysight/keysight_e4980a.py @@ -42,12 +42,16 @@ class KeysightE4980AMeasurementPair(MultiParameter): To create a measurement data with capacitance=1.2, and dissipation_factor=3.4. - >>> data = KeysightE4980AMeasurementPair(name="CPD", - names=("capacitance", "dissipation_factor"), - units=("F", "")) - >>> data.set((1.2, 3.4)) - >>> data.get() - (1.2, 3.4) + .. code-block:: python + + data = KeysightE4980AMeasurementPair( + name="CPD", + names=("capacitance", "dissipation_factor"), + units=("F", ""), + ) + data.set((1.2, 3.4)) + data.get() + (1.2, 3.4) """ diff --git a/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py b/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py index 433090d1389..53987d4cd22 100644 --- a/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py +++ b/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py @@ -628,9 +628,12 @@ def _get_sum_terms(available_terms: "Sequence[int]", number: int) -> list[int]: is a usually the default status code for success. Example: - >>> terms = [1, 16, 32, 64, 128] - >>> get_sum_terms(terms, 96) - ... [64, 32] # This is correct because 96=64+32 + + .. code-block:: python + + terms = [1, 16, 32, 64, 128] + get_sum_terms(terms, 96) + # returns [64, 32] because 96=64+32 """ terms_in_number: list[int] = [] diff --git a/src/qcodes/instrument_drivers/conftest.py b/src/qcodes/instrument_drivers/conftest.py new file mode 100644 index 00000000000..913d5113eea --- /dev/null +++ b/src/qcodes/instrument_drivers/conftest.py @@ -0,0 +1,9 @@ +import os + +# Drivers that require optional third-party libraries not generally available. +# These must be excluded from doctest collection to avoid ImportErrors. +collect_ignore_glob = [ + os.path.join("Galil", "*"), + os.path.join("Minicircuits", "*"), + os.path.join("QuantumDesign", "DynaCoolPPMS", "private", "*"), +] diff --git a/src/qcodes/instrument_drivers/rigol/Rigol_DG1062.py b/src/qcodes/instrument_drivers/rigol/Rigol_DG1062.py index ac6f6d7297b..cc28e8c750c 100644 --- a/src/qcodes/instrument_drivers/rigol/Rigol_DG1062.py +++ b/src/qcodes/instrument_drivers/rigol/Rigol_DG1062.py @@ -286,11 +286,19 @@ def apply(self, **kwargs: Any) -> None: """ Public interface to apply a waveform on the channel Example: - >>> gd = RigolDG1062("gd", "TCPIP0::169.254.187.99::inst0::INSTR") - >>> gd.channels[0].apply(waveform="SIN", freq=1E3, ampl=1.0, offset=0, phase=0) + + .. code-block:: python + + gd = RigolDG1062("gd", "TCPIP0::169.254.187.99::inst0::INSTR") + gd.channels[0].apply(waveform="SIN", freq=1E3, ampl=1.0, offset=0, phase=0) + Valid waveforms are: HARM, NOIS, RAMP, SIN, SQU, TRI, USER, DC, ARB To find the correct arguments of each waveform we can e.g. do: - >>> help(gd.channels[0].sin) + + .. code-block:: python + + help(gd.channels[0].sin) + Notice the lower case when accessing the waveform through convenience functions. If not kwargs are given a dictionary with the current waveform diff --git a/src/qcodes/instrument_drivers/stahl/stahl.py b/src/qcodes/instrument_drivers/stahl/stahl.py index afa3637fe84..30757ca15ba 100644 --- a/src/qcodes/instrument_drivers/stahl/stahl.py +++ b/src/qcodes/instrument_drivers/stahl/stahl.py @@ -35,9 +35,12 @@ def chain(*functions: Callable[..., Any]) -> Callable[..., Any]: The output of the first callable is piped to the input of the second, etc. Example: - >>> def f(): - >>> return "1.2" - >>> chain(f, float)() # return 1.2 as float + + .. code-block:: python + + def f(): + return "1.2" + chain(f, float)() # return 1.2 as float """ diff --git a/src/qcodes/instrument_drivers/stanford_research/SR830.py b/src/qcodes/instrument_drivers/stanford_research/SR830.py index 6ce5413f602..f21c5080080 100644 --- a/src/qcodes/instrument_drivers/stanford_research/SR830.py +++ b/src/qcodes/instrument_drivers/stanford_research/SR830.py @@ -769,10 +769,15 @@ def snap(self, *parameters: str) -> tuple[float, ...]: A tuple of floating point values in the same order as requested. Examples: - >>> lockin.snap('x','y') -> tuple(x,y) - >>> lockin.snap('aux1','aux2','freq','phase') - >>> -> tuple(aux1,aux2,freq,phase) + .. code-block:: python + + lockin.snap('x','y') -> tuple(x,y) + + .. code-block:: python + + lockin.snap('aux1','aux2','freq','phase') + -> tuple(aux1,aux2,freq,phase) Note: Volts for x, y, r, and aux 1-4 diff --git a/src/qcodes/logger/instrument_logger.py b/src/qcodes/logger/instrument_logger.py index ddc32374fcb..35192f74f8f 100644 --- a/src/qcodes/logger/instrument_logger.py +++ b/src/qcodes/logger/instrument_logger.py @@ -32,9 +32,11 @@ class InstrumentLoggerAdapter(logging.LoggerAdapter): instance. The context data gets stored in the `extra` dictionary as a property of the - Adapter. It is filled by the ``__init__`` method:: + Adapter. It is filled by the ``__init__`` method: - >>> LoggerAdapter(log, {'instrument': instrument_instance}) + .. code-block:: python + + LoggerAdapter(log, {'instrument': instrument_instance}) """ @@ -163,11 +165,13 @@ def filter_instrument( the supplied instruments to pass. Example: - >>> h1, h2 = logger.get_console_handler(), logger.get_file_handler() - >>> with logger.filter_instruments((qdac, dmm2), handler=[h1, h2]): - >>> qdac.ch01(1) # logged - >>> v1 = dmm2.v() # logged - >>> v2 = keithley.v() # not logged + .. code-block:: python + + h1, h2 = logger.get_console_handler(), logger.get_file_handler() + with logger.filter_instruments((qdac, dmm2), handler=[h1, h2]): + qdac.ch01(1) # logged + v1 = dmm2.v() # logged + v2 = keithley.v() # not logged Args: instrument: The instrument or sequence of instruments to enable diff --git a/src/qcodes/logger/log_analysis.py b/src/qcodes/logger/log_analysis.py index 2f58b5ed9b3..65fe3ef5cb5 100644 --- a/src/qcodes/logger/log_analysis.py +++ b/src/qcodes/logger/log_analysis.py @@ -161,9 +161,12 @@ def capture_dataframe( Context manager to capture the logs in a :class:`pd.DataFrame` Example: - >>> with logger.capture_dataframe() as (handler, cb): - >>> qdac.ch01(1) # some commands - >>> data_frame = cb() + + .. code-block:: python + + with logger.capture_dataframe() as (handler, cb): + qdac.ch01(1) # some commands + data_frame = cb() Args: level: Level at which to capture. diff --git a/src/qcodes/logger/logger.py b/src/qcodes/logger/logger.py index 8291d4b9dd7..507511c0e7f 100644 --- a/src/qcodes/logger/logger.py +++ b/src/qcodes/logger/logger.py @@ -357,8 +357,10 @@ def handler_level( Context manager to temporarily change the level of handlers. Example: - >>> with logger.handler_level(level=logging.DEBUG, handler=[h1, h1]): - >>> root_logger.debug('this is now visible') + .. code-block:: python + + with logger.handler_level(level=logging.DEBUG, handler=[h1, h1]): + root_logger.debug('this is now visible') Args: level: Level to set the handlers to. @@ -384,8 +386,10 @@ def console_level(level: LevelType) -> "Generator[None, None, None]": handler. Example: - >>> with logger.console_level(level=logging.DEBUG): - >>> root_logger.debug('this is now visible') + .. code-block:: python + + with logger.console_level(level=logging.DEBUG): + root_logger.debug('this is now visible') Args: level: Level to set the console handler to. @@ -404,9 +408,11 @@ class LogCapture: from a specific logger. Example: - >>> with LogCapture() as logs: - >>> code_that_makes_logs(...) - >>> log_str = logs.value + .. code-block:: python + + with LogCapture() as logs: + code_that_makes_logs(...) + log_str = logs.value """ diff --git a/src/qcodes/math_utils/field_vector.py b/src/qcodes/math_utils/field_vector.py index 032c60c60f8..78dc4533727 100644 --- a/src/qcodes/math_utils/field_vector.py +++ b/src/qcodes/math_utils/field_vector.py @@ -192,17 +192,19 @@ def set_vector(self, **new_values: float) -> None: Reset the the values of the vector. Examples: - >>> f = FieldVector(x=0, y=2, z=6) - >>> f.set_vector(x=9, y=3, z=1) - >>> f.set_vector(r=1, theta=30.0, phi=10.0) - # The following should raise a value error: - # "Can only set vector with a complete value set" - >>> f.set_vector(x=9, y=0) - # Although mathematically it is possible to compute the complete - # vector from the values given, this is too hard to implement with - # generality (and not worth it), so the following will raise the - # above-mentioned ValueError too. - >>> f.set_vector(x=9, y=0, r=3) + .. code-block:: python + + f = FieldVector(x=0, y=2, z=6) + f.set_vector(x=9, y=3, z=1) + f.set_vector(r=1, theta=30.0, phi=10.0) + # The following should raise a value error: + # "Can only set vector with a complete value set" + f.set_vector(x=9, y=0) + # Although mathematically it is possible to compute the complete + # vector from the values given, this is too hard to implement with + # generality (and not worth it), so the following will raise the + # above-mentioned ValueError too. + f.set_vector(x=9, y=0, r=3) """ names = sorted(list(new_values.keys())) @@ -221,14 +223,16 @@ def set_component(self, **new_values: float) -> None: other, setting one has to effect the other). Examples: - >>> f = FieldVector(x=2, y=3, z=4) - # Since r is part of the set (r, theta, phi) representing - # spherical coordinates, setting r means that theta and phi are - # kept constant and only r is changed. After changing r, - # (x, y, z) values are recomputed, as is the rho coordinate. - # Internally we arrange this by setting x, y, z and rho to None - # and calling self._compute_unknowns(). - >>> f.set_component(r=10) + .. code-block:: python + + f = FieldVector(x=2, y=3, z=4) + # Since r is part of the set (r, theta, phi) representing + # spherical coordinates, setting r means that theta and phi are + # kept constant and only r is changed. After changing r, + # (x, y, z) values are recomputed, as is the rho coordinate. + # Internally we arrange this by setting x, y, z and rho to None + # and calling self._compute_unknowns(). + f.set_component(r=10) Args: new_values (dict): Keys representing parameter names and values the diff --git a/src/qcodes/parameters/parameter.py b/src/qcodes/parameters/parameter.py index 7bcfbf304a5..3f975c9f46b 100644 --- a/src/qcodes/parameters/parameter.py +++ b/src/qcodes/parameters/parameter.py @@ -544,12 +544,14 @@ def sweep( iterated over. Examples: - >>> sweep(0, 10, num=5) - [0.0, 2.5, 5.0, 7.5, 10.0] - >>> sweep(5, 10, step=1) - [5.0, 6.0, 7.0, 8.0, 9.0, 10.0] - >>> sweep(15, 10.5, step=1.5) - >[15.0, 13.5, 12.0, 10.5] + .. code-block:: python + + sweep(0, 10, num=5) + # [0.0, 2.5, 5.0, 7.5, 10.0] + sweep(5, 10, step=1) + # [5.0, 6.0, 7.0, 8.0, 9.0, 10.0] + sweep(15, 10.5, step=1.5) + # [15.0, 13.5, 12.0, 10.5] """ return SweepFixedValues(self, start=start, stop=stop, step=step, num=num) diff --git a/src/qcodes/parameters/parameter_base.py b/src/qcodes/parameters/parameter_base.py index c009565b011..e8ba10abf40 100644 --- a/src/qcodes/parameters/parameter_base.py +++ b/src/qcodes/parameters/parameter_base.py @@ -71,11 +71,13 @@ class _SetParamContext: Example usage: - >>> v = dac.voltage() - >>> with dac.voltage.set_to(-1): - ... # Do stuff with the DAC output set to -1 V. - ... - >>> assert abs(dac.voltage() - v) <= tolerance + .. code-block:: python + + v = dac.voltage() + with dac.voltage.set_to(-1): + # Do stuff with the DAC output set to -1 V. + ... + assert abs(dac.voltage() - v) <= tolerance """ @@ -1223,16 +1225,18 @@ def set_to( This may be overridden with ``allow_changes=True``. Examples: - >>> from qcodes.parameters import Parameter - >>> p = Parameter("p", set_cmd=None, get_cmd=None) - >>> p.set(2) - >>> with p.set_to(3): - ... print(f"p value in with block {p.get()}") # prints 3 - ... p.set(5) # raises an exception - >>> print(f"p value outside with block {p.get()}") # prints 2 - >>> with p.set_to(3, allow_changes=True): - ... p.set(5) # now this works - >>> print(f"value after second block: {p.get()}") # still prints 2 + .. code-block:: python + + from qcodes.parameters import Parameter + p = Parameter("p", set_cmd=None, get_cmd=None) + p.set(2) + with p.set_to(3): + print(f"p value in with block {p.get()}") # prints 3 + p.set(5) # raises an exception + print(f"p value outside with block {p.get()}") # prints 2 + with p.set_to(3, allow_changes=True): + p.set(5) # now this works + print(f"value after second block: {p.get()}") # still prints 2 """ context_manager = _SetParamContext(self, value, allow_changes=allow_changes) @@ -1249,14 +1253,16 @@ def restore_at_exit(self, allow_changes: bool = True) -> _SetParamContext: unintentionally modifies a parameter. Example: - >>> p = Parameter("p", set_cmd=None, get_cmd=None) - >>> p.set(2) - >>> with p.restore_at_exit(): - ... p.set(3) - ... print(f"value inside with block: {p.get()}") # prints 3 - >>> print(f"value after with block: {p.get()}") # prints 2 - >>> with p.restore_at_exit(allow_changes=False): - ... p.set(5) # raises an exception + .. code-block:: python + + p = Parameter("p", set_cmd=None, get_cmd=None) + p.set(2) + with p.restore_at_exit(): + p.set(3) + print(f"value inside with block: {p.get()}") # prints 3 + print(f"value after with block: {p.get()}") # prints 2 + with p.restore_at_exit(allow_changes=False): + p.set(5) # raises an exception """ return self.set_to(self.cache(), allow_changes=allow_changes) @@ -1420,10 +1426,12 @@ class GetLatest(DelegateAttributes, Generic[ParameterDataTypeVar]): ``parameter.get_latest``. Examples: - >>> # Can be called: - >>> param.get_latest() - >>> # Or used as if it were a gettable-only parameter itself: - >>> Loop(...).each(param.get_latest) + .. code-block:: python + + # Can be called: + param.get_latest() + # Or used as if it were a gettable-only parameter itself: + Loop(...).each(param.get_latest) Args: parameter: Parameter to be wrapped. diff --git a/src/qcodes/parameters/scaled_paramter.py b/src/qcodes/parameters/scaled_paramter.py index 917c7b1aafc..66b375fb005 100644 --- a/src/qcodes/parameters/scaled_paramter.py +++ b/src/qcodes/parameters/scaled_paramter.py @@ -24,14 +24,23 @@ class ScaledParameter(Parameter): Examples: Resistive voltage divider - >>> vd = ScaledParameter(dac.chan0, division = 10) + + .. code-block:: python + + vd = ScaledParameter(dac.chan0, division = 10) Voltage multiplier - >>> vb = ScaledParameter(dac.chan0, gain = 30, name = 'Vb') + + .. code-block:: python + + vb = ScaledParameter(dac.chan0, gain = 30, name = 'Vb') Transimpedance amplifier - >>> Id = ScaledParameter(multimeter.amplitude, - ... division = 1e6, name = 'Id', unit = 'A') + + .. code-block:: python + + Id = ScaledParameter(multimeter.amplitude, + division = 1e6, name = 'Id', unit = 'A') Args: output: Physical Parameter that need conversion. diff --git a/src/qcodes/parameters/sweep_values.py b/src/qcodes/parameters/sweep_values.py index 8ce710347bb..6f914f236d2 100644 --- a/src/qcodes/parameters/sweep_values.py +++ b/src/qcodes/parameters/sweep_values.py @@ -43,12 +43,14 @@ def make_sweep( numpy.ndarray: numbers over a specified interval as a ``numpy.linspace``. Examples: - >>> make_sweep(0, 10, num=5) - [0.0, 2.5, 5.0, 7.5, 10.0] - >>> make_sweep(5, 10, step=1) - [5.0, 6.0, 7.0, 8.0, 9.0, 10.0] - >>> make_sweep(15, 10.5, step=1.5) - >[15.0, 13.5, 12.0, 10.5] + .. code-block:: python + + make_sweep(0, 10, num=5) + # [0.0, 2.5, 5.0, 7.5, 10.0] + make_sweep(5, 10, step=1) + # [5.0, 6.0, 7.0, 8.0, 9.0, 10.0] + make_sweep(15, 10.5, step=1.5) + # [15.0, 13.5, 12.0, 10.5] """ if step and num: @@ -90,13 +92,13 @@ class SweepValues(Metadatable): Must be subclassed to provide the sweep values Intended use is to iterate over in a sweep, so it must support: - >>> .__iter__ # (and .__next__ if necessary). - >>> .set # is provided by the base class + - ``.__iter__`` (and ``.__next__`` if necessary). + - ``.set`` is provided by the base class Optionally, it can have a feedback method that allows the sweep to pass measurements back to this object for adaptive sampling: - >>> .feedback(set_values, measured_values) + - ``.feedback(set_values, measured_values)`` Todo: - Link to adawptive sweep @@ -114,12 +116,14 @@ class SweepValues(Metadatable): example usage: - >>> for i, value in eumerate(sv): + .. code-block:: python + + for i, value in enumerate(sv): sv.set(value) sleep(delay) vals = measure() - sv.feedback((i, ), vals) # optional - sweep should not assume - # .feedback exists + sv.feedback((i, ), vals) # optional - sweep should not assume + # .feedback exists note though that sweeps should only require set and __iter__ - ie "for val in sv", so any class that implements these may be used in sweeps. @@ -188,21 +192,25 @@ class SweepFixedValues(SweepValues): A SweepFixedValues object is normally created by slicing a Parameter p: - >>> sv = p[1.2:2:0.01] # slice notation - sv = p[1, 1.1, 1.3, 1.6] # explicit individual values - sv = p[1.2:2:0.01, 2:3:0.02] # sequence of slices - sv = p[logrange(1,10,.01)] # some function that returns a sequence + .. code-block:: python + + sv = p[1.2:2:0.01] # slice notation + sv = p[1, 1.1, 1.3, 1.6] # explicit individual values + sv = p[1.2:2:0.01, 2:3:0.02] # sequence of slices + sv = p[logrange(1,10,.01)] # some function that returns a sequence You can also use list operations to modify these: - >>> sv += p[2:3:.01] # (another SweepFixedValues of the same parameter) - sv += [4, 5, 6] # (a bare sequence) - sv.extend(p[2:3:.01]) - sv.append(3.2) - sv.reverse() - sv2 = reversed(sv) - sv3 = sv + sv2 - sv4 = sv.copy() + .. code-block:: python + + sv += p[2:3:.01] # (another SweepFixedValues of the same parameter) + sv += [4, 5, 6] # (a bare sequence) + sv.extend(p[2:3:.01]) + sv.append(3.2) + sv.reverse() + sv2 = reversed(sv) + sv3 = sv + sv2 + sv4 = sv.copy() note though that sweeps should only require set and __iter__ - ie "for val in sv", so any class that implements these may be used in sweeps. diff --git a/src/qcodes/utils/partial_utils.py b/src/qcodes/utils/partial_utils.py index fa7587a1430..760cd81db01 100644 --- a/src/qcodes/utils/partial_utils.py +++ b/src/qcodes/utils/partial_utils.py @@ -15,12 +15,14 @@ def partial_with_docstring( Consider the follow example why this is needed: - >>> from functools import partial - >>> def f(): - >>> ... pass - >>> g = partial(f) - >>> g.__doc__ = "bla" - >>> help(g) # this will print the docstring of partial and not the docstring set above + .. code-block:: python + + from functools import partial + def f(): + pass + g = partial(f) + g.__doc__ = "bla" + help(g) # this will print the docstring of partial and not the docstring set above Args: func: A function that its docstring will be accessed. diff --git a/src/qcodes/utils/threading_utils.py b/src/qcodes/utils/threading_utils.py index 552c51e79a7..cd877b5a374 100644 --- a/src/qcodes/utils/threading_utils.py +++ b/src/qcodes/utils/threading_utils.py @@ -21,10 +21,12 @@ class RespondingThread(threading.Thread, Generic[T]): so, if you have a function `f` where `f(1, 2, a=3) == 4`, then: - >>> thread = RespondingThread(target=f, args=(1, 2), kwargs={'a': 3}) - >>> thread.start() - >>> # do other things while this is running - >>> out = thread.output() # out is 4 + .. code-block:: python + + thread = RespondingThread(target=f, args=(1, 2), kwargs={'a': 3}) + thread.start() + # do other things while this is running + out = thread.output() # out is 4 """ def __init__( diff --git a/src/qcodes/validators/validators.py b/src/qcodes/validators/validators.py index 37beb98fbf8..2d008df51f2 100644 --- a/src/qcodes/validators/validators.py +++ b/src/qcodes/validators/validators.py @@ -709,17 +709,23 @@ class MultiType(Validator[Any]): Examples: 1. To allow numbers as well as "off": - >>> MultiType(Numbers(), Enum("off")) + .. code-block:: python + + MultiType(Numbers(), Enum("off")) or: - >>> MultiType(Numbers(), Enum("off"), combiner='OR') + .. code-block:: python + + MultiType(Numbers(), Enum("off"), combiner='OR') 2. To require values that are divisible by 0.001 while >=0.002 and <=50000.0 - >>> MultiType(PermissiveMultiples(divisor=1e-3), - >>> Numbers(min_value=2e-3, max_value=5e4), - >>> combiner='AND') + .. code-block:: python + + MultiType(PermissiveMultiples(divisor=1e-3), + Numbers(min_value=2e-3, max_value=5e4), + combiner='AND') Raises: TypeError: If no validators provided. Or if any of the provided @@ -793,7 +799,9 @@ class MultiTypeOr(MultiType): Example: To allow numbers as well as "off": - >>> MultiTypeOr(Numbers(), Enum("off")) + .. code-block:: python + + MultiTypeOr(Numbers(), Enum("off")) Raises: TypeError: If no validators provided. Or if any of the provided @@ -821,9 +829,11 @@ class MultiTypeAnd(MultiType): Example: To require values that are divisible by 0.001 while >=0.002 and <=50000.0 - >>> MultiType(PermissiveMultiples(divisor=1e-3), - >>> Numbers(min_value=2e-3, max_value=5e4), - >>> combiner='AND') + .. code-block:: python + + MultiType(PermissiveMultiples(divisor=1e-3), + Numbers(min_value=2e-3, max_value=5e4), + combiner='AND') Raises: TypeError: If no validators provided. Or if any of the provided From 15a687b0d2e6c84590dd8f4ac3085006fd1e748d Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sun, 26 Apr 2026 10:44:10 +0200 Subject: [PATCH 3/8] Convert illustrative code blocks to runnable doctests where possible Convert 14 examples from .. code-block:: python back to executable >>> doctest syntax in cases where no hardware or complex external state is needed: - validators: MultiType, MultiTypeOr, MultiTypeAnd - field_vector: set_vector, set_component - parameter_base: set_to, restore_at_exit - parameter: sweep - sweep_values: make_sweep - stahl: chain - threading_utils: RespondingThread - logger: LogCapture - lakeshore_base: _get_sum_terms - keysight_e4980a: KeysightE4980AMeasurementPair 5 examples remain as skipped or code-blocks because they require instruments, database infrastructure, or produce complex output (Config.add, guids_from_list_str, DataSaver.add_result, Instrument.close_all, partial_with_docstring). --- .../Keysight/keysight_e4980a.py | 18 +++---- .../Lakeshore/lakeshore_base.py | 9 ++-- src/qcodes/instrument_drivers/stahl/stahl.py | 9 ++-- src/qcodes/logger/logger.py | 10 ++-- src/qcodes/math_utils/field_vector.py | 51 ++++++++++-------- src/qcodes/parameters/parameter.py | 16 +++--- src/qcodes/parameters/parameter_base.py | 53 +++++++++++-------- src/qcodes/parameters/sweep_values.py | 14 +++-- src/qcodes/utils/partial_utils.py | 14 +++-- src/qcodes/utils/threading_utils.py | 17 +++--- src/qcodes/validators/validators.py | 32 +++++------ 11 files changed, 121 insertions(+), 122 deletions(-) diff --git a/src/qcodes/instrument_drivers/Keysight/keysight_e4980a.py b/src/qcodes/instrument_drivers/Keysight/keysight_e4980a.py index 5da7425fadc..b8d5e32cd20 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysight_e4980a.py +++ b/src/qcodes/instrument_drivers/Keysight/keysight_e4980a.py @@ -42,16 +42,14 @@ class KeysightE4980AMeasurementPair(MultiParameter): To create a measurement data with capacitance=1.2, and dissipation_factor=3.4. - .. code-block:: python - - data = KeysightE4980AMeasurementPair( - name="CPD", - names=("capacitance", "dissipation_factor"), - units=("F", ""), - ) - data.set((1.2, 3.4)) - data.get() - (1.2, 3.4) + >>> data = KeysightE4980AMeasurementPair( + ... name="CPD", + ... names=("capacitance", "dissipation_factor"), + ... units=("F", ""), + ... ) + >>> data.set((1.2, 3.4)) + >>> data.get() + (1.2, 3.4) """ diff --git a/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py b/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py index 53987d4cd22..9407aaf43ba 100644 --- a/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py +++ b/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py @@ -628,12 +628,9 @@ def _get_sum_terms(available_terms: "Sequence[int]", number: int) -> list[int]: is a usually the default status code for success. Example: - - .. code-block:: python - - terms = [1, 16, 32, 64, 128] - get_sum_terms(terms, 96) - # returns [64, 32] because 96=64+32 + >>> terms = [1, 16, 32, 64, 128] + >>> [int(x) for x in LakeshoreBaseSensorChannel._get_sum_terms(terms, 96)] + [64, 32] """ terms_in_number: list[int] = [] diff --git a/src/qcodes/instrument_drivers/stahl/stahl.py b/src/qcodes/instrument_drivers/stahl/stahl.py index 30757ca15ba..bf8306fb6b5 100644 --- a/src/qcodes/instrument_drivers/stahl/stahl.py +++ b/src/qcodes/instrument_drivers/stahl/stahl.py @@ -35,12 +35,11 @@ def chain(*functions: Callable[..., Any]) -> Callable[..., Any]: The output of the first callable is piped to the input of the second, etc. Example: + >>> def f(): + ... return "1.2" + >>> chain(f, float)() + 1.2 - .. code-block:: python - - def f(): - return "1.2" - chain(f, float)() # return 1.2 as float """ diff --git a/src/qcodes/logger/logger.py b/src/qcodes/logger/logger.py index 507511c0e7f..f7f6a3e4c98 100644 --- a/src/qcodes/logger/logger.py +++ b/src/qcodes/logger/logger.py @@ -408,11 +408,11 @@ class LogCapture: from a specific logger. Example: - .. code-block:: python - - with LogCapture() as logs: - code_that_makes_logs(...) - log_str = logs.value + >>> import logging + >>> with LogCapture() as logs: + ... logging.warning("test message") + >>> isinstance(logs.value, str) + True """ diff --git a/src/qcodes/math_utils/field_vector.py b/src/qcodes/math_utils/field_vector.py index 78dc4533727..a84ccd17a18 100644 --- a/src/qcodes/math_utils/field_vector.py +++ b/src/qcodes/math_utils/field_vector.py @@ -192,19 +192,27 @@ def set_vector(self, **new_values: float) -> None: Reset the the values of the vector. Examples: - .. code-block:: python - - f = FieldVector(x=0, y=2, z=6) - f.set_vector(x=9, y=3, z=1) - f.set_vector(r=1, theta=30.0, phi=10.0) - # The following should raise a value error: - # "Can only set vector with a complete value set" - f.set_vector(x=9, y=0) - # Although mathematically it is possible to compute the complete - # vector from the values given, this is too hard to implement with - # generality (and not worth it), so the following will raise the - # above-mentioned ValueError too. - f.set_vector(x=9, y=0, r=3) + >>> f = FieldVector(x=0, y=2, z=6) + >>> f.set_vector(x=9, y=3, z=1) + >>> f.set_vector(r=1, theta=30.0, phi=10.0) + + The following raises a ValueError because the value set is + incomplete: + + >>> f.set_vector(x=9, y=0) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: ... + + Although mathematically it is possible to compute the complete + vector from the values given, this is too hard to implement with + generality (and not worth it), so the following will raise a + ValueError too: + + >>> f.set_vector(x=9, y=0, r=3) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: ... """ names = sorted(list(new_values.keys())) @@ -223,16 +231,13 @@ def set_component(self, **new_values: float) -> None: other, setting one has to effect the other). Examples: - .. code-block:: python - - f = FieldVector(x=2, y=3, z=4) - # Since r is part of the set (r, theta, phi) representing - # spherical coordinates, setting r means that theta and phi are - # kept constant and only r is changed. After changing r, - # (x, y, z) values are recomputed, as is the rho coordinate. - # Internally we arrange this by setting x, y, z and rho to None - # and calling self._compute_unknowns(). - f.set_component(r=10) + Since r is part of the set (r, theta, phi) representing + spherical coordinates, setting r means that theta and phi are + kept constant and only r is changed. After changing r, + (x, y, z) values are recomputed, as is the rho coordinate. + + >>> f = FieldVector(x=2, y=3, z=4) + >>> f.set_component(r=10) Args: new_values (dict): Keys representing parameter names and values the diff --git a/src/qcodes/parameters/parameter.py b/src/qcodes/parameters/parameter.py index 3f975c9f46b..0ffd5e39df3 100644 --- a/src/qcodes/parameters/parameter.py +++ b/src/qcodes/parameters/parameter.py @@ -544,14 +544,14 @@ def sweep( iterated over. Examples: - .. code-block:: python - - sweep(0, 10, num=5) - # [0.0, 2.5, 5.0, 7.5, 10.0] - sweep(5, 10, step=1) - # [5.0, 6.0, 7.0, 8.0, 9.0, 10.0] - sweep(15, 10.5, step=1.5) - # [15.0, 13.5, 12.0, 10.5] + >>> from qcodes.parameters import Parameter + >>> p = Parameter("p", set_cmd=None, get_cmd=None) + >>> list(p.sweep(0, 10, num=5)) + [0.0, 2.5, 5.0, 7.5, 10.0] + >>> list(p.sweep(5, 10, step=1)) + [5.0, 6.0, 7.0, 8.0, 9.0, 10.0] + >>> list(p.sweep(15, 10.5, step=1.5)) + [15.0, 13.5, 12.0, 10.5] """ return SweepFixedValues(self, start=start, stop=stop, step=step, num=num) diff --git a/src/qcodes/parameters/parameter_base.py b/src/qcodes/parameters/parameter_base.py index e8ba10abf40..c68ff029456 100644 --- a/src/qcodes/parameters/parameter_base.py +++ b/src/qcodes/parameters/parameter_base.py @@ -1225,18 +1225,23 @@ def set_to( This may be overridden with ``allow_changes=True``. Examples: - .. code-block:: python - - from qcodes.parameters import Parameter - p = Parameter("p", set_cmd=None, get_cmd=None) - p.set(2) - with p.set_to(3): - print(f"p value in with block {p.get()}") # prints 3 - p.set(5) # raises an exception - print(f"p value outside with block {p.get()}") # prints 2 - with p.set_to(3, allow_changes=True): - p.set(5) # now this works - print(f"value after second block: {p.get()}") # still prints 2 + >>> from qcodes.parameters import Parameter + >>> p = Parameter("p", set_cmd=None, get_cmd=None) + >>> p.set(2) + >>> with p.set_to(3): + ... print(f"p value in with block {p.get()}") + ... try: + ... p.set(5) + ... except TypeError: + ... print("raises an exception") + p value in with block 3 + raises an exception + >>> print(f"p value outside with block {p.get()}") + p value outside with block 2 + >>> with p.set_to(3, allow_changes=True): + ... p.set(5) + >>> print(f"value after second block: {p.get()}") + value after second block: 2 """ context_manager = _SetParamContext(self, value, allow_changes=allow_changes) @@ -1253,16 +1258,20 @@ def restore_at_exit(self, allow_changes: bool = True) -> _SetParamContext: unintentionally modifies a parameter. Example: - .. code-block:: python - - p = Parameter("p", set_cmd=None, get_cmd=None) - p.set(2) - with p.restore_at_exit(): - p.set(3) - print(f"value inside with block: {p.get()}") # prints 3 - print(f"value after with block: {p.get()}") # prints 2 - with p.restore_at_exit(allow_changes=False): - p.set(5) # raises an exception + >>> from qcodes.parameters import Parameter + >>> p = Parameter("p", set_cmd=None, get_cmd=None) + >>> p.set(2) + >>> with p.restore_at_exit(): + ... p.set(3) + ... print(f"value inside with block: {p.get()}") + value inside with block: 3 + >>> print(f"value after with block: {p.get()}") + value after with block: 2 + >>> with p.restore_at_exit(allow_changes=False): + ... p.set(5) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + TypeError: ... """ return self.set_to(self.cache(), allow_changes=allow_changes) diff --git a/src/qcodes/parameters/sweep_values.py b/src/qcodes/parameters/sweep_values.py index 6f914f236d2..491eb585601 100644 --- a/src/qcodes/parameters/sweep_values.py +++ b/src/qcodes/parameters/sweep_values.py @@ -43,14 +43,12 @@ def make_sweep( numpy.ndarray: numbers over a specified interval as a ``numpy.linspace``. Examples: - .. code-block:: python - - make_sweep(0, 10, num=5) - # [0.0, 2.5, 5.0, 7.5, 10.0] - make_sweep(5, 10, step=1) - # [5.0, 6.0, 7.0, 8.0, 9.0, 10.0] - make_sweep(15, 10.5, step=1.5) - # [15.0, 13.5, 12.0, 10.5] + >>> make_sweep(0, 10, num=5) + [0.0, 2.5, 5.0, 7.5, 10.0] + >>> make_sweep(5, 10, step=1) + [5.0, 6.0, 7.0, 8.0, 9.0, 10.0] + >>> make_sweep(15, 10.5, step=1.5) + [15.0, 13.5, 12.0, 10.5] """ if step and num: diff --git a/src/qcodes/utils/partial_utils.py b/src/qcodes/utils/partial_utils.py index 760cd81db01..906f76d1389 100644 --- a/src/qcodes/utils/partial_utils.py +++ b/src/qcodes/utils/partial_utils.py @@ -15,14 +15,12 @@ def partial_with_docstring( Consider the follow example why this is needed: - .. code-block:: python - - from functools import partial - def f(): - pass - g = partial(f) - g.__doc__ = "bla" - help(g) # this will print the docstring of partial and not the docstring set above + >>> from functools import partial # doctest: +SKIP + >>> def f(): # doctest: +SKIP + ... pass + >>> g = partial(f) # doctest: +SKIP + >>> g.__doc__ = "bla" # doctest: +SKIP + >>> help(g) # doctest: +SKIP Args: func: A function that its docstring will be accessed. diff --git a/src/qcodes/utils/threading_utils.py b/src/qcodes/utils/threading_utils.py index cd877b5a374..327baee49ec 100644 --- a/src/qcodes/utils/threading_utils.py +++ b/src/qcodes/utils/threading_utils.py @@ -19,14 +19,15 @@ class RespondingThread(threading.Thread, Generic[T]): The `output` method joins the thread, then checks for errors and returns the output value. - so, if you have a function `f` where `f(1, 2, a=3) == 4`, then: - - .. code-block:: python - - thread = RespondingThread(target=f, args=(1, 2), kwargs={'a': 3}) - thread.start() - # do other things while this is running - out = thread.output() # out is 4 + so, if you have a function `f` where `f(1, 2, a=3) == 6`, then: + + >>> def f(x, y, a=0): + ... return x + y + a + >>> thread = RespondingThread(target=f, args=(1, 2), kwargs={'a': 3}) + >>> thread.start() + >>> out = thread.output() + >>> out + 6 """ def __init__( diff --git a/src/qcodes/validators/validators.py b/src/qcodes/validators/validators.py index 2d008df51f2..fddaa939e50 100644 --- a/src/qcodes/validators/validators.py +++ b/src/qcodes/validators/validators.py @@ -709,23 +709,20 @@ class MultiType(Validator[Any]): Examples: 1. To allow numbers as well as "off": - .. code-block:: python - - MultiType(Numbers(), Enum("off")) + >>> MultiType(Numbers(), Enum("off")) + or: - .. code-block:: python - - MultiType(Numbers(), Enum("off"), combiner='OR') + >>> MultiType(Numbers(), Enum("off"), combiner='OR') + 2. To require values that are divisible by 0.001 while >=0.002 and <=50000.0 - .. code-block:: python - - MultiType(PermissiveMultiples(divisor=1e-3), - Numbers(min_value=2e-3, max_value=5e4), - combiner='AND') + >>> MultiType(PermissiveMultiples(divisor=1e-3), + ... Numbers(min_value=2e-3, max_value=5e4), + ... combiner='AND') + Raises: TypeError: If no validators provided. Or if any of the provided @@ -799,9 +796,8 @@ class MultiTypeOr(MultiType): Example: To allow numbers as well as "off": - .. code-block:: python - - MultiTypeOr(Numbers(), Enum("off")) + >>> MultiTypeOr(Numbers(), Enum("off")) + Raises: TypeError: If no validators provided. Or if any of the provided @@ -829,11 +825,9 @@ class MultiTypeAnd(MultiType): Example: To require values that are divisible by 0.001 while >=0.002 and <=50000.0 - .. code-block:: python - - MultiType(PermissiveMultiples(divisor=1e-3), - Numbers(min_value=2e-3, max_value=5e4), - combiner='AND') + >>> MultiTypeAnd(PermissiveMultiples(divisor=1e-3), + ... Numbers(min_value=2e-3, max_value=5e4)) + Raises: TypeError: If no validators provided. Or if any of the provided From 327e5b4b00a048aab7fc8ffc6ee53ab1c3b5f6ad Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sun, 26 Apr 2026 10:46:47 +0200 Subject: [PATCH 4/8] Skip deprecated driver modules during doctest collection Dynamically detect deprecated driver alias modules (those containing 'module is deprecated') and exclude them from doctest collection. This eliminates 14 QCoDeSDeprecationWarning warnings that were triggered just by importing these modules during pytest --doctest-modules. --- src/qcodes/instrument_drivers/conftest.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/qcodes/instrument_drivers/conftest.py b/src/qcodes/instrument_drivers/conftest.py index 913d5113eea..7b030db2879 100644 --- a/src/qcodes/instrument_drivers/conftest.py +++ b/src/qcodes/instrument_drivers/conftest.py @@ -1,4 +1,5 @@ import os +from pathlib import Path # Drivers that require optional third-party libraries not generally available. # These must be excluded from doctest collection to avoid ImportErrors. @@ -7,3 +8,24 @@ os.path.join("Minicircuits", "*"), os.path.join("QuantumDesign", "DynaCoolPPMS", "private", "*"), ] + + +def _find_deprecated_driver_modules() -> list[str]: + """ + Scan for deprecated driver alias modules that emit deprecation warnings + on import. Importing these during doctest collection triggers noisy + warnings and serves no purpose since they contain no doctests. + """ + drivers_dir = Path(__file__).parent + deprecated: list[str] = [] + for path in sorted(drivers_dir.rglob("*.py")): + try: + text = path.read_text(encoding="utf-8") + except Exception: + continue + if "module is deprecated" in text: + deprecated.append(str(path.relative_to(drivers_dir))) + return deprecated + + +collect_ignore = _find_deprecated_driver_modules() From 9695189df1da639287eebbab7e0e3a796fb81974 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sun, 26 Apr 2026 10:47:31 +0200 Subject: [PATCH 5/8] Enable warnings as error --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c29f9a91d6b..cb2ac9cc499 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -219,7 +219,7 @@ markers = "serial" # triggered by third party packages # and error on all other warnings filterwarnings = [ - # 'error', + 'error', 'ignore:open_binary is deprecated:DeprecationWarning', # pyvisa-sim deprecated in 3.11 un-deprecated in 3.12. Drop filter once we drop support for 3.11 'ignore:unclosed database in:ResourceWarning', # internal should be fixed 'ignore:unclosed\ Date: Sun, 26 Apr 2026 10:57:17 +0200 Subject: [PATCH 6/8] Switch shellcheck hook to shellcheck-py for cross-platform support Replace jumanjihouse/pre-commit-hooks (requires Docker or system shellcheck) with shellcheck-py/shellcheck-py which provides a pip-installable shellcheck binary for macOS, Linux, and Windows. --- .pre-commit-config.yaml | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5d8648ab490..0060e0cde1a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,29 +3,29 @@ repos: # Ruff version. rev: v0.15.11 hooks: - - id: ruff-check - types_or: [python, pyi, jupyter, toml] - args: [ --fix, --exit-non-zero-on-fix ] - - id: ruff-format + - id: ruff-check + types_or: [python, pyi, jupyter, toml] + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-ast - - id: check-json - exclude: asv.conf.json - - id: check-toml - - id: check-yaml - - id: check-case-conflict - - id: debug-statements - - id: mixed-line-ending - args: ['--fix=no'] + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-ast + - id: check-json + exclude: asv.conf.json + - id: check-toml + - id: check-yaml + - id: check-case-conflict + - id: debug-statements + - id: mixed-line-ending + args: ["--fix=no"] - repo: https://github.com/gitleaks/gitleaks rev: v8.30.1 hooks: - - id: gitleaks - - repo: https://github.com/jumanjihouse/pre-commit-hooks - rev: 3.0.0 + - id: gitleaks + - repo: https://github.com/shellcheck-py/shellcheck-py + rev: v0.11.0.1 hooks: - - id: shellcheck + - id: shellcheck From 539d959205a8b6c78a78e7bcea7c0168a4458803 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sun, 26 Apr 2026 11:07:49 +0200 Subject: [PATCH 7/8] Collect doctests from src --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cb2ac9cc499..be7cc0a2b2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -209,7 +209,7 @@ reportUnnecessaryContains = "none" [tool.pytest.ini_options] minversion = "7.2" -# testpaths = "tests" +testpaths = ["tests","src"] # we include src to collect doctests addopts = "-n auto --dist=loadfile --cov-config=pyproject.toml" asyncio_default_fixture_loop_scope = "function" markers = "serial" From 9b4b135281dbb40ba5caad28d04bb808ef190dc2 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Sun, 26 Apr 2026 14:10:48 +0200 Subject: [PATCH 8/8] Make one more doctest run --- src/qcodes/dataset/guid_helpers.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/qcodes/dataset/guid_helpers.py b/src/qcodes/dataset/guid_helpers.py index 3a5cfc5538b..04b2fdaaf24 100644 --- a/src/qcodes/dataset/guid_helpers.py +++ b/src/qcodes/dataset/guid_helpers.py @@ -80,12 +80,10 @@ def guids_from_list_str(s: str) -> tuple[str, ...] | None: For an empty list/tuple/set or empty string an empty tuple is returned. Examples: - >>> guids_from_str( # doctest: +SKIP - "['07fd7195-c51e-44d6-a085-fa8274cf00d6', \ - '070d7195-c51e-44d6-a085-fa8274cf00d6']") - will return - ('07fd7195-c51e-44d6-a085-fa8274cf00d6', - '070d7195-c51e-44d6-a085-fa8274cf00d6') + >>> guids_from_list_str( + ... "['07fd7195-c51e-44d6-a085-fa8274cf00d6','070d7195-c51e-44d6-a085-fa8274cf00d6']" + ... ) + ('07fd7195-c51e-44d6-a085-fa8274cf00d6', '070d7195-c51e-44d6-a085-fa8274cf00d6') """ if s == "":