diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index c49f9286e625..e2337ce2fb33 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -103,6 +103,10 @@ jobs: run: mypy -p qcodes if: ${{ !matrix.min-version && always() }} id: mypy + - name: Run ty + run: ty check --output-format github src docs + if: ${{ !matrix.min-version && always() }} + id: ty - name: Set pytest basetemp if: ${{ runner.os == 'Windows' }} run: echo "PYTEST_BASETEMP=--basetemp=D:\\tmp" >> $GITHUB_ENV diff --git a/docs/changes/newsfragments/8360.underthehood b/docs/changes/newsfragments/8360.underthehood new file mode 100644 index 000000000000..b7ee1dd573a6 --- /dev/null +++ b/docs/changes/newsfragments/8360.underthehood @@ -0,0 +1,9 @@ +:class:`.Parameter` no longer replaces its own ``get_raw``/``set_raw`` methods +with the implementation generated from ``get_cmd``/``set_cmd``. The generated +implementation is stored on the parameter instead, and ``get_raw``/``set_raw`` +are now regular methods that dispatch to it. Assigning over the methods made +static type checkers infer ``get_raw``/``set_raw`` to be instance attributes of +:class:`.Parameter`, which in turn made every subclass implementing them as +regular methods an invalid override. There is no change in behaviour; note only +that ``parameter.get_raw`` is now always a bound method rather than, depending +on the arguments, a ``Command`` instance. diff --git a/docs/changes/newsfragments/8361.underthehood b/docs/changes/newsfragments/8361.underthehood new file mode 100644 index 000000000000..674cd1067ae2 --- /dev/null +++ b/docs/changes/newsfragments/8361.underthehood @@ -0,0 +1,9 @@ +The ``TParameter`` type variable used by :meth:`.InstrumentBase.add_parameter` +now defaults to ``Parameter[Any, Any]`` rather than to a bare ``Parameter``. +When ``add_parameter`` is called without an explicit ``parameter_class`` the +returned parameter is bound to the instrument it is added to, so the previous +default (which expands to ``Parameter[Any, InstrumentBase | None]``) wrongly +claimed that the instrument was ``InstrumentBase | None``. This made the result +unassignable to the ``Parameter[SomeType, Self]`` annotations that drivers use. +Code that relies on the inferred type of an unannotated +``instrument.add_parameter("name")`` will now see ``Parameter[Any, Any]``. diff --git a/docs/changes/newsfragments/8362.underthehood b/docs/changes/newsfragments/8362.underthehood new file mode 100644 index 000000000000..b1e9b3fd772c --- /dev/null +++ b/docs/changes/newsfragments/8362.underthehood @@ -0,0 +1,5 @@ +The legacy dataset importer now raises a clear :class:`ValueError` when a +setpoint array has no ``array_id``, instead of passing ``None`` on to +``add_result`` where a parameter name is expected. This was found by annotating +``DataArray`` in ``qcodes_loop``, which also removes the need for a ``pyright`` +suppression on the array shape. diff --git a/docs/changes/newsfragments/8364.underthehood b/docs/changes/newsfragments/8364.underthehood new file mode 100644 index 000000000000..168eaff90ec2 --- /dev/null +++ b/docs/changes/newsfragments/8364.underthehood @@ -0,0 +1,4 @@ +``CombinedParameter.parameter`` is now a small dataclass holding the ``name``, +``full_name``, ``label`` and ``unit`` of the combined parameter, replacing a +lambda that had those attributes attached to it. It remains callable, and +calling it returns ``None`` as before. diff --git a/docs/changes/newsfragments/8365.underthehood b/docs/changes/newsfragments/8365.underthehood new file mode 100644 index 000000000000..492f371ee712 --- /dev/null +++ b/docs/changes/newsfragments/8365.underthehood @@ -0,0 +1,6 @@ +Subclasses of ``ParameterBase`` that forward ``**kwargs`` on to their super +class now carry a ``ty: ignore[invalid-argument-type]``. When a generic TypedDict +declares a PEP 696 default for a type parameter, ty computes the upper bound of +the synthesized ``Self`` as the default specialization, so every other +specialization is rejected by the members that bind ``Self``, including expanding +with ``**``. The reason is documented on ``ParameterBaseKWArgs``. diff --git a/docs/changes/newsfragments/8366.underthehood b/docs/changes/newsfragments/8366.underthehood new file mode 100644 index 000000000000..21ba0d20655c --- /dev/null +++ b/docs/changes/newsfragments/8366.underthehood @@ -0,0 +1,5 @@ +The Infiniium driver now uses ``cast`` where it narrows ``root_instrument`` and +``instrument`` to the concrete driver classes, and where ``pyvisa`` types the +return of ``read_binary_values``/``query_binary_values`` as a ``Sequence[float]`` +regardless of the requested ``container``. This replaces five type checker +suppressions and has no effect at runtime. diff --git a/docs/changes/newsfragments/8367.underthehood b/docs/changes/newsfragments/8367.underthehood new file mode 100644 index 000000000000..81eef08c23d8 --- /dev/null +++ b/docs/changes/newsfragments/8367.underthehood @@ -0,0 +1,5 @@ +``DataSet._finalize_res_dict_standalones`` now appends to its result list +directly instead of building intermediate lists. The intermediate lists took +their element type from the branch that built them rather than from the +declaration, and ``dict`` is invariant in its value type, so the result was not +assignable back. There is no change in behaviour. diff --git a/docs/changes/newsfragments/8368.underthehood b/docs/changes/newsfragments/8368.underthehood new file mode 100644 index 000000000000..7cab22c1c784 --- /dev/null +++ b/docs/changes/newsfragments/8368.underthehood @@ -0,0 +1,4 @@ +The Alazar DLL wrapper no longer assumes that the callable handed to a ctypes +``errcheck`` has a ``__name__``, which a plain ``Callable`` does not guarantee. +The error message falls back to the repr of the callable instead. This only +affects the text of an error that should not occur in practice. diff --git a/docs/changes/newsfragments/8370.underthehood b/docs/changes/newsfragments/8370.underthehood new file mode 100644 index 000000000000..3317ec3768c5 --- /dev/null +++ b/docs/changes/newsfragments/8370.underthehood @@ -0,0 +1,5 @@ +``numpy_ints`` and ``numpy_floats`` in ``qcodes.utils.types`` are now annotated +as tuples of ``type[np.integer]`` and ``type[np.floating]`` rather than of bare +``type``. As a consequence ``_adapt_float``, which is registered as a sqlite +adapter for the numpy float types as well as for ``float``, now declares that it +accepts ``np.floating`` too. Its behaviour is unchanged. diff --git a/docs/changes/newsfragments/8371.underthehood b/docs/changes/newsfragments/8371.underthehood new file mode 100644 index 000000000000..10becb126c81 --- /dev/null +++ b/docs/changes/newsfragments/8371.underthehood @@ -0,0 +1,5 @@ +The ``__getattr__`` that provides backwards-compatible access to the old flat +parameter names on the Tektronix AWG5014 now names its parameter ``key``, +matching ``DelegateAttributes.__getattr__`` which it overrides and delegates to. +Python only ever calls ``__getattr__`` positionally, so this has no effect at +runtime. diff --git a/docs/changes/newsfragments/8373.underthehood b/docs/changes/newsfragments/8373.underthehood new file mode 100644 index 000000000000..e519a10b0793 --- /dev/null +++ b/docs/changes/newsfragments/8373.underthehood @@ -0,0 +1,5 @@ +``json_template_linear`` and ``json_template_heatmap`` in +``qcodes.dataset.json_exporter`` are now annotated as ``dict[str, Any]``. They +are templates for a JSON document, so their values are deliberately +heterogeneous, and without the annotation the inferred value type made indexing +into them an error for callers filling the template in. diff --git a/docs/changes/newsfragments/8374.improved b/docs/changes/newsfragments/8374.improved new file mode 100644 index 000000000000..e0c385c8e161 --- /dev/null +++ b/docs/changes/newsfragments/8374.improved @@ -0,0 +1,6 @@ +The ``callback`` argument of :meth:`.DataSet.subscribe` is now typed as +``Callable[..., None]``. The previous annotation described a callback taking +exactly three arguments, which contradicted ``callback_kwargs``: those are bound +onto the callback with ``functools.partial``, so a callback using them takes +further arguments. ``_Subscriber``, which ``subscribe`` forwards to, already +typed it this way. diff --git a/docs/changes/newsfragments/8375.improved b/docs/changes/newsfragments/8375.improved new file mode 100644 index 000000000000..b90716a9f224 --- /dev/null +++ b/docs/changes/newsfragments/8375.improved @@ -0,0 +1,7 @@ +``Keysight34980A.module`` is now a ``dict`` of +``Keysight34980ASwitchMatrixSubModule`` rather than one built with +``dict.fromkeys``, whose values were typed as possibly ``None``. ``scan_slots`` +puts an entry in for every slot, either the driver for the installed module or a +generic submodule, so the values were never ``None`` once the instrument was +constructed. Code using ``instrument.module[slot]`` no longer has to account for +a ``None`` that cannot occur. diff --git a/docs/changes/newsfragments/8376.improved b/docs/changes/newsfragments/8376.improved new file mode 100644 index 000000000000..0437c9e88e28 --- /dev/null +++ b/docs/changes/newsfragments/8376.improved @@ -0,0 +1,6 @@ +The return type of :func:`.parse_awg_file` has been corrected. The waveform and +marker entries were declared as lists of dicts, but the parser returns the arrays +from inside those dicts, and the loop counts and sequencing values were declared +as possibly ``str`` when they are always ``int``. The type now matches the call +signature of :meth:`.TektronixAWG5014.make_send_and_load_awg_file`, which the +docstring already promised and which is how the function is meant to be used. diff --git a/docs/changes/newsfragments/8377.underthehood b/docs/changes/newsfragments/8377.underthehood new file mode 100644 index 000000000000..73cf4961e3be --- /dev/null +++ b/docs/changes/newsfragments/8377.underthehood @@ -0,0 +1,3 @@ +``ty`` now also type checks the example notebooks in ``docs``. Unlike mypy and +pyright it understands Jupyter notebooks, so this is coverage that the other two +checkers do not provide. diff --git a/docs/changes/newsfragments/8381.improved b/docs/changes/newsfragments/8381.improved new file mode 100644 index 000000000000..b124c324651d --- /dev/null +++ b/docs/changes/newsfragments/8381.improved @@ -0,0 +1,6 @@ +The Keysight B1500 example notebook called +``b1500.run_iv_staircase_sweep.measurement_status()`` in the phase compensation +section. ``IVSweepMeasurement`` has no such method, so the cell raised +``AttributeError``. The surrounding text asks for all channel outputs to be +enabled before performing phase compensation, so the cell now calls +``b1500.enable_channels()``. diff --git a/docs/changes/newsfragments/8382.improved b/docs/changes/newsfragments/8382.improved new file mode 100644 index 000000000000..3727910a2a5c --- /dev/null +++ b/docs/changes/newsfragments/8382.improved @@ -0,0 +1,7 @@ +``KeysightE4980AMeasurementPair`` now declares a ``__getattr__`` for type +checkers. The two measured values are exposed as attributes named after the +``names`` of the measurement function, for example ``capacitance`` for ``CPD`` +and ``inductance`` for ``LPD``, so which attributes exist is only known at +runtime. The declaration lets this documented usage be written in typed code. It +is not defined at runtime, so accessing an attribute that the current +measurement function does not provide still raises the usual ``AttributeError``. diff --git a/docs/changes/newsfragments/8384.improved b/docs/changes/newsfragments/8384.improved new file mode 100644 index 000000000000..786b261bb836 --- /dev/null +++ b/docs/changes/newsfragments/8384.improved @@ -0,0 +1,4 @@ +The Stanford SR86x buffered readout example notebook now packs the waveforms for +:meth:`.TektronixAWG70000Base.makeSEQXFile` in lists rather than wrapping them in +further numpy arrays, which is the shape the method documents. The two forms +behave the same at runtime. diff --git a/docs/changes/newsfragments/8385.improved b/docs/changes/newsfragments/8385.improved new file mode 100644 index 000000000000..ffc16e01e346 --- /dev/null +++ b/docs/changes/newsfragments/8385.improved @@ -0,0 +1,6 @@ +``connect_paths``, ``disconnect_paths`` and ``to_channel_list`` on the Keysight +B220X switch matrix drivers now accept any iterable of paths rather than only a +``Sequence``. They iterate the paths once and do not index them, so passing a +set, as the example notebook does, is fine. Note that the order in which the +paths appear in the channel list then follows the iteration order of the +argument. diff --git a/docs/changes/newsfragments/8386.improved b/docs/changes/newsfragments/8386.improved new file mode 100644 index 000000000000..60fc859b5b8c --- /dev/null +++ b/docs/changes/newsfragments/8386.improved @@ -0,0 +1,6 @@ +The path arguments of ``connect_paths``, ``disconnect_paths``, ``are_closed``, +``are_open`` and ``to_channel_list`` on the Keysight 34980A switch matrix +submodules are now typed as a ``Collection`` rather than a ``list``, so a set or +a tuple of paths is accepted as well. A ``Collection`` rather than an +``Iterable`` because these methods walk the paths twice, once to validate them +and once to build the channel list, which a one shot iterator would not survive. diff --git a/docs/changes/newsfragments/8387.improved b/docs/changes/newsfragments/8387.improved new file mode 100644 index 000000000000..7d3920c0da1d --- /dev/null +++ b/docs/changes/newsfragments/8387.improved @@ -0,0 +1,5 @@ +The live temperature plot helper in the two Lakeshore example notebooks keeps +the appended y data in a local variable instead of reading it back with +``Line2D.get_ydata``. The return of ``get_ydata`` is typed as ``ArrayLike``, +which is not necessarily sized, so taking its length was a type error. This also +avoids reading the data back from the line on every iteration. diff --git a/docs/changes/newsfragments/8389.improved b/docs/changes/newsfragments/8389.improved new file mode 100644 index 000000000000..b2f6f471d3d8 --- /dev/null +++ b/docs/changes/newsfragments/8389.improved @@ -0,0 +1,3 @@ +The offline plotting tutorial now checks the optional values it gets back from +:func:`.plot_dataset` before using them, and asks for the root figure when +saving. ``Axes.figure`` may be a ``SubFigure``, which has no ``savefig``. diff --git a/docs/changes/newsfragments/8390.improved b/docs/changes/newsfragments/8390.improved new file mode 100644 index 000000000000..080e53462aae --- /dev/null +++ b/docs/changes/newsfragments/8390.improved @@ -0,0 +1,5 @@ +``snapshot_raw`` is now part of :class:`.DataSetProtocol` and is available on +:class:`.DataSetInMem` as well as on :class:`.DataSet`. It is documented as the +way to get the snapshot of a run as a JSON string, and is used as such in the +example notebooks, but it was only declared on one of the two dataset classes, +so reading it from a dataset returned by a measurement did not type check. diff --git a/docs/changes/newsfragments/8391.improved b/docs/changes/newsfragments/8391.improved new file mode 100644 index 000000000000..04a44aad1a7c --- /dev/null +++ b/docs/changes/newsfragments/8391.improved @@ -0,0 +1,3 @@ +The snapshot example notebook now checks that the snapshots it reads back from +the datasets are present before using them. A run only has a snapshot if one was +recorded, so both ``snapshot`` and ``snapshot_raw`` are optional. diff --git a/docs/changes/newsfragments/8392.improved b/docs/changes/newsfragments/8392.improved new file mode 100644 index 000000000000..3a3cbdae81b2 --- /dev/null +++ b/docs/changes/newsfragments/8392.improved @@ -0,0 +1,5 @@ +The type alias ``ValuesType``, used by ``DataSaver.add_result`` and related +methods, now also accepts a sequence of numpy arrays. This is what a +``MultiParameter`` measuring more than one array returns, so passing such a +result to ``DataSaver.add_result`` no longer produces a type checking error. +The behavior at runtime is unchanged since this was already supported. diff --git a/docs/changes/newsfragments/8393.underthehood b/docs/changes/newsfragments/8393.underthehood new file mode 100644 index 000000000000..782b81a23f2a --- /dev/null +++ b/docs/changes/newsfragments/8393.underthehood @@ -0,0 +1,4 @@ +The type checker ``ty`` is now a test dependency and is run as part of the +CI job that also runs ``mypy`` and ``pyright``. Unlike the other type +checkers ``ty`` understands Jupyter notebooks, so this type checks the +example notebooks in ``docs`` in addition to the source code in ``src``. diff --git a/docs/conf.py b/docs/conf.py index 9b78ed91a829..b4c9a301f9bc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -33,7 +33,7 @@ # this should happen as early as possible import qcodes.instrument.instrument_meta -qcodes.instrument.instrument_meta.InstrumentMeta = ABCMeta +qcodes.instrument.instrument_meta.InstrumentMeta = ABCMeta # ty: ignore[invalid-assignment] # we need to reload any module that has been imported and # makes use of this metaclass. The modules below are all imported # by importing qcodes.instrument so we need to reload them diff --git a/docs/examples/DataSet/Accessing-data-in-DataSet.ipynb b/docs/examples/DataSet/Accessing-data-in-DataSet.ipynb index e3909e71bdc0..15b33bcd9d94 100644 --- a/docs/examples/DataSet/Accessing-data-in-DataSet.ipynb +++ b/docs/examples/DataSet/Accessing-data-in-DataSet.ipynb @@ -710,26 +710,6 @@ "dataset.get_parameters()" ] }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'x,t,y,y2,q'" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "dataset.parameters" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -1047,7 +1027,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv", + "display_name": "qcodes", "language": "python", "name": "python3" }, @@ -1061,7 +1041,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.12" + "version": "3.14.7" }, "toc": { "base_numbering": 1, diff --git a/docs/examples/DataSet/DataSet-class-walkthrough.ipynb b/docs/examples/DataSet/DataSet-class-walkthrough.ipynb index 44e9fe09ef02..9c360867a7ef 100644 --- a/docs/examples/DataSet/DataSet-class-walkthrough.ipynb +++ b/docs/examples/DataSet/DataSet-class-walkthrough.ipynb @@ -235,7 +235,7 @@ }, { "data": { - "image/png": "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\n", + "image/png": "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", "text/plain": [ "
" ] @@ -247,7 +247,7 @@ }, { "data": { - "image/png": "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\n", + "image/png": "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", "text/plain": [ "
" ] @@ -866,26 +866,6 @@ "dataset.completed" ] }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 26, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "dataset.started" - ] - }, { "cell_type": "code", "execution_count": 27, @@ -1218,33 +1198,6 @@ "dataset.get_parameters()" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Lastly, `DataSet` has `parameters` that returns a string with comma-separated names of all the dataset parameters (will likely be deprecated soon):" - ] - }, - { - "cell_type": "code", - "execution_count": 39, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'x,t,y,y2'" - ] - }, - "execution_count": 39, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "dataset.parameters" - ] - }, { "cell_type": "markdown", "metadata": {}, diff --git a/docs/examples/DataSet/Exporting-data-to-other-file-formats.ipynb b/docs/examples/DataSet/Exporting-data-to-other-file-formats.ipynb index 8ea718a348e0..f600aca5494c 100644 --- a/docs/examples/DataSet/Exporting-data-to-other-file-formats.ipynb +++ b/docs/examples/DataSet/Exporting-data-to-other-file-formats.ipynb @@ -1116,7 +1116,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -1131,6 +1131,12 @@ } ], "source": [ + "from qcodes.dataset.data_set_in_memory import DataSetInMem\n", + "\n", + "# the set_netcdf_location only exists for DataSetInMem so we validate that we are indeed using that.\n", + "if not isinstance(reloaded_ds, DataSetInMem):\n", + " raise ValueError(\"reloaded_ds must be an instance of DataSetInMem\")\n", + "\n", "reloaded_ds.set_netcdf_location(new_file_path)\n", "reloaded_ds.export_info" ] diff --git a/docs/examples/DataSet/Offline Plotting Tutorial.ipynb b/docs/examples/DataSet/Offline Plotting Tutorial.ipynb index dfddf0e41b06..164fafe2ed99 100644 --- a/docs/examples/DataSet/Offline Plotting Tutorial.ipynb +++ b/docs/examples/DataSet/Offline Plotting Tutorial.ipynb @@ -477,6 +477,8 @@ "outputs": [], "source": [ "colorbar = colorbars[0]\n", + "# 2D plots have a colorbar, 1D plots do not, so the entries are optional\n", + "assert colorbar is not None\n", "colorbar.set_label(\"Correct science label\")" ] }, @@ -939,9 +941,11 @@ "source": [ "%%time\n", "axeslist, _ = plot_dataset(dataset)\n", - "axeslist[0].figure.savefig(\n", - " Path.cwd().parent / \"example_output\" / f\"test_plot_dataset_{dataid}.pdf\"\n", - ")" + "# Axes.figure may be a SubFigure, which cannot be saved, so ask for the\n", + "# root figure\n", + "figure = axeslist[0].get_figure(root=True)\n", + "assert figure is not None\n", + "figure.savefig(Path.cwd().parent / \"example_output\" / f\"test_plot_dataset_{dataid}.pdf\")" ] }, { @@ -971,9 +975,11 @@ "source": [ "%%time\n", "axeslist, _ = plot_dataset(dataset, rasterized=False)\n", - "axeslist[0].figure.savefig(\n", - " Path.cwd().parent / \"example_output\" / f\"test_plot_dataset_{dataid}.pdf\"\n", - ")" + "# Axes.figure may be a SubFigure, which cannot be saved, so ask for the\n", + "# root figure\n", + "figure = axeslist[0].get_figure(root=True)\n", + "assert figure is not None\n", + "figure.savefig(Path.cwd().parent / \"example_output\" / f\"test_plot_dataset_{dataid}.pdf\")" ] } ], diff --git a/docs/examples/DataSet/Threaded data acquisition.ipynb b/docs/examples/DataSet/Threaded data acquisition.ipynb index ae5e2c38cbde..0f41089fcd08 100644 --- a/docs/examples/DataSet/Threaded data acquisition.ipynb +++ b/docs/examples/DataSet/Threaded data acquisition.ipynb @@ -92,7 +92,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "2add302b", "metadata": { "ExecuteTime": { @@ -115,6 +115,17 @@ " time.sleep(0.1)\n", " return val\n", "\n", + " @property\n", + " def root_instrument(self) -> \"DummyInstrumentWithMeasurement\":\n", + " if self.instrument is None or not isinstance(\n", + " self.instrument.root_instrument, DummyInstrumentWithMeasurement\n", + " ):\n", + " raise ValueError(\n", + " \"SleepyDmmExponentialParameter must be bound to a DummyInstrumentWithMeasurement\"\n", + " )\n", + " instr = self.instrument.root_instrument\n", + " return instr\n", + "\n", " @staticmethod\n", " def _exponential_decay(a: float, b: float):\n", " x = 0\n", diff --git a/docs/examples/DataSet/Using_doNd_functions_in_comparison_to_Measurement_context_manager_for_performing_measurements.ipynb b/docs/examples/DataSet/Using_doNd_functions_in_comparison_to_Measurement_context_manager_for_performing_measurements.ipynb index fd2977868fcd..20d10cb774bd 100644 --- a/docs/examples/DataSet/Using_doNd_functions_in_comparison_to_Measurement_context_manager_for_performing_measurements.ipynb +++ b/docs/examples/DataSet/Using_doNd_functions_in_comparison_to_Measurement_context_manager_for_performing_measurements.ipynb @@ -1404,7 +1404,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -1451,7 +1451,15 @@ } ], "source": [ - "result = dond(sweep_1, sweep_2, [dmm.v1], [dmm.v2], do_plot=True, show_progress=True)" + "result = dond(\n", + " sweep_1,\n", + " sweep_2,\n", + " [dmm.v1],\n", + " [dmm.v2],\n", + " do_plot=True,\n", + " show_progress=True,\n", + " squeeze=False,\n", + ")" ] }, { @@ -1979,7 +1987,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv", + "display_name": "qcodes", "language": "python", "name": "python3" }, @@ -1993,7 +2001,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.7" + "version": "3.14.7" }, "toc": { "base_numbering": 1, diff --git a/docs/examples/DataSet/Working with snapshots.ipynb b/docs/examples/DataSet/Working with snapshots.ipynb index 49d42c62d4e6..801126e0a1b4 100644 --- a/docs/examples/DataSet/Working with snapshots.ipynb +++ b/docs/examples/DataSet/Working with snapshots.ipynb @@ -593,7 +593,9 @@ "metadata": {}, "outputs": [], "source": [ - "snapshot_of_run = dataset.snapshot" + "snapshot_of_run = dataset.snapshot\n", + "# a run only has a snapshot if one was recorded, this one has\n", + "assert snapshot_of_run is not None" ] }, { @@ -602,7 +604,8 @@ "metadata": {}, "outputs": [], "source": [ - "snapshot_of_run_in_json_format = dataset.snapshot_raw" + "snapshot_of_run_in_json_format = dataset.snapshot_raw\n", + "assert snapshot_of_run_in_json_format is not None" ] }, { @@ -881,7 +884,10 @@ "metadata": {}, "outputs": [], "source": [ - "diff_param_values(dataset.snapshot, bad_dataset.snapshot).changed" + "snapshot_of_bad_run = bad_dataset.snapshot\n", + "assert snapshot_of_bad_run is not None\n", + "\n", + "diff_param_values(snapshot_of_run, snapshot_of_bad_run).changed" ] }, { diff --git a/docs/examples/Parameters/Parameter-With-Setpoints-defined-on-a-different-instrument.ipynb b/docs/examples/Parameters/Parameter-With-Setpoints-defined-on-a-different-instrument.ipynb index c86165c1306d..8225df8c836b 100644 --- a/docs/examples/Parameters/Parameter-With-Setpoints-defined-on-a-different-instrument.ipynb +++ b/docs/examples/Parameters/Parameter-With-Setpoints-defined-on-a-different-instrument.ipynb @@ -81,7 +81,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -108,6 +108,18 @@ " npoints = self.root_instrument.sweep_n_points.get_latest()\n", " return np.random.default_rng().random(npoints)\n", "\n", + " @property\n", + " def root_instrument(self) -> \"DummyBufferedDMM\":\n", + " # we override this to enforce that the root instrument is a DummyBufferedDMM\n", + " # to ensure that we can get the value of the n_points parameter\n", + " if self._instrument is None:\n", + " raise ValueError(\"Instrument is not set for this parameter\")\n", + " root_instrument = self._instrument.root_instrument\n", + " if not isinstance(root_instrument, DummyBufferedDMM):\n", + " raise TypeError(\"root_instrument must be an instance of DummyBufferedDMM\")\n", + "\n", + " return root_instrument\n", + "\n", "\n", "class DummyBufferedDMM(Instrument):\n", " def __init__(self, name, **kwargs):\n", @@ -655,7 +667,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "qcodes", "language": "python", "name": "python3" }, @@ -669,7 +681,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.9" + "version": "3.14.7" }, "toc": { "base_numbering": 1, diff --git a/docs/examples/Parameters/Parameter_defined_InterDependencies.ipynb b/docs/examples/Parameters/Parameter_defined_InterDependencies.ipynb index 3cd2ca6c7bc8..cb672c7fb204 100644 --- a/docs/examples/Parameters/Parameter_defined_InterDependencies.ipynb +++ b/docs/examples/Parameters/Parameter_defined_InterDependencies.ipynb @@ -224,7 +224,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "73823b84", "metadata": {}, "outputs": [ @@ -252,14 +252,14 @@ "source": [ "from qcodes.dataset import LinSweep, dond\n", "\n", - "ds, _, _ = dond(LinSweep(control, 0, 1, 11), meas_param)\n", - "ds.get_parameter_data()" + "ds, _, _ = dond(LinSweep(control, 0, 1, 11), meas_param, squeeze=False)\n", + "ds[0].get_parameter_data()" ] } ], "metadata": { "kernelspec": { - "display_name": "py311", + "display_name": "qcodes", "language": "python", "name": "python3" }, @@ -273,7 +273,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.8" + "version": "3.14.7" } }, "nbformat": 4, diff --git a/docs/examples/Parameters/Parameters.ipynb b/docs/examples/Parameters/Parameters.ipynb index f5d488a6c9f2..aff6c0527980 100644 --- a/docs/examples/Parameters/Parameters.ipynb +++ b/docs/examples/Parameters/Parameters.ipynb @@ -82,7 +82,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -103,8 +103,8 @@ " self._count += 1\n", " return self._count\n", "\n", - " def set_raw(self, val):\n", - " self._count = val\n", + " def set_raw(self, value):\n", + " self._count = value\n", " return self._count" ] }, diff --git a/docs/examples/Parameters/Simple-Example-of-ParameterWithSetpoints.ipynb b/docs/examples/Parameters/Simple-Example-of-ParameterWithSetpoints.ipynb index 0358ae1f4c93..1db1eb9e5b09 100644 --- a/docs/examples/Parameters/Simple-Example-of-ParameterWithSetpoints.ipynb +++ b/docs/examples/Parameters/Simple-Example-of-ParameterWithSetpoints.ipynb @@ -83,7 +83,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -110,6 +110,20 @@ " npoints = self.root_instrument.n_points.get_latest()\n", " return np.random.default_rng().random(npoints)\n", "\n", + " @property\n", + " def root_instrument(self) -> \"DummySpectrumAnalyzer\":\n", + " # we override this to enforce that the root instrument is a DummySpectrumAnalyzer\n", + " # to ensure that we can get the value of the n_points parameter\n", + " if self._instrument is None:\n", + " raise ValueError(\"Instrument is not set for this parameter\")\n", + " root_instrument = self._instrument.root_instrument\n", + " if not isinstance(root_instrument, DummySpectrumAnalyzer):\n", + " raise TypeError(\n", + " \"root_instrument must be an instance of DummySpectrumAnalyzer\"\n", + " )\n", + "\n", + " return root_instrument\n", + "\n", "\n", "class DummySpectrumAnalyzer(Instrument):\n", " def __init__(self, name, **kwargs):\n", diff --git a/docs/examples/basic_examples/Configuring_QCoDeS.ipynb b/docs/examples/basic_examples/Configuring_QCoDeS.ipynb index 0a02fb04a7c8..b6f2229db17c 100644 --- a/docs/examples/basic_examples/Configuring_QCoDeS.ipynb +++ b/docs/examples/basic_examples/Configuring_QCoDeS.ipynb @@ -553,10 +553,12 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ + "if qc.config.current_config is None:\n", + " raise RuntimeError(\"No current QCoDeS configuration is set.\")\n", "qc.config.current_config.core.loglevel = \"INFO\"" ] }, @@ -585,7 +587,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -603,6 +605,8 @@ } ], "source": [ + "if qc.config.current_config is None:\n", + " raise RuntimeError(\"No current QCoDeS configuration is set.\")\n", "qc.config.current_config.core.loglevel = \"YOLO\"\n", "qc.config.validate()\n", "# NOTE that you how have a broken config!" diff --git a/docs/examples/driver_examples/Qcodes example with AMI430.ipynb b/docs/examples/driver_examples/Qcodes example with AMI430.ipynb index 3705dc284386..750ca3d212d8 100644 --- a/docs/examples/driver_examples/Qcodes example with AMI430.ipynb +++ b/docs/examples/driver_examples/Qcodes example with AMI430.ipynb @@ -35,7 +35,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "tags": [] }, @@ -860,8 +860,10 @@ "time_p.reset_clock()\n", "\n", "while elapsed_time < 2:\n", - " ds, _, _ = dond(sweep_1, sweep_2, dmm.v1, additional_setpoints=(time_p,))\n", - " timed_datasets.append(ds)\n", + " dss, _, _ = dond(\n", + " sweep_1, sweep_2, dmm.v1, additional_setpoints=(time_p,), squeeze=False\n", + " )\n", + " timed_datasets.append(dss[0])\n", " time.sleep(0.5)\n", " elapsed_time = time_p.get()" ] diff --git a/docs/examples/driver_examples/Qcodes example with Keysight 34980A Switch Mainframe and Modules.ipynb b/docs/examples/driver_examples/Qcodes example with Keysight 34980A Switch Mainframe and Modules.ipynb index 3726074aa3ee..b1a64ba32a6a 100644 --- a/docs/examples/driver_examples/Qcodes example with Keysight 34980A Switch Mainframe and Modules.ipynb +++ b/docs/examples/driver_examples/Qcodes example with Keysight 34980A Switch Mainframe and Modules.ipynb @@ -428,9 +428,10 @@ "metadata": {}, "outputs": [], "source": [ - "switch_matrix.module[\n", - " 2\n", - "]._is_locked = True # DO NOT perform this action in real situation" + "# DO NOT perform this action in a real situation. ``_is_locked`` is defined\n", + "# on the 34934A driver rather than on the shared submodule base class that\n", + "# ``module`` is typed as, hence the suppression.\n", + "switch_matrix.module[2]._is_locked = True # ty: ignore[unresolved-attribute]" ] }, { diff --git a/docs/examples/driver_examples/Qcodes example with Keysight B1500 Parameter Analyzer.ipynb b/docs/examples/driver_examples/Qcodes example with Keysight B1500 Parameter Analyzer.ipynb index fffbf3d12229..e5ee5a34304d 100644 --- a/docs/examples/driver_examples/Qcodes example with Keysight B1500 Parameter Analyzer.ipynb +++ b/docs/examples/driver_examples/Qcodes example with Keysight B1500 Parameter Analyzer.ipynb @@ -288,7 +288,7 @@ "metadata": {}, "outputs": [], "source": [ - "b1500.by_kind[\"SMU\"]" + "b1500.by_kind[constants.ModuleKind.SMU]" ] }, { @@ -331,8 +331,9 @@ "# Selecting a module by channel number using the Enum\n", "m1 = b1500.by_channel[constants.ChNr.SLOT_01_CH1]\n", "\n", - "# Without enum\n", - "m2 = b1500.by_channel[1]\n", + "# Without enum. ChNr is an IntEnum, so a plain int is the same key at\n", + "# runtime, but the dict is typed as taking ChNr.\n", + "m2 = b1500.by_channel[1] # ty: ignore[invalid-argument-type]\n", "\n", "# And we assert that we selected the same module:\n", "assert m1 is m2" @@ -1118,7 +1119,8 @@ "metadata": {}, "outputs": [], "source": [ - "b1500.run_iv_staircase_sweep.measurement_status()" + "# enable all channel outputs\n", + "b1500.enable_channels()" ] }, { diff --git a/docs/examples/driver_examples/Qcodes example with Lakeshore 325.ipynb b/docs/examples/driver_examples/Qcodes example with Lakeshore 325.ipynb index 44a36f8baed8..042ed8d79c90 100644 --- a/docs/examples/driver_examples/Qcodes example with Lakeshore 325.ipynb +++ b/docs/examples/driver_examples/Qcodes example with Lakeshore 325.ipynb @@ -517,8 +517,9 @@ " text.value = f\"T = {channel_to_read.temperature()}\"\n", "\n", " # Add new point to the data that is being plotted\n", - " line.set_ydata(numpy.append(line.get_ydata(), channel_to_read.temperature()))\n", - " line.set_xdata(numpy.arange(0, len(line.get_ydata()), 1) * read_period)\n", + " ydata = numpy.append(line.get_ydata(), channel_to_read.temperature())\n", + " line.set_ydata(ydata)\n", + " line.set_xdata(numpy.arange(0, len(ydata), 1) * read_period)\n", "\n", " ax.relim() # Recalculate limits\n", " ax.autoscale_view(True, True, True) # Autoscale\n", diff --git a/docs/examples/driver_examples/Qcodes example with Lakeshore 336 or 372 - Bluefors T control.ipynb b/docs/examples/driver_examples/Qcodes example with Lakeshore 336 or 372 - Bluefors T control.ipynb index 8837ebb9e9fe..b8c566172e2a 100644 --- a/docs/examples/driver_examples/Qcodes example with Lakeshore 336 or 372 - Bluefors T control.ipynb +++ b/docs/examples/driver_examples/Qcodes example with Lakeshore 336 or 372 - Bluefors T control.ipynb @@ -508,8 +508,9 @@ " text.value = f\"T = {channel_to_read.temperature()}\"\n", "\n", " # Add new point to the data that is being plotted\n", - " line.set_ydata(numpy.append(line.get_ydata(), channel_to_read.temperature()))\n", - " line.set_xdata(numpy.arange(0, len(line.get_ydata()), 1) * read_period)\n", + " ydata = numpy.append(line.get_ydata(), channel_to_read.temperature())\n", + " line.set_ydata(ydata)\n", + " line.set_xdata(numpy.arange(0, len(ydata), 1) * read_period)\n", "\n", " ax.relim() # Recalculate limits\n", " ax.autoscale_view(True, True, True) # Autoscale\n", diff --git a/docs/examples/driver_examples/Qcodes example with Stanford SR86x with buffered readout.ipynb b/docs/examples/driver_examples/Qcodes example with Stanford SR86x with buffered readout.ipynb index 4adf9855f8f2..1f8aae138e10 100644 --- a/docs/examples/driver_examples/Qcodes example with Stanford SR86x with buffered readout.ipynb +++ b/docs/examples/driver_examples/Qcodes example with Stanford SR86x with buffered readout.ipynb @@ -683,8 +683,10 @@ "# (3000 samples for 3000 S/s sample rate)\n", "waveform_ch1[1, :-1500] = 1 # falling from 1 to 0 (a.u.),\n", "# at 0.5s after the start of the waveform\n", - "elements = numpy.array([waveform_ch1]) # we only have one element in the sequence\n", - "waveforms = numpy.array([elements]) # we will use only 1 channel\n", + "# makeSEQXFile takes the waveform arrays packed in lists, per channel and\n", + "# then per element, rather than in a further numpy array\n", + "elements = [waveform_ch1] # we only have one element in the sequence\n", + "waveforms = [elements] # we will use only 1 channel\n", "\n", "# Create a sequence file from the \"waveform\" array\n", "seq_name = \"single_trigger_marker_1\"\n", @@ -929,8 +931,10 @@ " n_trigger_pulses,\n", ") # falling from 1 to 0 (a.u.) every 0.01s after the start of the waveform\n", "\n", - "elements = numpy.array([waveform_ch1]) # we only have one element in the sequence\n", - "waveforms = numpy.array([elements]) # we will use only 1 channel\n", + "# makeSEQXFile takes the waveform arrays packed in lists, per channel and\n", + "# then per element, rather than in a further numpy array\n", + "elements = [waveform_ch1] # we only have one element in the sequence\n", + "waveforms = [elements] # we will use only 1 channel\n", "\n", "# Create a sequence file from the \"waveform\" array\n", "seq_name = \"single_trigger_marker_1\"\n", diff --git a/docs/examples/driver_examples/Qcodes example with Tektronix AWG5014C.ipynb b/docs/examples/driver_examples/Qcodes example with Tektronix AWG5014C.ipynb index abe47966ccc7..494e9a4697a2 100644 --- a/docs/examples/driver_examples/Qcodes example with Tektronix AWG5014C.ipynb +++ b/docs/examples/driver_examples/Qcodes example with Tektronix AWG5014C.ipynb @@ -100,19 +100,23 @@ "metadata": {}, "outputs": [], "source": [ + "# ``instrument.parameters`` is a dict of ``ParameterBase``, and not every\n", + "# parameter type carries a ``label``: ``MultiParameter`` has ``labels`` instead.\n", + "# Fall back to an empty string so this works for any parameter.\n", + "\n", "# Top-level parameters\n", "for name in sorted(awg1.parameters):\n", - " print(name, \": \", awg1.parameters[name].label)\n", + " print(name, \": \", getattr(awg1.parameters[name], \"label\", \"\"))\n", "\n", "# Channel parameters (e.g. ch1)\n", "print(\"\\nChannel 1 parameters:\")\n", "for name in sorted(awg1.ch1.parameters):\n", - " print(f\" ch1.{name}: \", awg1.ch1.parameters[name].label)\n", + " print(f\" ch1.{name}: \", getattr(awg1.ch1.parameters[name], \"label\", \"\"))\n", "\n", "# Marker parameters (e.g. ch1.m1)\n", "print(\"\\nChannel 1 Marker 1 parameters:\")\n", "for name in sorted(awg1.ch1.m1.parameters):\n", - " print(f\" ch1.m1.{name}: \", awg1.ch1.m1.parameters[name].label)" + " print(f\" ch1.m1.{name}: \", getattr(awg1.ch1.m1.parameters[name], \"label\", \"\"))" ] }, { diff --git a/pyproject.toml b/pyproject.toml index 0ff7239f30e9..37cf1b059479 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,9 @@ test = [ "pytest-rerunfailures>=14.0", "pytest-xdist>=3.6.1", "PyVisa-sim>=0.6.0", + "scipy-stubs>=1.18.1.0", # type check docs examples using scipy "sphinx>=4.5.0", # sphinx extension tests + "ty>=0.0.75", "types-jsonschema>=4.16.0", "types-networkx >= 3.6.1.20260512", # minimum version required for generic data types in graph "types_requests>=0.1.8", @@ -293,6 +295,48 @@ quote-annotations = true sdist = "versioningit.cmdclass.sdist" build_py = "versioningit.cmdclass.build_py" +[tool.ty.environment] +# a number of drivers are only usable on Windows. Checking against all +# platforms means that these are type checked no matter which platform ty +# runs on, and that the result does not depend on the platform of the developer. +python-platform = "all" + +[tool.ty.src] +# unlike pyright above, ty also understands Jupyter notebooks, so the example +# notebooks in docs are checked too. That is coverage we get from ty alone. +include = ["src", "tests", "docs"] +exclude = [ + "src/qcodes/instrument_drivers/Harvard/Decadac.py", + ] + +# these are packages that we import +# but don't have installed by default. +# Compare with ignore_missing_imports in the mypy config above +[[tool.ty.overrides]] +include = [ + "src/qcodes/instrument_drivers/Galil/dmc_41x3.py", + "src/qcodes/instrument_drivers/Minicircuits/USBHIDMixin.py", + "src/qcodes/instrument_drivers/Minicircuits/_minicircuits_usb_spdt.py", +] +[tool.ty.overrides.rules] +unresolved-import = "ignore" + +# clr is provided by pythonnet which is not installed by default +# so its members cannot be resolved either +[[tool.ty.overrides]] +include = ["src/qcodes/instrument_drivers/Minicircuits/_minicircuits_usb_spdt.py"] +[tool.ty.overrides.rules] +unresolved-attribute = "ignore" + +# plottr is a separate package that this notebook demonstrates integrating with, +# it is not a dependency of qcodes +[[tool.ty.overrides]] +include = [ + "docs/examples/plotting/How-to-use-Plottr-with-QCoDeS-for-live-plotting.ipynb", +] +[tool.ty.overrides.rules] +unresolved-import = "ignore" + [tool.towncrier] package = "qcodes" name = "QCoDeS" diff --git a/requirements.txt b/requirements.txt index 1f3e95d6555d..d6b7a899fd53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -457,6 +457,8 @@ traitlets==5.16.1 # nbconvert # nbformat # nbsphinx +ty==0.0.75 + # via qcodes (pyproject.toml) types-jsonschema==4.26.0.20260518 # via qcodes (pyproject.toml) types-networkx==3.6.1.20260728 diff --git a/src/qcodes/dataset/data_set.py b/src/qcodes/dataset/data_set.py index f36ad008772f..96c88bad1d7a 100644 --- a/src/qcodes/dataset/data_set.py +++ b/src/qcodes/dataset/data_set.py @@ -810,7 +810,7 @@ def _ensure_dataset_written(self) -> None: def get_parameter_data( self, - *params: str | ParamSpec | ParameterBase, + *params: str | ParamSpecBase | ParameterBase, start: int | None = None, end: int | None = None, callback: Callable[[float], None] | None = None, @@ -1144,7 +1144,11 @@ def write_data_to_text_file( def subscribe( self, - callback: Callable[[Any, int, Any | None], None], + # ``Callable[..., None]`` rather than a three argument callable because + # ``callback_kwargs`` below is bound onto the callback with + # ``functools.partial``, so it may take further keyword arguments. This + # matches how ``_Subscriber`` types the same callback. + callback: Callable[..., None], min_wait: int = 0, min_count: int = 1, state: Any | None = None, @@ -1406,28 +1410,21 @@ def _finalize_res_dict_standalones( for param, value in result_dict.items(): if param.type == "text": if value.shape: - new_res: list[dict[str, VALUE]] = [ - {param.name: str(val)} for val in value - ] - res_list += new_res + res_list.extend({param.name: str(val)} for val in value) else: - new_res = [{param.name: str(value)}] - res_list += new_res + res_list.append({param.name: str(value)}) elif param.type == "numeric": if value.shape: - res_list += [{param.name: number} for number in value] + res_list.extend({param.name: number} for number in value) else: - new_res = [{param.name: float(value)}] - res_list += new_res + res_list.append({param.name: float(value)}) elif param.type == "complex": if value.shape: - res_list += [{param.name: number} for number in value] + res_list.extend({param.name: number} for number in value) else: - new_res = [{param.name: complex(value)}] - res_list += new_res + res_list.append({param.name: complex(value)}) else: - new_res = [{param.name: value}] - res_list += new_res + res_list.append({param.name: value}) return res_list diff --git a/src/qcodes/dataset/data_set_in_memory.py b/src/qcodes/dataset/data_set_in_memory.py index 241b789ba3bb..d42fac295d59 100644 --- a/src/qcodes/dataset/data_set_in_memory.py +++ b/src/qcodes/dataset/data_set_in_memory.py @@ -595,6 +595,11 @@ def _snapshot_raw(self) -> str | None: """Snapshot of the run as a JSON-formatted string (or None).""" return self._snapshot_raw_data + @property + def snapshot_raw(self) -> str | None: + """Snapshot of the run as a JSON-formatted string (or None).""" + return self._snapshot_raw + def add_metadata(self, tag: str, metadata: Any) -> None: """ Adds metadata to the :class:`.DataSet`. @@ -894,7 +899,7 @@ def to_pandas_dataframe( def get_parameter_data( self, - *params: str | ParamSpec | ParameterBase, + *params: str | ParamSpecBase | ParameterBase, start: int | None = None, end: int | None = None, callback: Callable[[float], None] | None = None, @@ -904,7 +909,7 @@ def get_parameter_data( @staticmethod def _warn_if_set( - *params: str | ParamSpec | ParameterBase, + *params: str | ParamSpecBase | ParameterBase, start: int | None = None, end: int | None, ) -> None: diff --git a/src/qcodes/dataset/data_set_protocol.py b/src/qcodes/dataset/data_set_protocol.py index cd31082e8809..8e95a7d67e1d 100644 --- a/src/qcodes/dataset/data_set_protocol.py +++ b/src/qcodes/dataset/data_set_protocol.py @@ -55,6 +55,7 @@ | npt.NDArray | Sequence[ScalarResTypes] | Sequence[Sequence[ScalarResTypes]] + | Sequence[npt.NDArray] ) type ResType = "tuple[ParameterBase | str, ValuesType]" type SetpointsType = "Sequence[str | ParameterBase]" @@ -168,6 +169,9 @@ def add_snapshot(self, snapshot: str, overwrite: bool = False) -> None: ... @property def _snapshot_raw(self) -> str | None: ... + @property + def snapshot_raw(self) -> str | None: ... + def add_metadata(self, tag: str, metadata: Any) -> None: ... @property @@ -204,7 +208,7 @@ def cache(self) -> DataSetCache[DataSetProtocol]: ... def get_parameter_data( self, - *params: str | ParamSpec | ParameterBase, + *params: str | ParamSpecBase | ParameterBase, start: int | None = None, end: int | None = None, callback: Callable[[float], None] | None = None, @@ -467,11 +471,11 @@ def _add_metadata_to_netcdf_if_nc_exported(self, tag: str, data: Any) -> None: ) @staticmethod - def _validate_parameters(*params: str | ParamSpec | ParameterBase) -> list[str]: + def _validate_parameters(*params: str | ParamSpecBase | ParameterBase) -> list[str]: """ Validate that the provided parameters have a name and return those names as a list. - The Parameters may be a mix of strings, ParamSpecs or ordinary + The Parameters may be a mix of strings, ParamSpecsBase or ordinary QCoDeS parameters. """ diff --git a/src/qcodes/dataset/descriptions/param_spec.py b/src/qcodes/dataset/descriptions/param_spec.py index 8a965080cca6..d455565b80e2 100644 --- a/src/qcodes/dataset/descriptions/param_spec.py +++ b/src/qcodes/dataset/descriptions/param_spec.py @@ -181,7 +181,7 @@ def base_version(self) -> _ParamSpecBase: ) @classmethod - def _from_dict(cls, ser: ParamSpecDict) -> ParamSpec: # type: ignore[override] + def _from_dict(cls, ser: ParamSpecDict) -> ParamSpec: # type: ignore[override] # ty: ignore[invalid-method-override] """ Create a ParamSpec instance of the current version from a dictionary representation of ParamSpec of some version diff --git a/src/qcodes/dataset/json_exporter.py b/src/qcodes/dataset/json_exporter.py index dcf4ac3fef19..6c60aa79e4d1 100644 --- a/src/qcodes/dataset/json_exporter.py +++ b/src/qcodes/dataset/json_exporter.py @@ -8,13 +8,17 @@ if TYPE_CHECKING: from collections.abc import Mapping -json_template_linear = { +# These are templates for a JSON document, so the values are deliberately +# heterogeneous and consumers index arbitrarily deep into them. Annotating the +# value type as ``Any`` matches how ``export_data_as_json_*`` below already +# types the state they are copied into. +json_template_linear: dict[str, Any] = { "type": "linear", "x": {"data": [], "name": "", "full_name": "", "is_setpoint": True, "unit": ""}, "y": {"data": [], "name": "", "full_name": "", "is_setpoint": False, "unit": ""}, } -json_template_heatmap = { +json_template_heatmap: dict[str, Any] = { "type": "heatmap", "x": {"data": [], "name": "", "full_name": "", "is_setpoint": True, "unit": ""}, "y": {"data": [], "name": "", "full_name": "", "is_setpoint": True, "unit": ""}, diff --git a/src/qcodes/dataset/legacy_import.py b/src/qcodes/dataset/legacy_import.py index babd3955209e..22cd030ce436 100644 --- a/src/qcodes/dataset/legacy_import.py +++ b/src/qcodes/dataset/legacy_import.py @@ -44,23 +44,45 @@ def setup_measurement( return meas +def _array_id(array: DataArray) -> str: + """ + Return the ``array_id`` of a legacy ``DataArray``. + + Args: + array: Legacy data array to read the id from. + + Raises: + ValueError: If the array has no ``array_id``. Parameters are registered + by name, so an array without an id cannot be stored. + + """ + array_id = array.array_id + if array_id is None: + raise ValueError(f"Cannot store an array without an array_id: {array!r}") + return array_id + + def store_array_to_database(datasaver: DataSaver, array: DataArray) -> int: assert array.shape is not None dims = len(array.shape) assert array.array_id is not None if dims == 2: - for index1, i in enumerate(array.set_arrays[0]): - for index2, j in enumerate(array.set_arrays[1][index1]): + setpoints_outer = array.set_arrays[0] + setpoints_inner = array.set_arrays[1] + outer_id = _array_id(setpoints_outer) + inner_id = _array_id(setpoints_inner) + for index1, i in enumerate(setpoints_outer): + for index2, j in enumerate(setpoints_inner[index1]): datasaver.add_result( - (array.set_arrays[0].array_id, i), - (array.set_arrays[1].array_id, j), + (outer_id, i), + (inner_id, j), (array.array_id, array[index1, index2]), ) elif dims == 1: - for index, i in enumerate(array.set_arrays[0]): - datasaver.add_result( - (array.set_arrays[0].array_id, i), (array.array_id, array[index]) - ) + setpoints = array.set_arrays[0] + setpoints_id = _array_id(setpoints) + for index, i in enumerate(setpoints): + datasaver.add_result((setpoints_id, i), (array.array_id, array[index])) else: raise NotImplementedError( "The exporter only currently handles 1 and 2 Dimensional data" @@ -73,23 +95,25 @@ def store_array_to_database_alt(meas: Measurement, array: DataArray) -> int: dims = len(array.shape) assert array.array_id is not None if dims == 2: - outer_data = np.empty( - array.shape[1] # pyright: ignore[reportGeneralTypeIssues] - ) + setpoints_outer = array.set_arrays[0] + setpoints_inner = array.set_arrays[1] + outer_id = _array_id(setpoints_outer) + inner_id = _array_id(setpoints_inner) + outer_data = np.empty(array.shape[1]) with meas.run() as datasaver: - for index1, i in enumerate(array.set_arrays[0]): + for index1, i in enumerate(setpoints_outer): outer_data[:] = i datasaver.add_result( - (array.set_arrays[0].array_id, outer_data), - (array.set_arrays[1].array_id, array.set_arrays[1][index1, :]), + (outer_id, outer_data), + (inner_id, setpoints_inner[index1, :]), (array.array_id, array[index1, :]), ) elif dims == 1: + setpoints = array.set_arrays[0] + setpoints_id = _array_id(setpoints) with meas.run() as datasaver: - for index, i in enumerate(array.set_arrays[0]): - datasaver.add_result( - (array.set_arrays[0].array_id, i), (array.array_id, array[index]) - ) + for index, i in enumerate(setpoints): + datasaver.add_result((setpoints_id, i), (array.array_id, array[index])) else: raise NotImplementedError( "The exporter only currently handles 1 and 2 Dimensional data" diff --git a/src/qcodes/dataset/sqlite/database.py b/src/qcodes/dataset/sqlite/database.py index a0d17507babf..f3c9be83a0b3 100644 --- a/src/qcodes/dataset/sqlite/database.py +++ b/src/qcodes/dataset/sqlite/database.py @@ -105,7 +105,7 @@ def _convert_numeric(value: bytes) -> float | int | str: return numeric_int -def _adapt_float(fl: float) -> float | str: +def _adapt_float(fl: float | np.floating) -> float | str: # For a single value, math.isnan is 10 times faster than np.isnan # Overall, saving floats with numeric format is 2 times faster with math.isnan if math.isnan(fl): @@ -174,7 +174,10 @@ def connect( sqlite3.register_converter("numeric", _convert_numeric) - for numpy_float in (float, *numpy_floats): + # registered separately from the numpy floats below, so that the element + # type of the loop stays a numpy float rather than widening to object + sqlite3.register_adapter(float, _adapt_float) + for numpy_float in numpy_floats: sqlite3.register_adapter(numpy_float, _adapt_float) for complex_type in complex_types: diff --git a/src/qcodes/dataset/sqlite/db_overview.py b/src/qcodes/dataset/sqlite/db_overview.py index e596439f46aa..590a94fb18a9 100644 --- a/src/qcodes/dataset/sqlite/db_overview.py +++ b/src/qcodes/dataset/sqlite/db_overview.py @@ -253,7 +253,7 @@ def get_db_overview( # The keys of ``extra`` are only known at runtime (they are the # user-supplied ``extra_columns``), so they cannot be part of # the closed ``RunOverviewDict`` definition. - entry.update(extra) # type: ignore[typeddict-item] + entry.update(extra) # type: ignore[typeddict-item] # ty: ignore[invalid-argument-type] overview[run_id] = entry diff --git a/src/qcodes/extensions/infer.py b/src/qcodes/extensions/infer.py index 3abc0d3ff0bd..80c002857f5c 100644 --- a/src/qcodes/extensions/infer.py +++ b/src/qcodes/extensions/infer.py @@ -226,7 +226,10 @@ def get_chain_links_of_type[C: ParameterBase]( link_param_type: type[C] | tuple[type[C], ...], parameter: Parameter ) -> tuple[C, ...]: """Gets all parameters in a chain of linked parameters that match a given type""" - chain_links: list[C] = [ + # ty does not narrow the element type to C here: for a generic parameter + # class it widens the isinstance narrowing to a union with the unnarrowed + # type. The equivalent non generic code narrows correctly. + chain_links: list[C] = [ # ty: ignore[invalid-assignment] param for param in get_parameter_chain(parameter) if isinstance(param, link_param_type) diff --git a/src/qcodes/extensions/parameters/parameter_mixin_on_cache_change.py b/src/qcodes/extensions/parameters/parameter_mixin_on_cache_change.py index edd9ca9cbc88..fdeafd7c6701 100644 --- a/src/qcodes/extensions/parameters/parameter_mixin_on_cache_change.py +++ b/src/qcodes/extensions/parameters/parameter_mixin_on_cache_change.py @@ -143,7 +143,7 @@ def wrapped_cache_update( raw_value_new=raw_value_new, ) - parameter.cache._update_with = wrapped_cache_update # type: ignore[method-assign] + parameter.cache._update_with = wrapped_cache_update # type: ignore[method-assign] # ty: ignore[invalid-assignment] def _handle_on_cache_change( self, *, value_old: Any, value_new: Any, raw_value_old: Any, raw_value_new: Any diff --git a/src/qcodes/instrument/channel.py b/src/qcodes/instrument/channel.py index 1dbc25deabca..2c77792638e7 100644 --- a/src/qcodes/instrument/channel.py +++ b/src/qcodes/instrument/channel.py @@ -731,9 +731,10 @@ def __setitem__( # asserts added to work around https://github.com/python/mypy/issues/7858 if isinstance(index, int): assert isinstance(value, InstrumentModule) - self._channels[index] = value # type: ignore[assignment] - # mypy does not know that InstrumentModuleType is a TypeVar bound to - # InstrumentModule so complains here + # neither mypy nor ty knows that InstrumentModuleType is a TypeVar + # bound to InstrumentModule, so narrowing value with the isinstance + # above does not give them the element type of the list + self._channels[index] = value # type: ignore[assignment] # ty: ignore[invalid-assignment] else: assert not isinstance(value, InstrumentModule) self._channels[index] = value @@ -1213,7 +1214,9 @@ def __init__( chan_type: type[TAUTORELOADCHANNEL], chan_list: Sequence[TAUTORELOADCHANNEL] | None = None, snapshotable: bool = True, - multichan_paramclass: type = MultiChannelInstrumentParameter, + multichan_paramclass: type[MultiChannelInstrumentParameter] = ( + MultiChannelInstrumentParameter + ), **kwargs: Any, ) -> None: super().__init__( diff --git a/src/qcodes/instrument/instrument_base.py b/src/qcodes/instrument/instrument_base.py index 733bfa0ecb6c..b94e33a0bfa6 100644 --- a/src/qcodes/instrument/instrument_base.py +++ b/src/qcodes/instrument/instrument_base.py @@ -32,7 +32,13 @@ log = logging.getLogger(__name__) # Cannot convert to PEP 695: uses default= which requires PEP 696 (Python 3.13+). -TParameter = TypeVar("TParameter", bound="ParameterBase", default="Parameter") +# The default is `Parameter[Any, Any]` rather than a bare `Parameter`: when +# `add_parameter` is called without a `parameter_class` the returned parameter is +# bound to `self`, so spelling the default as `Parameter` (which expands to +# `Parameter[Any, InstrumentBase | None]`) would wrongly claim that the +# instrument is `InstrumentBase | None` and make the result unassignable to the +# `Parameter[SomeType, Self]` annotations that drivers use. +TParameter = TypeVar("TParameter", bound="ParameterBase", default="Parameter[Any, Any]") TSubmodule = TypeVar( "TSubmodule", bound="InstrumentModule | ChannelTuple", default="InstrumentModule" ) diff --git a/src/qcodes/instrument/ip_to_visa.py b/src/qcodes/instrument/ip_to_visa.py index 67d61acb421b..1183509a3f5e 100644 --- a/src/qcodes/instrument/ip_to_visa.py +++ b/src/qcodes/instrument/ip_to_visa.py @@ -24,7 +24,7 @@ # Such a driver is just a two-line class definition. -class IPToVisa(VisaInstrument, IPInstrument): # type: ignore[misc] +class IPToVisa(VisaInstrument, IPInstrument): # type: ignore[misc] # ty: ignore[invalid-method-override] """ Class to inject an VisaInstrument like behaviour in an IPInstrument that we'd like to use as a VISAInstrument with the diff --git a/src/qcodes/instrument_drivers/AlazarTech/ATS.py b/src/qcodes/instrument_drivers/AlazarTech/ATS.py index dd37c8244686..ea07debefd85 100644 --- a/src/qcodes/instrument_drivers/AlazarTech/ATS.py +++ b/src/qcodes/instrument_drivers/AlazarTech/ATS.py @@ -153,7 +153,7 @@ def __init__( self.buffer_list: list[Buffer] = [] - def get_idn(self) -> dict[str, str | int | None]: # type: ignore[override] + def get_idn(self) -> dict[str, str | int | None]: # type: ignore[override] # ty: ignore[invalid-method-override] # TODO return type is inconsistent with the super class. We should consider # if ints and floats are allowed as values in the dict """ diff --git a/src/qcodes/instrument_drivers/AlazarTech/dll_wrapper.py b/src/qcodes/instrument_drivers/AlazarTech/dll_wrapper.py index 425ab143f5e8..2432ae048452 100644 --- a/src/qcodes/instrument_drivers/AlazarTech/dll_wrapper.py +++ b/src/qcodes/instrument_drivers/AlazarTech/dll_wrapper.py @@ -64,17 +64,21 @@ def _check_error_code( if len(argrepr) > 100: argrepr = argrepr[:96] + "...]" + # ``errcheck`` is always handed a ctypes foreign function, which has a + # ``__name__``, but a plain ``Callable`` is not guaranteed to. + func_name = getattr(func, "__name__", repr(func)) + logger.error( f"Alazar API returned code {return_code} from function " - f"{func.__name__} with args {argrepr}" + f"{func_name} with args {argrepr}" ) if return_code not in ERROR_CODES: raise RuntimeError( - f"unknown error {return_code} from function {func.__name__} with args: {argrepr}" + f"unknown error {return_code} from function {func_name} with args: {argrepr}" ) raise RuntimeError( - f"error {return_code}: {ERROR_CODES[ReturnCode(return_code)]} from function {func.__name__} with args: {argrepr}" + f"error {return_code}: {ERROR_CODES[ReturnCode(return_code)]} from function {func_name} with args: {argrepr}" ) return arguments diff --git a/src/qcodes/instrument_drivers/Keithley/Keithley_7510.py b/src/qcodes/instrument_drivers/Keithley/Keithley_7510.py index b45b87504149..2929ce948c87 100644 --- a/src/qcodes/instrument_drivers/Keithley/Keithley_7510.py +++ b/src/qcodes/instrument_drivers/Keithley/Keithley_7510.py @@ -367,7 +367,8 @@ def _get_data(self) -> DataArray7510: n_elements = len(elements) units = tuple(elements_units[element] for element in elements) - processed_data = dict.fromkeys(elements) + # every element is filled in by the loop below + processed_data: dict[str, npt.NDArray] = {} for i, (element, unit) in enumerate(zip(elements, units)): if unit == "str": processed_data[element] = np.array(all_data[i::n_elements]) @@ -384,12 +385,9 @@ def _get_data(self) -> DataArray7510: setpoint_units=((self.setpoints.unit,),) * n_elements, setpoint_names=((self.setpoints.label,),) * n_elements, ) - data._data = tuple( - tuple(processed_data[element]) # type: ignore[arg-type] - for element in elements - ) + data._data = tuple(tuple(processed_data[element]) for element in elements) for i in range(len(data.names)): - setattr(data, data.names[i], tuple(processed_data[data.names[i]])) # type: ignore[arg-type] + setattr(data, data.names[i], tuple(processed_data[data.names[i]])) return data def clear_buffer(self) -> None: diff --git a/src/qcodes/instrument_drivers/Keysight/Infiniium.py b/src/qcodes/instrument_drivers/Keysight/Infiniium.py index 8fdb6ee41adb..80ce22637bd1 100644 --- a/src/qcodes/instrument_drivers/Keysight/Infiniium.py +++ b/src/qcodes/instrument_drivers/Keysight/Infiniium.py @@ -3,7 +3,7 @@ from io import BytesIO from os.path import splitext from pathlib import Path -from typing import TYPE_CHECKING, Any, ClassVar, Literal +from typing import TYPE_CHECKING, Any, ClassVar, Literal, cast import numpy as np import numpy.typing as npt @@ -134,8 +134,7 @@ def setpoints(self) -> "Sequence[ParameterBase]": """ instrument = self.instrument if isinstance(instrument, KeysightInfiniiumChannel): - root_instrument: KeysightInfiniium - root_instrument = self.root_instrument # type: ignore[assignment] + root_instrument = cast("KeysightInfiniium", self.root_instrument) cache_setpoints = root_instrument.cache_setpoints() if not cache_setpoints: self.update_setpoints() @@ -201,7 +200,8 @@ def update_fft_setpoints(self) -> None: """ Update waveform parameters for an FFT. """ - instrument: KeysightInfiniiumFunction = self.instrument # type: ignore[assignment] + # only reached for a function parameter, see the caller in ``setpoints`` + instrument = cast("KeysightInfiniiumFunction", self.instrument) instrument.write(f":WAV:SOUR {self._channel}") preamble = instrument.ask(":WAV:PRE?").strip().split(",") self.update_setpoints(preamble) @@ -215,7 +215,7 @@ def get_raw(self) -> npt.NDArray: """ if self.instrument is None: raise RuntimeError("Cannot get data without instrument") - root_instr: KeysightInfiniium = self.root_instrument # type: ignore[assignment] + root_instr = cast("KeysightInfiniium", self.root_instrument) # Check if we can use cached trace parameters if not root_instr.cache_setpoints(): self.update_setpoints() @@ -234,13 +234,16 @@ def get_raw(self) -> npt.NDArray: root_instr.write(":WAV:DATA?") # Ignore first two bytes, which should be "#0" _ = root_instr.visa_handle.read_bytes(2) - data: npt.NDArray - data = root_instr.visa_handle.read_binary_values( # type: ignore[assignment] - "h", - container=np.ndarray, - header_fmt="empty", - expect_termination=True, - data_points=self._points, + # pyvisa types the return as a Sequence[float] regardless of ``container`` + data = cast( + "npt.NDArray", + root_instr.visa_handle.read_binary_values( + "h", + container=np.ndarray, + header_fmt="empty", + expect_termination=True, + data_points=self._points, + ), ) data = data.astype(np.float64) data = (data * self._yincrement) + self._yoffset @@ -1275,15 +1278,20 @@ def screenshot( ) try: with open(img_path, "wb") as f: - screen_bytes = self.visa_handle.query_binary_values( - f":DISPlay:DATA? {img_type.upper()[1:]}", # without . - # https://docs.python.org/3/library/struct.html#format-characters - datatype="B", # Capitcal B for unsigned byte - container=bytes, + # pyvisa types the return as a Sequence[float] regardless of + # ``container`` + screen_bytes = cast( + "bytes", + self.visa_handle.query_binary_values( + f":DISPlay:DATA? {img_type.upper()[1:]}", # without . + # https://docs.python.org/3/library/struct.html#format-characters + datatype="B", # Capitcal B for unsigned byte + container=bytes, + ), ) - f.write(screen_bytes) # type: ignore[arg-type] + f.write(screen_bytes) print(f"Screen image written to {img_path}") - return np.asarray(pil_open(BytesIO(screen_bytes))) # type: ignore[arg-type] + return np.asarray(pil_open(BytesIO(screen_bytes))) except Exception as e: self.log.error(f"Failed to save screenshot, Error occurred: \n{e}") return None diff --git a/src/qcodes/instrument_drivers/Keysight/keysight_34934a.py b/src/qcodes/instrument_drivers/Keysight/keysight_34934a.py index b206bf733474..a91d4f3d49b8 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysight_34934a.py +++ b/src/qcodes/instrument_drivers/Keysight/keysight_34934a.py @@ -6,7 +6,7 @@ from .keysight_34980a_submodules import Keysight34980ASwitchMatrixSubModule if TYPE_CHECKING: - from collections.abc import Callable + from collections.abc import Callable, Collection from typing import Unpack from qcodes.instrument import ( @@ -105,7 +105,7 @@ def _set_relay_protection_mode(self, mode: str) -> None: self.write(f"SYSTem:MODule:ROW:PROTection {self.slot}, {mode}") def to_channel_list( - self, paths: list[tuple[int, int]], wiring_config: str | None = "" + self, paths: "Collection[tuple[int, int]]", wiring_config: str | None = "" ) -> str: """ Convert the (row, column) pair to a 4-digit channel number 'sxxx', where diff --git a/src/qcodes/instrument_drivers/Keysight/keysight_34980a.py b/src/qcodes/instrument_drivers/Keysight/keysight_34980a.py index 576b98882519..f93c08a149f2 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysight_34980a.py +++ b/src/qcodes/instrument_drivers/Keysight/keysight_34980a.py @@ -70,7 +70,8 @@ def __init__( self._total_slot = 8 self._system_slots_info_dict: dict[int, dict[str, str]] | None = None - self.module = dict.fromkeys(self.system_slots_info.keys()) + # populated by scan_slots below, which puts an entry in for every slot + self.module: dict[int, Keysight34980ASwitchMatrixSubModule] = {} self.scan_slots() self.connect_message() @@ -132,7 +133,7 @@ def scan_slots(self) -> None: self.module[slot] = sub_module self.add_submodule(sub_module_name, sub_module) break - if self.module[slot] is None: + if slot not in self.module: sub_module_name = f"slot_{slot}_{model_string}_no_driver" sub_module_no_driver = Keysight34980ASwitchMatrixSubModule( self, sub_module_name, slot diff --git a/src/qcodes/instrument_drivers/Keysight/keysight_34980a_submodules.py b/src/qcodes/instrument_drivers/Keysight/keysight_34980a_submodules.py index 67c44060a613..ae9ad9b0d37a 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysight_34980a_submodules.py +++ b/src/qcodes/instrument_drivers/Keysight/keysight_34980a_submodules.py @@ -3,6 +3,7 @@ from qcodes.instrument import InstrumentBaseKWArgs, InstrumentChannel if TYPE_CHECKING: + from collections.abc import Collection from typing import Unpack from .keysight_34980a import Keysight34980A @@ -43,7 +44,7 @@ def validate_value(self, row: int, column: int) -> None: raise NotImplementedError("Please subclass this") def to_channel_list( - self, paths: list[tuple[int, int]], wiring_config: str | None = None + self, paths: "Collection[tuple[int, int]]", wiring_config: str | None = None ) -> str: """ Convert the (row, column) pair to a 4-digit channel number 'sxxx', where @@ -125,7 +126,7 @@ def disconnect(self, row: int, column: int) -> None: channel = self.to_channel_list([(row, column)]) self.write(f"ROUT:OPEN {channel}") - def connect_paths(self, paths: list[tuple[int, int]]) -> None: + def connect_paths(self, paths: "Collection[tuple[int, int]]") -> None: """ To connect/close the specified channels. @@ -138,7 +139,7 @@ def connect_paths(self, paths: list[tuple[int, int]]) -> None: channel_list_str = self.to_channel_list(paths) self.write(f"ROUTe:CLOSe {channel_list_str}") - def disconnect_paths(self, paths: list[tuple[int, int]]) -> None: + def disconnect_paths(self, paths: "Collection[tuple[int, int]]") -> None: """ To disconnect/open the specified channels. @@ -151,7 +152,7 @@ def disconnect_paths(self, paths: list[tuple[int, int]]) -> None: channel_list_str = self.to_channel_list(paths) self.write(f"ROUTe:OPEN {channel_list_str}") - def are_closed(self, paths: list[tuple[int, int]]) -> list[bool]: + def are_closed(self, paths: "Collection[tuple[int, int]]") -> list[bool]: """ To check if a list of channels is closed/connected @@ -170,7 +171,7 @@ def are_closed(self, paths: list[tuple[int, int]]) -> list[bool]: messages = self.ask(f"ROUTe:CLOSe? {channel_list_str}") return [bool(int(message)) for message in messages.split(",")] - def are_open(self, paths: list[tuple[int, int]]) -> list[bool]: + def are_open(self, paths: "Collection[tuple[int, int]]") -> list[bool]: """ To check if a list of channels is open/disconnected diff --git a/src/qcodes/instrument_drivers/Keysight/keysight_b220x.py b/src/qcodes/instrument_drivers/Keysight/keysight_b220x.py index d6c4f9ee2c96..c8e01b519620 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysight_b220x.py +++ b/src/qcodes/instrument_drivers/Keysight/keysight_b220x.py @@ -7,7 +7,7 @@ from qcodes.validators import Enum, Ints, Lists, MultiType if TYPE_CHECKING: - from collections.abc import Callable, Sequence + from collections.abc import Callable, Iterable from typing import Concatenate, Unpack from qcodes.parameters import Parameter @@ -251,12 +251,12 @@ def connect(self, input_ch: int, output_ch: int) -> None: self.write(f":CLOS (@{self._card:01d}{input_ch:02d}{output_ch:02d})") @post_execution_status_poll - def connect_paths(self, paths: "Sequence[tuple[int, int]]") -> None: + def connect_paths(self, paths: "Iterable[tuple[int, int]]") -> None: channel_list_str = self.to_channel_list(paths) self.write(f":CLOS {channel_list_str}") @post_execution_status_poll - def disconnect_paths(self, paths: "Sequence[tuple[int, int]]") -> None: + def disconnect_paths(self, paths: "Iterable[tuple[int, int]]") -> None: channel_list_str = self.to_channel_list(paths) self.write(f":OPEN {channel_list_str}") @@ -424,7 +424,7 @@ def parse_channel_list(channel_list: str) -> set[tuple[int, int]]: for match in re.finditer(pattern, channel_list) } - def to_channel_list(self, paths: "Sequence[tuple[int, int]]") -> str: + def to_channel_list(self, paths: "Iterable[tuple[int, int]]") -> str: chan = [f"{self._card:01d}{i:02d}{o:02d}" for i, o in paths] channel_list = f"(@{','.join(chan)})" return channel_list diff --git a/src/qcodes/instrument_drivers/Keysight/keysight_e4980a.py b/src/qcodes/instrument_drivers/Keysight/keysight_e4980a.py index 5b339d2e90bf..0c0e0f2107bd 100644 --- a/src/qcodes/instrument_drivers/Keysight/keysight_e4980a.py +++ b/src/qcodes/instrument_drivers/Keysight/keysight_e4980a.py @@ -50,6 +50,16 @@ class KeysightE4980AMeasurementPair(MultiParameter): value: tuple[float, float] = (0.0, 0.0) + if TYPE_CHECKING: + # The two measured values are exposed as attributes named after the + # ``names`` of the measurement function, so which attributes exist is + # only known at runtime. Declaring this for type checkers lets the + # documented usage, such as ``measurement.capacitance``, be written in + # typed code. It is not defined at runtime, so accessing an attribute + # that the current measurement function does not provide still raises + # the usual ``AttributeError``. + def __getattr__(self, name: str) -> float: ... + def __init__( self, name: str, names: "Sequence[str]", units: "Sequence[str]", **kwargs: Any ): diff --git a/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py b/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py index 1b8b9ff9d8e1..2ad9601ae4f8 100644 --- a/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py +++ b/src/qcodes/instrument_drivers/Lakeshore/lakeshore_base.py @@ -690,9 +690,9 @@ class LakeshoreBase(VisaInstrument, Generic[ChanType_co]): # Define this in the model-specific class in case you want to use a # different class for sensor channels # type error. It's not clear to me why assigning a value that matches the - # default of the TypeVar is an error but both mypy and pyright - # flags it here. - CHANNEL_CLASS: type[ChanType_co] = LakeshoreBaseSensorChannel # type: ignore[assignment] + # default of the TypeVar is an error but mypy, pyright and ty all + # flag it here. + CHANNEL_CLASS: type[ChanType_co] = LakeshoreBaseSensorChannel # type: ignore[assignment] # ty: ignore[invalid-assignment] # This dict has channel name in the driver as keys, and channel "name" that # is used in instrument commands as values. For example, if channel called diff --git a/src/qcodes/instrument_drivers/QuantumDesign/DynaCoolPPMS/private/server.py b/src/qcodes/instrument_drivers/QuantumDesign/DynaCoolPPMS/private/server.py index 0494c21f161b..d08f587de900 100644 --- a/src/qcodes/instrument_drivers/QuantumDesign/DynaCoolPPMS/private/server.py +++ b/src/qcodes/instrument_drivers/QuantumDesign/DynaCoolPPMS/private/server.py @@ -31,7 +31,7 @@ def run_server() -> None: # Dictionary to keep track of sockets and addresses. # Keys are sockets and values are addresses. # Add server socket to the dictionary first. - socket_dict = {server_socket: (ADDRESS, PORT)} + socket_dict: dict[socket.socket, tuple[str, int]] = {server_socket: (ADDRESS, PORT)} print(f"Server started on port {PORT}.") print("Press ESC to exit.") diff --git a/src/qcodes/instrument_drivers/tektronix/AWG5014.py b/src/qcodes/instrument_drivers/tektronix/AWG5014.py index c0084abea479..604a7ade9f64 100644 --- a/src/qcodes/instrument_drivers/tektronix/AWG5014.py +++ b/src/qcodes/instrument_drivers/tektronix/AWG5014.py @@ -605,7 +605,7 @@ def __init__( r"^ch(?P[1-4])_(?:(?Pm[12])_)?(?P.+)$" ) - def __getattr__(self, name: str) -> Any: + def __getattr__(self, key: str) -> Any: """ Provide backwards-compatible access to the old flat parameter names like ``ch1_amp``, ``ch1_m1_high``, etc. @@ -613,7 +613,7 @@ def __getattr__(self, name: str) -> Any: These now live on channel / marker submodules but are still reachable via the old names with a deprecation warning. """ - m = self._LEGACY_CHANNEL_RE.match(name) + m = self._LEGACY_CHANNEL_RE.match(key) if m is not None: ch_num = int(m.group("ch")) marker = m.group("marker") @@ -629,7 +629,7 @@ def __getattr__(self, name: str) -> Any: if hasattr(mrk, new_param): new_name = f"ch{ch_num}.{marker}.{new_param}" warnings.warn( - f"Accessing '{name}' is deprecated. " + f"Accessing '{key}' is deprecated. " f"Use '{new_name}' instead.", category=QCoDeSDeprecationWarning, stacklevel=2, @@ -638,12 +638,12 @@ def __getattr__(self, name: str) -> Any: elif hasattr(ch, param): new_name = f"ch{ch_num}.{param}" warnings.warn( - f"Accessing '{name}' is deprecated. Use '{new_name}' instead.", + f"Accessing '{key}' is deprecated. Use '{new_name}' instead.", category=QCoDeSDeprecationWarning, stacklevel=2, ) return getattr(ch, param) - return super().__getattr__(name) + return super().__getattr__(key) # Convenience parser def newlinestripper(self, string: str) -> str: diff --git a/src/qcodes/instrument_drivers/tektronix/AWGFileParser.py b/src/qcodes/instrument_drivers/tektronix/AWGFileParser.py index 9c7ed36b4c0e..da64e7919eb6 100644 --- a/src/qcodes/instrument_drivers/tektronix/AWGFileParser.py +++ b/src/qcodes/instrument_drivers/tektronix/AWGFileParser.py @@ -295,14 +295,18 @@ "WAIT_VALUE": {1: "First", 2: "Last"}, } +# The tuple returned by ``_parser3``, and therefore by ``parse_awg_file``. It +# deliberately matches the call signature of +# ``TektronixAWG5014.make_send_and_load_awg_file``, so that the output of the +# parser can be passed straight back in. _parser3_output = tuple[ - list[list[dict[Any, Any]]], - list[list[dict[Any, Any]]], - list[list[dict[Any, Any]]], - list[str | int], - list[str | int], - list[str | int], - list[str | int], + list[list[npt.NDArray]], + list[list[npt.NDArray]], + list[list[npt.NDArray]], + list[int], + list[int], + list[int], + list[int], list[int], ] diff --git a/src/qcodes/parameters/array_parameter.py b/src/qcodes/parameters/array_parameter.py index d3565f83f023..5fb1df55f58d 100644 --- a/src/qcodes/parameters/array_parameter.py +++ b/src/qcodes/parameters/array_parameter.py @@ -142,7 +142,8 @@ def __init__( kwargs.setdefault("snapshot_value", False) super().__init__( name, - **kwargs, + # see the note on ParameterBaseKWArgs + **kwargs, # ty: ignore[invalid-argument-type] ) if self.settable: diff --git a/src/qcodes/parameters/combined_parameter.py b/src/qcodes/parameters/combined_parameter.py index 4a6adab034b0..9c6ead6b2928 100644 --- a/src/qcodes/parameters/combined_parameter.py +++ b/src/qcodes/parameters/combined_parameter.py @@ -3,6 +3,7 @@ import collections import logging from copy import copy +from dataclasses import dataclass from typing import TYPE_CHECKING, Any import numpy as np @@ -21,6 +22,30 @@ _LOG = logging.getLogger(__name__) +@dataclass +class _CombinedParameterInfo: + """ + The subset of the ``Parameter`` api that :class:`CombinedParameter` fakes. + + This exists because :class:`CombinedParameter` does not inherit from + :class:`.Parameter` or :class:`.ParameterBase`, yet is expected to carry the + identifying metadata of one so that it can be snapshotted like one. + """ + + name: str + full_name: str + label: str | None + unit: str | None + + def __call__(self) -> None: + """ + Do nothing. + + This used to be a lambda, so external code may be calling it. Calling it + has always returned ``None``. + """ + + def combine( *parameters: Parameter, name: str, @@ -77,10 +102,6 @@ def __init__( aggregator: Callable[..., Any] | None = None, ) -> None: super().__init__() - # TODO(giulioungaretti)temporary hack - # starthack - # this is a dummy parameter - # that mimicks the api that a normal parameter has if not name.isidentifier(): raise ValueError( f"Parameter name must be a valid identifier " @@ -89,13 +110,6 @@ def __init__( f"must not contain spaces or special characters" ) - self.parameter = lambda: None - # mypy will complain that a callable does not have these attributes - # but you can still create them here. - self.parameter.full_name = name # type: ignore[attr-defined] - self.parameter.name = name # type: ignore[attr-defined] - self.parameter.label = label # type: ignore[attr-defined] - if units is not None: _LOG.warning( f"`units` is deprecated for the " @@ -103,9 +117,19 @@ def __init__( ) if unit is None: unit = units - self.parameter.unit = unit # type: ignore[attr-defined] - self.setpoints: list[Any] = [] + + # TODO(giulioungaretti)temporary hack + # starthack + # this is a dummy parameter + # that mimicks the api that a normal parameter has. + # CombinedParameter does not inherit from Parameter or ParameterBase, + # so it has to fake the parts of their api that it is expected to + # provide. + self.parameter = _CombinedParameterInfo( + name=name, full_name=name, label=label, unit=unit + ) # endhack + self.setpoints: list[Any] = [] self.parameters = parameters self.sets = [parameter.set for parameter in self.parameters] self.dimensionality = len(self.sets) @@ -215,9 +239,9 @@ def snapshot_base( meta_data: dict[str, Any] = collections.OrderedDict() meta_data["__class__"] = full_class(self) param = self.parameter - meta_data["unit"] = param.unit # type: ignore[attr-defined] - meta_data["label"] = param.label # type: ignore[attr-defined] - meta_data["full_name"] = param.full_name # type: ignore[attr-defined] + meta_data["unit"] = param.unit + meta_data["label"] = param.label + meta_data["full_name"] = param.full_name meta_data["aggregator"] = repr(getattr(self, "f", None)) update = normalize_snapshot_update(update) for parameter in self.parameters: diff --git a/src/qcodes/parameters/command.py b/src/qcodes/parameters/command.py index e2da0b14d014..0561dab1dabe 100644 --- a/src/qcodes/parameters/command.py +++ b/src/qcodes/parameters/command.py @@ -124,7 +124,10 @@ def __init__( elif is_function(cmd, arg_count): assert cmd is not None self._cmd = cmd - exec_mapping = { + cmd_exec_mapping: dict[ + tuple[bool | Literal["multi"], bool], + Callable[..., Output | ParsedOutput], + ] = { # (parse_input, parse_output) (False, False): cmd, (False, True): self.call_cmd_parsed_out, (True, False): self.call_cmd_parsed_in, @@ -132,7 +135,7 @@ def __init__( ("multi", False): self.call_cmd_parsed_in2, ("multi", True): self.call_cmd_parsed_in2_out, } - self.exec_function = exec_mapping[(parse_input, parse_output)] + self.exec_function = cmd_exec_mapping[(parse_input, parse_output)] elif cmd is None: if no_cmd_function is not None: diff --git a/src/qcodes/parameters/delegate_parameter.py b/src/qcodes/parameters/delegate_parameter.py index b948b8c568d4..b63736f19288 100644 --- a/src/qcodes/parameters/delegate_parameter.py +++ b/src/qcodes/parameters/delegate_parameter.py @@ -210,7 +210,8 @@ def __init__( initial_cache_value = kwargs.pop("initial_cache_value", None) self.source = source - super().__init__(name, **kwargs) + # see the note on ParameterBaseKWArgs + super().__init__(name, **kwargs) # ty: ignore[invalid-argument-type] self.label = kwargs.get("label", None) self.unit = kwargs.get("unit", None) diff --git a/src/qcodes/parameters/multi_parameter.py b/src/qcodes/parameters/multi_parameter.py index 0f230b693343..80385ff6304b 100644 --- a/src/qcodes/parameters/multi_parameter.py +++ b/src/qcodes/parameters/multi_parameter.py @@ -153,7 +153,8 @@ def __init__( kwargs.setdefault("snapshot_value", False) super().__init__( name, - **kwargs, + # see the note on ParameterBaseKWArgs + **kwargs, # ty: ignore[invalid-argument-type] ) self._meta_attrs.extend( diff --git a/src/qcodes/parameters/parameter.py b/src/qcodes/parameters/parameter.py index 47626681be8c..74feae11f0fe 100644 --- a/src/qcodes/parameters/parameter.py +++ b/src/qcodes/parameters/parameter.py @@ -10,6 +10,8 @@ from typing_extensions import TypedDict +from qcodes.utils import qcodes_abstractmethod + from .command import Command from .parameter_base import ( InstrumentTypeVar_co, @@ -286,6 +288,12 @@ class Parameter( """ + _get_raw_impl: Callable[[], ParamRawDataType] | None = None + """Implementation of ``get_raw`` generated from ``get_cmd``, if any.""" + + _set_raw_impl: Callable[[ParamRawDataType], None] | None = None + """Implementation of ``set_raw`` generated from ``set_cmd``, if any.""" + def __init__( self, name: str, @@ -354,7 +362,8 @@ def _set_manual_parameter( super().__init__( name=name, - **kwargs, + # see the note on ParameterBaseKWArgs + **kwargs, # ty: ignore[invalid-argument-type] ) no_instrument_get = not self._implements_get_raw and ( @@ -382,9 +391,9 @@ def _set_manual_parameter( " get_raw is an error." ) elif not self._implements_get_raw and get_cmd is not False: + get_raw_impl: Callable[[], ParamRawDataType] if get_cmd is None: - # ignore typeerror since mypy does not allow setting a method dynamically - self.get_raw = MethodType(_get_manual_parameter, self) # type: ignore[method-assign] + get_raw_impl = MethodType(_get_manual_parameter, self) else: if isinstance(get_cmd, str) and instrument is None: raise TypeError( @@ -396,14 +405,14 @@ def _set_manual_parameter( exec_str_ask = getattr(instrument, "ask", None) if instrument else None # TODO get_raw should also be a method here. This should probably be done by wrapping # it with MethodType like above - # ignore typeerror since mypy does not allow setting a method dynamically - self.get_raw = Command( # type: ignore[method-assign] + get_raw_impl = Command( arg_count=0, cmd=get_cmd, exec_str=exec_str_ask, ) + self._get_raw_impl = get_raw_impl self._gettable = True - self.get = self._wrap_get(self.get_raw) + self.get = self._wrap_get(get_raw_impl) if self._implements_set_raw and set_cmd not in (None, False): raise TypeError( @@ -412,9 +421,9 @@ def _set_manual_parameter( " set_raw is an error." ) elif not self._implements_set_raw and set_cmd is not False: + set_raw_impl: Callable[[ParamRawDataType], None] if set_cmd is None: - # ignore typeerror since mypy does not allow setting a method dynamically - self.set_raw = MethodType(_set_manual_parameter, self) # type: ignore[method-assign] + set_raw_impl = MethodType(_set_manual_parameter, self) else: if isinstance(set_cmd, str) and instrument is None: raise TypeError( @@ -426,14 +435,14 @@ def _set_manual_parameter( exec_str_write = ( getattr(instrument, "write", None) if instrument else None ) - # TODO get_raw should also be a method here. This should probably be done by wrapping + # TODO set_raw should also be a method here. This should probably be done by wrapping # it with MethodType like above - # ignore typeerror since mypy does not allow setting a method dynamically - self.set_raw = Command( # type: ignore[assignment] + set_raw_impl = Command( arg_count=1, cmd=set_cmd, exec_str=exec_str_write ) + self._set_raw_impl = set_raw_impl self._settable = True - self.set = self._wrap_set(self.set_raw) + self.set = self._wrap_set(set_raw_impl) self._meta_attrs.extend(["label", "unit", "vals"]) @@ -459,6 +468,31 @@ def _set_manual_parameter( self._docstring = docstring self.__doc__ = self._build__doc__() + @qcodes_abstractmethod + def get_raw(self) -> ParamRawDataType: + """ + Call the ``get_raw`` implementation generated from ``get_cmd``. + + This method stays marked as abstract so that + :attr:`~ParameterBase._implements_get_raw` keeps reporting ``False`` + for :class:`Parameter` itself: a subclass is still expected to either + override ``get_raw`` or supply a ``get_cmd``. + """ + if self._get_raw_impl is None: + raise NotImplementedError + return self._get_raw_impl() + + @qcodes_abstractmethod + def set_raw(self, value: ParamRawDataType) -> None: + """ + Call the ``set_raw`` implementation generated from ``set_cmd``. + + See :meth:`get_raw` for why this method stays marked as abstract. + """ + if self._set_raw_impl is None: + raise NotImplementedError + self._set_raw_impl(value) + def _build__doc__(self) -> str: if len(self.validators) == 0: validator_docstrings = ["* `vals` None"] @@ -521,7 +555,7 @@ def increment(self, value: ParameterDataTypeVar) -> None: """ # this method only works with parameters that support addition # however we don't currently enforce that via typing - self.set(self.get() + value) # type: ignore[operator] + self.set(self.get() + value) # type: ignore[operator] # ty: ignore[unsupported-operator] def sweep( self, diff --git a/src/qcodes/parameters/parameter_base.py b/src/qcodes/parameters/parameter_base.py index 16d96c8b08e4..ddd9dd190f08 100644 --- a/src/qcodes/parameters/parameter_base.py +++ b/src/qcodes/parameters/parameter_base.py @@ -148,6 +148,16 @@ class ParameterBaseKWArgs( ``**kwargs: Unpack[ParameterBaseKWArgs]`` as input and forward this to the super class to ensure that it can accept all the arguments defined here. + + Note that forwarding the kwargs on requires a + ``ty: ignore[invalid-argument-type]``. When a generic TypedDict declares a + PEP 696 default for a type parameter, ty computes the upper bound of the + synthesized ``Self`` as the default specialization, so every other + specialization is rejected by the members that bind ``Self``. Expanding + with ``**`` is one of those, and reports the rather misleading + ``must be a mapping type``. ``InstrumentTypeVar_co`` defaults to + ``InstrumentBase | None`` and so triggers this; ``ParameterDataTypeVar`` + defaults to ``Any``, which happens to be the one default ty accepts. """ instrument: NotRequired[InstrumentTypeVar_co] @@ -1023,7 +1033,7 @@ def set_wrapper(value: ParameterDataTypeVar, **kwargs: Any) -> None: # a list containing only `value`. # The steps are deliberately untyped: ``get_ramp_values`` works # in terms of numbers rather than the parameter's data type. - steps: Sequence[Any] = self.get_ramp_values(value, step=self.step) # type: ignore[arg-type] + steps: Sequence[Any] = self.get_ramp_values(value, step=self.step) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] for val_step in steps: # even if the final value is valid we may be generating diff --git a/src/qcodes/parameters/parameter_with_setpoints.py b/src/qcodes/parameters/parameter_with_setpoints.py index 9653be69281f..812b36f5a6ce 100644 --- a/src/qcodes/parameters/parameter_with_setpoints.py +++ b/src/qcodes/parameters/parameter_with_setpoints.py @@ -74,7 +74,8 @@ def __init__( super().__init__( name=name, - **kwargs, + # see the note on ParameterBaseKWArgs + **kwargs, # ty: ignore[invalid-argument-type] ) if setpoints is None: self.setpoints = [] diff --git a/src/qcodes/plotting/matplotlib_helpers.py b/src/qcodes/plotting/matplotlib_helpers.py index c86ade62ad69..fb6ea81cfba4 100644 --- a/src/qcodes/plotting/matplotlib_helpers.py +++ b/src/qcodes/plotting/matplotlib_helpers.py @@ -49,7 +49,7 @@ def _set_colorbar_extend( "min": slice(1, None), "max": slice(0, -1), } - colorbar._inside = _slice_dict[extend] # type: ignore[attr-defined] + colorbar._inside = _slice_dict[extend] # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def apply_color_scale_limits( diff --git a/src/qcodes/utils/abstractmethod.py b/src/qcodes/utils/abstractmethod.py index 26d0ac3611f1..ed6db0ecbf4e 100644 --- a/src/qcodes/utils/abstractmethod.py +++ b/src/qcodes/utils/abstractmethod.py @@ -16,7 +16,7 @@ def qcodes_abstractmethod[**input, output]( instantiated and we will use this property to detect if the method is abstract and should be overwritten. """ - funcobj.__qcodes_is_abstract_method__ = True # type: ignore[attr-defined] + funcobj.__qcodes_is_abstract_method__ = True # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] return funcobj diff --git a/src/qcodes/utils/types.py b/src/qcodes/utils/types.py index 01fe50e3a2eb..dc82f1692674 100644 --- a/src/qcodes/utils/types.py +++ b/src/qcodes/utils/types.py @@ -44,7 +44,7 @@ Default integer types. The size may be platform dependent. """ -numpy_ints: tuple[type, ...] = ( +numpy_ints: tuple[type[np.integer], ...] = ( numpy_concrete_ints + numpy_c_ints + numpy_non_concrete_ints_instantiable ) """ @@ -61,7 +61,7 @@ Floating point types that matches C types. """ -numpy_floats: tuple[type, ...] = numpy_concrete_floats + numpy_c_floats +numpy_floats: tuple[type[np.floating], ...] = numpy_concrete_floats + numpy_c_floats """ All numpy float types """ diff --git a/src/qcodes/validators/validators.py b/src/qcodes/validators/validators.py index bcda83550e19..24d5f38be7be 100644 --- a/src/qcodes/validators/validators.py +++ b/src/qcodes/validators/validators.py @@ -987,12 +987,12 @@ def shape_unevaluated(self) -> shape_tuple_type: def shape(self) -> tuple[int, ...] | None: if self._shape is None: return None - shape_array = [] + shape_array: list[int] = [] for s in self._shape: - if callable(s): - shape_array.append(s()) - else: + if isinstance(s, int): shape_array.append(s) + else: + shape_array.append(s()) shape = tuple(shape_array) return shape diff --git a/suppression-codes-mypy-and-ty.md b/suppression-codes-mypy-and-ty.md new file mode 100644 index 000000000000..8479b7d269c7 --- /dev/null +++ b/suppression-codes-mypy-and-ty.md @@ -0,0 +1,211 @@ +# Combining mypy and ty suppression codes + +## Summary + +The [ty suppression docs](https://docs.astral.sh/ty/suppression/) document putting +a ty rule into a mypy `type: ignore` comment by prefixing it with `ty:`: + +```python +sum_three_numbers("one", 5, 2) # type: ignore[arg-type, ty:invalid-argument-type] +``` + +ty honours this. **mypy does not ignore the `ty:` prefixed code**, and reports it +as an unused suppression when `warn_unused_ignores` is enabled, which qcodes +enables in `pyproject.toml`. So the combined form cannot be used here. + +qcodes therefore uses two comments on the same line: + +```python +f("one") # type: ignore[arg-type] # ty: ignore[invalid-argument-type] +``` + +That is the only form of the three below that all three checkers accept. + +## Results + +| form | ty 0.0.74 | mypy 2.3.1 with `warn_unused_ignores` | mypy 2.3.1 without it | pyright 1.1.411 | +| --- | --- | --- | --- | --- | +| `# type: ignore[arg-type, ty:invalid-argument-type]` | suppressed | `Unused "type: ignore[ty:invalid-argument-type]" comment` | clean | suppressed | +| `# type: ignore[arg-type]` + `# ty: ignore[invalid-argument-type]` | suppressed | clean | clean | suppressed | +| `# type: ignore[ty:invalid-argument-type]` | suppressed | unused, and `arg-type` not covered | `arg-type` not covered | suppressed | + +Note that the `arg-type` half of the combined form *is* honoured by mypy. It is +only the `ty:` prefixed code that mypy does not recognise, and therefore reports +as unused. + +## How pyright fits in + +pyright has its own suppression comment and also honours mypy's, which is why it +accepts all three forms above. + +| comment | pyright | +| --- | --- | +| `# type: ignore` | suppressed | +| `# type: ignore[arg-type]` | suppressed | +| `# type: ignore[arg-type, ty:invalid-argument-type]` | suppressed | +| `# pyright: ignore` | suppressed | +| `# pyright: ignore[reportArgumentType]` | suppressed | +| `# pyright: ignore[reportGeneralTypeIssues]` | **not** suppressed, wrong rule | +| `# ty: ignore[invalid-argument-type]` | **not** suppressed | + +Two things follow from this. + +**`# type: ignore` is a blanket suppression for pyright.** pyright does not parse +the codes in it, so `# type: ignore[arg-type]` silences *every* pyright rule on +that line, not just the argument type one. A consequence that came up repeatedly +during the ty migration: removing a mypy suppression can surface a pyright error +on the same line that was never visible before. `# pyright: ignore[rule]` is the +precise form, and unlike `# type: ignore` it only suppresses the rules listed. + +**A ty only suppression does not silence pyright.** `# ty: ignore[...]` is just a +comment as far as pyright is concerned. That is what makes the two comment form +safe: the mypy half keeps pyright quiet as a side effect, and the ty half is +inert for both of the others. + +## Unused suppression detection + +The three checkers differ in whether they tell you a suppression has gone stale. + +| checker | setting | default | reports unused | +| --- | --- | --- | --- | +| mypy | `warn_unused_ignores` | off | enabled in `pyproject.toml` | +| ty | `unused-ignore-comment` | on | yes, for `ty: ignore` directives | +| pyright | `reportUnnecessaryTypeIgnoreComment` | off | not enabled, see below | + +With the pyright setting enabled it reports all of these: + +```python +def g(a: int) -> None: ... + + +g(1) # type: ignore +g(1) # pyright: ignore +g(1) # pyright: ignore[reportArgumentType] +``` + +``` +Unnecessary "# type: ignore" comment +Unnecessary "# type: ignore" comment +Unnecessary "# pyright: ignore" rule: "reportArgumentType" +``` + +**We cannot enable it while we also run mypy.** Because pyright treats +`# type: ignore` as a blanket suppression of *its own* rules, it calls the +comment unnecessary whenever pyright itself has nothing to report on the line, +with no knowledge of whether mypy needed it. Every mypy only suppression in the +code base would be reported as unnecessary. For example: + +```python +from typing import Any + + +class A: + def m(self) -> None: ... + + +def make(a: A, replacement: Any) -> None: + # mypy reports method-assign here, pyright has no equivalent check + a.m = replacement # type: ignore[method-assign] +``` + +mypy needs that suppression: removing it gives +`error: Cannot assign to a method [method-assign]`. pyright with +`reportUnnecessaryTypeIgnoreComment` enabled reports the very same line as +`Unnecessary "# type: ignore" comment`. + +So mypy's `warn_unused_ignores` and ty's `unused-ignore-comment` are the two +stale suppression checks we can actually rely on. + +## Test case + +```python +def f(a: int) -> None: ... + + +# 1. combined form from the ty docs +f("one") # type: ignore[arg-type, ty:invalid-argument-type] + +# 2. the two comment form used in qcodes +f("one") # type: ignore[arg-type] # ty: ignore[invalid-argument-type] + +# 3. combined form, ty rule only +f("one") # type: ignore[ty:invalid-argument-type] + +# 4. control, expected to be reported by every checker +f("one") +``` + +And for the pyright specific forms: + +```python +def h(a: int) -> None: ... + + +# 5. pyright: ignore, blanket +h("one") # pyright: ignore + +# 6. pyright: ignore with the matching rule +h("one") # pyright: ignore[reportArgumentType] + +# 7. pyright: ignore with a non matching rule +h("one") # pyright: ignore[reportGeneralTypeIssues] + +# 8. ty: ignore only +h("one") # ty: ignore[invalid-argument-type] +``` + +Run from the repository root so that the mypy configuration in `pyproject.toml` +is picked up: + +``` +uv run ty check --output-format concise +uv run --extra test mypy +uv run --extra test mypy --no-warn-unused-ignores +uv run pyright +``` + +Expected results: + +| block | ty | mypy | pyright | +| --- | --- | --- | --- | +| first, cases 1 to 4 | 4 | 1, 3, 4 and two unused directives | 4 | +| second, cases 5 to 8 | 5, 6, 7 | 5, 6, 7, 8 | 7, 8 | + +The second block deliberately exercises comments that only one checker +understands, so most cases are reported by the other two. That is the point: it +shows that `pyright: ignore` is inert for mypy and ty, and that `ty: ignore` is +inert for mypy and pyright. + +Any deviation from this table tells you that one of the checkers has changed how +it reads these comments. + +## Why we keep `warn_unused_ignores` + +Dropping `warn_unused_ignores` would make the combined form work, but that +setting is worth more than the shorter comments. As shown above it is, together +with ty's `unused-ignore-comment`, one of only two stale suppression checks +available to us. During the ty migration it caught: + +- the `issuperset` suppression becoming redundant once + [astral-sh/ty#4303](https://github.com/astral-sh/ty/issues/4303) was fixed in + ty 0.0.74 +- the two suppressions in the Keithley 7510 buffer becoming unnecessary once the + data dictionary was annotated +- several suppressions in `ParameterBase` becoming unnecessary once the duck + typed conversions were moved behind helpers + +## Suggested upstream change + +mypy could ignore codes carrying a `:` prefix in `type: ignore` comments, +rather than treating them as mypy codes that turned out to be unused. That would +make the form documented by ty usable in projects that run both checkers with +`warn_unused_ignores` enabled, and would generalise to any other checker that +wants to share the comment. + +Failing that, the ty documentation could note that the combined form conflicts +with mypy's `warn_unused_ignores`, and suggest the two comment form for projects +that run both. + +## Versions + +Measured with ty 0.0.74, mypy 2.3.1 and pyright 1.1.411. diff --git a/tests/dataset/test_snapshot.py b/tests/dataset/test_snapshot.py index b723033574e0..1a72ec0c7e1d 100644 --- a/tests/dataset/test_snapshot.py +++ b/tests/dataset/test_snapshot.py @@ -68,9 +68,7 @@ def test_station_snapshot_during_measurement( assert expected_snapshot == snapshot_from_dataset # 2. Test `snapshot_raw` property - # this is not part of the DatasetProtocol interface - # but we test it anyway - assert json_snapshot_from_dataset == data_saver.dataset.snapshot_raw # type: ignore[attr-defined] + assert json_snapshot_from_dataset == data_saver.dataset.snapshot_raw # 3. Test `snapshot` property diff --git a/tests/test_instrument.py b/tests/test_instrument.py index b2ba6dec987b..3b77fed4569c 100644 --- a/tests/test_instrument.py +++ b/tests/test_instrument.py @@ -212,8 +212,10 @@ def test_attr_access(testdummy: DummyInstrument) -> None: def test_parameter_property(testdummy: DummyInstrument) -> None: # since this is added dynamically we cannot know the type statically assert_type(testdummy.dac1, Any) - # this is an assigned attribute so we know it statically - assert_type(testdummy.fixed_parameter, Parameter) + # this is an assigned attribute so we know it statically. Without an + # explicit ``parameter_class`` the data and instrument types of the + # returned parameter are unknown, hence ``Parameter[Any, Any]``. + assert_type(testdummy.fixed_parameter, Parameter[Any, Any]) assert testdummy.fixed_parameter.get() == 5 testdummy.fixed_parameter.set(10) diff --git a/ty-issue-1-typeddict-self-bound.md b/ty-issue-1-typeddict-self-bound.md new file mode 100644 index 000000000000..70ea4ed48587 --- /dev/null +++ b/ty-issue-1-typeddict-self-bound.md @@ -0,0 +1,195 @@ +# ty issue draft 1 + +**Title** + +> Generic `TypedDict` with a type parameter default: `Self`-bound methods and `**`-unpacking rejected for every non-default specialization + +**Labels to suggest:** `bug`, `generics`, `typeddict`, `constraint-solver` + +--- + +### Summary + +When a generic `TypedDict` declares a default for its type parameter, ty computes +the upper bound of the synthesized `Self` type variable as the *default* +specialization rather than the generic one. Every other specialization is then +rejected by any method that binds `Self`. + +```python +from typing import TypedDict + + +class Movie[T = int](TypedDict): + extra: T + + +def f(m: Movie[str]) -> None: + m.keys() +``` + +``` +error[invalid-argument-type]: Argument to bound method `TypedDictFallback.keys` is incorrect + --> repro.py:7:5 + | +7 | m.keys() + | ^^^^^^^^ Argument type `Movie[str]` does not satisfy upper bound `Movie[int]` of type variable `Self` +``` + +Removing the default (`class Movie[T](TypedDict)`) makes the error go away +without any other change, so the default is what introduces the bound. + +Note that `Movie[str]` here is an ordinary concrete specialization. No type +variable is unsolved at the call site, and nothing is being inferred. + +### Which specializations are affected + +Only the declared default is accepted: + +| annotation | result | +| --- | --- | +| `Movie[int]` (the default) | ok | +| `Movie` (bare, default applies) | ok | +| `Movie[str]` | error | +| `Movie[T]` for an enclosing type variable `T` | error | + +### Which members are affected + +Members whose signature binds `Self`: + +| member | result | +| --- | --- | +| `keys()` | error | +| `values()` | error | +| `items()` | error | +| `copy()` | error | +| `**` unpacking | error | +| `get()` | ok | +| `setdefault()` | ok | +| `pop()` | ok | +| `update()` | ok | + +Assignability is unaffected, which is consistent with the problem being the +`Self` bound rather than the type itself: + +```python +from typing import Mapping, TypedDict + + +class Movie[T = int](TypedDict): + extra: T + + +def f(m: Movie[str]) -> None: + ok: Mapping[str, object] = m # no error +``` + +### The `**` unpacking symptom + +`**`-unpacking reports a different and rather misleading message, which is how I +originally ran into this: + +```python +from typing import TypedDict + + +class Movie[T = int](TypedDict): + extra: T + + +def f(m: Movie[str]) -> None: + dict(**m) +``` + +``` +error[invalid-argument-type]: Argument expression after ** must be a mapping type + --> repro.py:7:12 + | +7 | dict(**m) + | ^ Found `Movie[str]` +``` + +A `TypedDict` is always a `Mapping[str, object]`, so this message points away +from the real cause. + +### Not specific to `TypedDict` syntax or version + +The legacy spelling behaves identically: + +```python +from typing import Generic, TypedDict, TypeVar + +T = TypeVar("T", default=int) + + +class Movie(TypedDict, Generic[T]): + extra: T + + +def f(m: Movie[str]) -> None: + m.copy() +``` + +A plain generic class with a type parameter default is **not** affected, so this +looks specific to the synthesized `TypedDictFallback` `Self`: + +```python +class WithDefault[T = int]: + def m(self) -> None: ... + + +def f[T](a: WithDefault[T]) -> None: + a.m() # no error +``` + +Any non-`Any` default triggers it. `Any` is the only default that is accepted, +which is probably why this has gone unnoticed: + +| type parameter | result | +| --- | --- | +| `class Movie[T](TypedDict)` | ok | +| `class Movie[T = Any](TypedDict)` | ok | +| `class Movie[T = int](TypedDict)` | error | +| `class Movie[T = None](TypedDict)` | error | +| `class Movie[T = int \| None](TypedDict)` | error | +| `class Movie[T = object](TypedDict)` | error | +| `class Movie[T: int \| None = int \| None](TypedDict)` | error | + +Reproduced on 0.0.72, 0.0.73 and 0.0.74. Checked with `--python-version 3.13` so +that the PEP 696 syntax is not itself reported as an error. mypy 2.3.1 and +pyright both accept all of the above. + +I searched existing issues for `"must be a mapping type"`, `TypedDict Unpack +default`, `"generic TypedDict"`, `"PEP 696"` and `Unpack kwargs` and did not +find a preexisting issue. #4255 is the closest but is about a union alias in a +stub leaking an unspecialized type variable. + +The error shape is reminiscent of #4303, which is also an upper bound on a type +variable being applied too strictly, though that one is about a `bound=` on a +class scoped type variable rather than a `default=` on `Self`. + +### Relation to the feature overview + +The type system feature overview in #1889 lists all of the following as +implemented: + +- Generics: `TypeVar` defaults (PEP 696) +- `TypedDict`: Inheritance, generic `TypedDict`s +- `TypedDict`: Structural assignability and equivalence +- `TypedDict`: Methods (`get`, `pop`, `setdefault`, `keys`, `values`, `copy`) + +This report sits at the intersection of those, so following the guidance at the +top of #1889 for features marked completed, it seemed worth reporting rather +than upvoting a tracking issue. + +It is worth stressing that this is **not** about `Unpack` for `**kwargs` typing, +which #1889 tracks separately in #1746. The lead repro contains no `Unpack` and +no `**` at all, just `Movie[str].keys()`. The `**` message is only how I +happened to notice it. + +Structural assignability also still works (`Mapping[str, object] = m` is +accepted), so this looks narrowly scoped to the upper bound computed for the +synthesized `Self`. + +### Version + +0.0.74 diff --git a/ty-issue-2-typevar-default-context.md b/ty-issue-2-typevar-default-context.md new file mode 100644 index 000000000000..62eec02d3d0c --- /dev/null +++ b/ty-issue-2-typevar-default-context.md @@ -0,0 +1,142 @@ +# ty issue draft 2 + +**Title** + +> Function scoped `TypeVar` default takes precedence over the declared type context, where an unsolved type variable would be accepted + +**Labels to suggest:** `bidirectional inference`, `constraint-solver`, `generics` + +--- + +### Summary + +When a function scoped type variable appears only in the return type and is not +constrained by any argument, ty leaves it unsolved as `Unknown`, which is +gradually compatible with whatever the result is assigned to. If that same type +variable declares a PEP 696 default, ty substitutes the default instead, which +is concrete and then conflicts with the declared type. + +```python +class Box[T]: + pass + + +def make[T = int](cls: type[T] | None = None) -> Box[T]: + raise NotImplementedError + + +def caller() -> None: + a: Box[str] = make() +``` + +``` +error[invalid-assignment]: Object of type `Box[int]` is not assignable to `Box[str]` + --> repro.py:8:19 + | +8 | a: Box[str] = make() + | ^^^^^^ +``` + +Removing the default makes ty accept it: + +```python +class Box[T]: + pass + + +def make[T](cls: type[T] | None = None) -> Box[T]: + raise NotImplementedError + + +def caller() -> None: + a: Box[str] = make() # ty: ok +``` + +`reveal_type` shows what is actually happening. The declared type is never used +to solve `T` in either case; the difference is only what fills the unsolved slot: + +| declaration | `reveal_type(make())` | `a: Box[str] = make()` | +| --- | --- | --- | +| `def make[T](...) -> Box[T]` | `Box[Unknown]` | accepted | +| `def make[T = int](...) -> Box[T]` | `Box[int]` | error | + +So adding a default is strictly worse than having no default at all, at every +call site that annotates its target. mypy 2.3.1 and pyright accept both forms. + +### The type context is available + +This is not a case of ty lacking the necessary context. Using the example from +#3933, the declared type of the assignment target clearly does reach the +constraint solver, since it widens the argument: + +```python +class Parent: ... + + +class Child(Parent): ... + + +def head[T](x: list[T]) -> T: + return x[0] + + +x: Parent = head(reveal_type([Child()])) # revealed: list[Parent] +``` + +I reproduced that on 0.0.74. So in `a: Box[str] = make()` the constraint +`Box[T] <: Box[str]` is available, but the default is applied in preference to +it. + +### Why this matters + +This pattern is common in factory functions, where the default exists to give a +sensible type to an unannotated call while still allowing the caller to ask for +something more specific (illustrative, from our codebase): + +```python +p = instrument.add_parameter("name") # want the default +q: Parameter[float, Self] = instrument.add_parameter("x") # want this instead +``` + +With ty's current behaviour the default wins in both cases, so the second form +is unusable and every annotated call site becomes an error. In our codebase this +produced 34 errors across instrument drivers from a single type variable +declaration. We ended up widening the default to a fully gradual type to work +around it, which loses the information the default was there to provide. + +### Relation to #3933 and the feature overview + +This looks like it may fall under #3933, constraint-set-aware bidirectional +inference. That issue is written in terms of constraints flowing into *argument* +inference, and all of its examples involve arguments that get eagerly +specialized or wrongly widened. The case here has no arguments at all, so the +symptom is different, but the underlying gap looks similar: the outer constraint +is not being unified with the specialization of the call. + +If the second approach in #3933 is taken, propagating constraints during +bidirectional inference rather than eagerly specializing, then `Box[T] <: +Box[str]` should presumably solve `T` to `str` before any default is considered, +which would fix this too. Filing separately in case that is not the intent, and +because the interaction with PEP 696 defaults is not mentioned there. + +The type system feature overview in #1889 lists "`TypeVar` defaults (PEP 696)" +as implemented under Generics. That section also has an open sub-item, "Solve +type variables in all cases" (#623), which may be the more appropriate home if +this is considered a solver limitation rather than a deliberate choice about +defaults. + +### Note on the spec + +I could not find wording in PEP 696 or the typing spec that settles whether the +declared type context should take precedence over a type variable default, so +this may be intentional. If it is, it would be helpful to say so explicitly, +since the natural reading of "the default is used when the type variable cannot +be solved" is that a solution derived from the type context counts as solving +it. The current behaviour also has the surprising property that adding a default +makes a call site fail that would otherwise have been accepted. + +Reproduced on 0.0.72, 0.0.73 and 0.0.74, checked with `--python-version 3.13`. + +### Version + +0.0.74