diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index c49f9286e625..0125cb8fc94a 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 + 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..fc64d898b71e --- /dev/null +++ b/docs/changes/newsfragments/8393.underthehood @@ -0,0 +1,5 @@ +The type checker ``ty`` is now a test dependency and is run as part of the +CI job that also runs ``mypy`` and ``pyright``. Unlike the other type +checkers ``ty`` understands Jupyter notebooks, so this type checks the +example notebooks in ``docs`` in addition to the source code in ``src`` +and the test suite. diff --git a/docs/changes/newsfragments/8394.improved b/docs/changes/newsfragments/8394.improved new file mode 100644 index 000000000000..1093e7f909c9 --- /dev/null +++ b/docs/changes/newsfragments/8394.improved @@ -0,0 +1,5 @@ +The annotation of ``qcodes.utils.types.numpy_complex`` now correctly +describes its content as a tuple of numpy complex types. It was previously +annotated as also containing the builtin ``complex``, which via the implicit +numeric tower made type checkers treat it as possibly containing ``int`` or +``float`` as well. diff --git a/docs/changes/newsfragments/8395.improved b/docs/changes/newsfragments/8395.improved new file mode 100644 index 000000000000..bec83574a152 --- /dev/null +++ b/docs/changes/newsfragments/8395.improved @@ -0,0 +1,3 @@ +The module level ports used by ``qcodes.monitor`` are now annotated as +``int``. They are intended to be reassigned to select another port, which +type checkers rejected since the type was inferred from the default value. diff --git a/docs/changes/newsfragments/8396.underthehood b/docs/changes/newsfragments/8396.underthehood new file mode 100644 index 000000000000..75e79dce770c --- /dev/null +++ b/docs/changes/newsfragments/8396.underthehood @@ -0,0 +1,4 @@ +The suppressions needed to forward ``**kwargs: Unpack[ParameterBaseKWArgs]`` +to ``ParameterBase`` have been removed along with the note documenting them. +They worked around a bug in ``ty`` that is fixed in 0.0.77, which is now the +minimum version used to type check QCoDeS. diff --git a/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": "iVBORw0KGgoAAAANSUhEUgAAAY0AAAEWCAYAAACaBstRAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8QVMy6AAAACXBIWXMAAAsTAAALEwEAmpwYAAA60ElEQVR4nO29eZxlVXnv/f3Vqam7q+hquhtoemRGjMrQosZcAaMGEMG8+t4Lor56URIDDjeaiDcRjSZRo0TNFaMtEuRGIUYUwYAoyqAiQgMtg4g2TTc9Qc9DzXXqPO8fexecPmvtql1dp845Vf1867M/dc7aa+31PHvvs5+9hmc9MjMcx3EcJw9N9RbAcRzHmTq40XAcx3Fy40bDcRzHyY0bDcdxHCc3bjQcx3Gc3LjRcBzHcXLjRsOZUkhaIqlbUqHeskwlJF0n6Y0Z+5ZJMknNNZbpvZI+U8s6nYnjRmOSkLRWUl/6gHtG0jWSOmpQ70ZJMyS9WtJ3K/Z9UtIjkoqSPj7O494pqT/VZ2S7uarC58DMnjazDjMbrnXd6YP16FrXO1EkvRh4CfD9estSwdeACyUdUm9BnPy40Zhc3mBmHcCJwEnARyazMkmLge1m1gecAjxYkWU18NfAf+1nFZemD+yR7Q0TEHfc1PpNeDJRQq1+f38GfNP2w5N3MuU0s37gVuDtk3F8Z3Jwo1EDzOwZ4DYS44Gk0yVtKM+Ttkxek37+uKRvS7pW0l5Jj0lanqOq5cADZZ/3MRpm9g0zuxXYOzGN9kXShyX9auShLuk9qcztZV0fF0vaJGmzpA+VlW2SdJmkJyVtT/U+ON03UvYiSU8DP63sSklbQH8v6Z6R1o+kuZK+KWmPpPslLSur73hJP5a0Q9ITkv572b5rJF0p6b/S8/4rSUel++5Os/06red/RM5DQdIVkrZJekrSpRFZ/0HSL4Be4Mgx5JmRHm+dpN2Sfi5pRrrv3PQc70qP+4JRLtFZwF0Vcn4ulXMN8PoKPWJyvlPS4+l5WSPpzyrKnCdpVXrOn5R0Zpp+uKSbUv1WS3p3hWx3VtbvNDhm5tskbMBa4DXp50XAI8AX0++nAxtGyf9xoB84GygAnwLuHaWujwG70jK96edhYHf6uVCR/9+Bj49TnzuBd2XsawLuTuU+BtgJnJTuWwYYcB0wC3gRsLVM1/cD96bnqA34KnBdRdlr07IzytKay+RaDRwFzAZ+A/wOeA3QnJb9tzTvLGA98M5030nANuCEdP81wHbg1HT/N4Hry/Q04OhRztGfp/UvAuYAt0dkfRp4YXr82WPIc2VaZmF6H/xheo6OBXqA1wItJK3H1UBrRKZZqQzzK+T8LbAYOBi4Yww5W0ge7EcBAk4juc9OTvOfSnKvvZbkXlgIHJ/uuxv4MtBO8tK0FXh1mSwnAzvq/Xv1bRzPgnoLMF03EiPQTfJWb8BPgK503+mMbTRuL9t3AtA3Rn3NwOPAoenD5b9Gybu/RmPEII1snyzbvwzYkcrwkYp0G3mIpGn/BHw9/fw48Mdl+xYAQ6k+I2WPjByv/AH3N2X7rwBuLfv+BmBV+vl/AD+r0OurwMfSz9cAV5XtOxv4bdn3sYzGT4E/K/v+moisnyjbnykPycO3D3hJpJ6PAt8u+94EbAROj+RdmMrQXiHnn5d9f91ocmboeiPw/jKZPx/Js5jk5aWzLO1TwDVl348Bhifzt+hbdTfvnppc3mhmnSRG4nhg3jjKPlP2uRdoj/XpSzpR0i6St/ujgSdI3hxPT7su/p/9lD3G+8ysq2z76MgOM1ub1ruM5A25kvVln9cBh6eflwLfS2XdRWJEhkmMX6xsjGfLPvdFvo9MQFgKvGykrrS+C4HDyvJXnvfxTF44vELWmNzlaaPJM4/k7fzJjHrWjXwxs1J63IWRvLvS/52jyLmOkH1kl3SWpHvTbqZdJAZ15H5ePIqcO8ysvDt0XYWcnSStFGeK4EajBpjZXSRvsZ9Lk3qAmSP7lUwfnb+fx15lZl3APwCXp59/Q/KG2mVm3x2tfLWQ9HrgFSQtqs9Gsiwu+7wE2JR+Xg+cVWGM2s1sY1n+ai3FvB64q6KuDjN7T5WOv5mka2qExZE85bqMJs82ku7GoyLH2ERicIBksDqta2NlRjPrIXmgH1shZ+X1yJRTUhtwA8n9e2h6j91C0lU1okeWnAdLKjdYSyrkfAHw60hZp0Fxo1E7vgC8VtJLSPrc2yW9XlIL8LckfdUT4RTgQUmtwOFmtroyg6QWSe0k1705HagupPtGBpiXjbdiSfOAq4B3Af8f8AZJZ1dk+6ikmZJeSNKH/x9p+leAf5C0ND3WfEnnjVeGnPwAOFbS29Jz0SLppWMMIpfzLHDkKPu/Dbxf0kJJXcCH91eetPVwNfDP6WByQdIr0gf4t4HXS/rj9P75IDAA3JNRzy0k4xDlcr5P0iJJc4DLxpCzleT+3AoUJZ1F0qU1wteBd6byNKX6H29m61OZPpXeay8GLiLpHh3hNJIZVM4UwY1GjTCzrSSDspeb2W7gL0getBtJWh4bRimeh5Epti8CHs3I8zWS7poLgL9JP78t3beYpOsgeFst40va109jZKbWCuD7ZnaLmW0neTBcJWluWdm7SAZrfwJ8zsx+lKZ/EbgJ+JGkvSSD4i/Lq/R4SLtJXgecT/IW/AzwGfIb7I8D30i7kv57ZP/XgB8BDwMPkTysiyTdbfsjz4dIJlDcTzJe9BmgycyeAN4K/B+SFskbSKZ3D2bIvYLEH2KkZfA1ktl8vya5Z0ZtjaZyvo/E2OwE3kJyzUb230fyIvB5kq6mu3i+JXQBSZflJuB7JONHtwOkLzBnA98YrX6nsZCZB2FyQNLfAlvN7KtVPu4y4CmgxcyK1Tx2o5O+kX/FzJaOmXnyZfkWyeD5jfWWZQRJ7wUWm9lf11sWJz9uNJxJ5UAyGqkPxRkkrY1DScYB7jWzD9RTLsepJt495TjVQ8DfkXThPEQyE+zyukrkOFXGWxqO4zhObryl4TiO4+Rm2iwAV0l7V7t1LNjXL2tv74wwY1O8pXXIrD1B2qGFcHLKzlJ8he4NvV1h4lBoo9tmxCe8LGvfEaTpuWnx+7JuIKyrrzcyIai5FC1/+KzQt2puUzj8sHW4JVr+mZ7ZYeJwKOusmQPR8kvadoXFIy3gtQNzgzSAwb5QLrWEui6euTNafnZTmHdzsT2ad1tPxNfPQl0PmtUbLb+4pTtI64/p2hfXdXgg/MkW2sLJWUtmbI+WnxW5hTYMzQrSdvXMDDMCsVvw4Fk90ayHN/cFad2Rn9vTvQdHy5cGw99WS3t4Xy5rj+vaEpF1/eBB0bzbf7t9m5ntl6/UCH9yxizbviPf4ssPPDxwm5mdOZH66sW0NRodCzp4/Tf2ne5/18rIdPxZ8Yv8/lN/EqbNWRuk3dDTGaQBXLYydMQubQkfREe/KD7T9upjrg/SWjKMxrvXvClIe3TVsiBNc+MP7Y+99AdB2ts6twVpX9u9IFr+U/edFda1O3yQLz85cB0B4CvLwhXWd5fC6/KOJ94aLb/hscOCtOaF4UP7ipP+M1r+9TP7g7RPbT82khNW/OpVQZoGw5eBM18a91f7/OE/D9IeHwp1fdej8YVfd62eE6R1HBEa/a+8+N+DNIBTWsO0y549NUj73n2nRMtbpG/iraf+Mpr37w55OEi7uz80BJc89JZo+YH1oYE+5Ljwvrz6hGuj5RcVwrreu+G10bzXvuzqmFf8uNi+Y5j7bov5SYYUFvx+PKtDNBR1756SdLWkLZKivgVKVoTdna6guUqSDyw6jtNwGFDK+TeVaYSWxjXAl0gc37L4mZmdUxtxHMdxxo9hDNU+NljNqbvRMLO792fpCsdxnEZjqrci8lD37qmcvELSryXdmq5dFEVJoJ+Vklb27wr7qR3HcSYLwxi2fNtUpu4tjRw8CCw1s+50EbwbSdbgDzCzFSTr7DDvBfOm9pVxHGfKUaragsyNS8O3NMxsj5l1p59vAVrSVVUdx3EaBgOGsVzbVKbhWxqSDgOeNTOTdCqJoYtPzHYcx6kjB0JLo+5GQ9J1JJHt5knaQBLqsgXAzL4CvBl4j6QiyVLe55uvfeI4ToNhwNAB8Giqu9EwswvG2P8lkim542LYmuguVnhFxzrj4v5ybBsKHYueLIbevJuG8jnzQNwxamg47lG+NuKl26r4dL7+4fAyWiG8eTNU5dmh0KN7bfGpSL7jo+WlsK5Y/TE5AdYUw/S9pVD/rHMVO6+xF75NQ6FjHMCTxd8HabHrD6DYCgKRtL0ZHuWri0NB2vpi2Ns6XIpfrZiuw6Uwcf1Q3KO8q2lTkLZrKFwpIXpOIarrzmLce/zJodDBcv3QsrCuiEd9lgyx87K2GL+uQxY6PQbPhCpi06DrKQ91NxqO4zjTAoPh6W8z3Gg4juNUg8QjfPrjRsNxHKcqiOHMTuDpgxsNx3GcKpAMhLvRcBzHcXKQ+Gm40XAcx3FyUvKWhuM4jpMHb2lMcUqI3uK+gYAKXWGUvEJL3Pfh6b4wmtgt3ScEaU/0hgGAANrbw/n4fXPCG6qUcZPdEamroPjcjGJknn5TRNe2GaFMAKt7DwnSbm4K14V8KiOaXNvM8LiD4/DTuD2i60ApDOLUFPEHAWBORNfWMMLboz2LosV7S+Hc/Wf64xHeWjvCuoaLof/InsG4n8Zt3eF53TYUBvJqLWQssR3TtSXU9YGeZdHiMV+VXYOhn0VzZzyiZFPET2NrfzwQ2S3dfxCkPT0Q/q7aWuP35UBE10Kk/l92R5eio7MQLlpa+UyoJoYYbvyVmSbMtDUajuM4tca7pxzHcZxcGGLQ4qsWTCfcaDiO41SBxLnPu6ccx3GcnPhAuOM4jpMLMzGcudLj9GH6a+g4jlMjSijXNhqSFku6Q9JvJD0m6f01Ej8X3tJwHMepAslAeFUeqUXgg2b2oKRO4AFJPzaz31Tj4BPFjYbjOE4VqNZAuJltBjann/dKehxYCDSE0dB0DYLXedxhdsqX31pvMRzHmQLc9ZorHjCz5RM5xtEvmmn/dONxufK+6ehVueqTtAy4G/gDM9szEfmqRc3GNCRdLWmLpEcz9l8o6WFJj0i6R9JLyvatTdNXSVpZK5kdx3HyMuIRnmcjCW+9smy7uPJ4kjqAG4APNIrBgNp2T11DErb12oz9TwGnmdlOSWcBK4CXle0/w8y2Ta6IjuM4+08p/+ypbaO1NCS1kBiMb5rZd6shW7WomdEws7vTplbW/nvKvt4LxBcKchzHaUCSBQsn3nkjScDXgcfN7J8nfMAq06hTbi8Cbi37bsCPJD0Qa8aNIOnikebe0K4wqL3jOM5kYYghK+TaxuCVwNuAV6dd8qsknT35GuSj4WZPSTqDxGj8UVnyH5nZRkmHAD+W9Fszu7uyrJmtIOnWovO4w6bnCL/jOA2JGVVx7jOzn0PjupY3VEtD0ouBq4DzzGz7SLqZbUz/bwG+B5xaHwkdx3GyyOfYN5ZzX6PTMEZD0hLgu8DbzOx3ZemzUgcXJM0CXgdEZ2A5juPUCyNpaeTZpjI1656SdB1wOslUsw3Ax4AWADP7CnA5MBf4cjIORDGdXXAo8L00rRn4lpn9sFZyO47j5MWDMFURM7tgjP3vAt4VSV8DvCQsMTolE71DrfukveLQp4J8PcNh1DaAX6w/Ikjr2xaJcDZ7IFr+pUueDtIWztgdpK3ctjhafu2G+WFixijNokXbg7SXz18XpD2bEWHtV+uXBmmDO8PIc21z+6LlX74orOvg1p4g7d4ty6LlN28Mo7kRifx31OJno+VPOXh9kLauN4wyuPLp+Lku7m0N0jrmh/IDvHJheA/NKIQR5n7+zJHR8ts2zQ7SmtrCKH0nLNkcLf/C2WH6E3sPDdIeXrcwWr7UF/7k5ywIXQD+24I10fJDkbfkezaFvxWA3c+G91thVhil76SlG6Llj5q1NUj79a5QryfWxaNnWjGU9dDDd0XzVgNDHoTJcRzHyYcBQ9VZe6qhmf4aOo7j1AR5PA3HcRwnH8a4PMKnLG40HMdxqoS3NBzHcZxcmMlbGo7jOE4+koHwMZcImfK40XAcx6kKB0aM8GlrNMzEQHFfq3/CzE1Bvs1DXdHyfd3HB2ntm8LT1Z9R/7yIn8KLZob+BPexJFq++ZnQd0CleF3Dh4c36gkzNkZyxufuD+wJfVVmbIzo2hr3aVnQHvqfHN0e+lTcPXxUtHzr5pYgrdQS+mk0LYk7qsTO666h0KdmeGdc/hnPhm+HfbPC8w+wdEboEzO7EC6O+eOBeDCe9k2hrkOd4bluP7IYLR/TdX3fnDDjjrj87bvCe6Xv4FCmI2eEPhIQf5O+vS9L11Cvgblh/R1HxX2dYro+tntBkFbYFte10BuOLwwcMnktgWQg3Mc0HMdxnJy4R7jjOI6TC/cIdxzHccZFyVsajuM4Th7MYKjkRsNxHMfJQdI95UbDcRzHyYl7hDuO4zi5OFCm3E7/tpTjOE5NSLqn8mxjHkm6WtIWSdEopZJOl7Rb0qp0u7zq6mQwbVsaZjA4tK96LQqD3TRlRDaygdAJqCWMVcPgnPx2t11hAJqh4bizUUskBlCWc99g5BjtTWFdWagvouveMF9/JB9AgVCwtpiuxXj55u4wrdQavrFlnauYrrE4zE198WsVu679kesP8XsoljY0FP9ptUXOqzWFshYzBlRjuhZLoazNPfl17R0IZW1R3LkwRjFSHmBGpK7hGaGuWW/nUV0jD9zmnnj55shvqPKZUG2qGP/7GuBLwLWj5PmZmZ1TrQrzMmVaGmNZXsdxnHqSzJ4q5NrGPpbdDeyYfKnHz5QxGiSW98x6C+E4jhNjxLkvzwbMk7SybLt4P6p8haRfS7pV0gurrE4mU6Z7yszulrSs3nI4juNkMY7uqW1mtnwCVT0ILDWzbklnAzcCx0zgeLmZSi2NMZF08YjlLu4JF5FzHMeZLEZmT+VsaUysLrM9Ztadfr4FaJE0b8IHzsG0MhpmtsLMlpvZ8uaDwlVOHcdxJpNqzZ4aC0mHSVL6+VSSZ3m4BPMkMGW6pxzHcRoZM0Vnd+0Pkq4DTicZ+9gAfAxoSeqxrwBvBt4jqQj0AeebWXwqaJVxo+E4jlMlquXcZ2YXjLH/SyRTcmvOlOmeSi3vL4HjJG2QdFG9ZXIcxxmhlmMa9WTKtDTGsrxhfjE0tO986KcG5gf5nh04KFo+5kgXDf+bcQM80x8ed3XboUFa72AYNQ1gPK3cvoHwGKv7w7o2R2SCceia4Vy4aWB2kNbSFDq8DWQ4VhUiusb03zsYj7y3eiDUdWt/R5CmjMZ7TFcbjl/X9f0HB2k7mmcFacPF+AWMntdIVTsHZkTLPzVwSFh/Rt689Zcisj49MDdaPhoDuxQ/VzncEQDYNhCeP4hf1z0D7fkOSvweqnwmVJupbhDyMGWMhuM4TiPjQZgcx3GccVHFZUQaFjcajuM4VcAse82w6YQbDcdxnCrh3VOO4zhOLnxMw3EcxxkX5kbDcRzHyYsPhE9hzMRwxZzse7YdEeTrL+b3kxiYE6aVWuLOC2t2h/P59wyFfga9fXHfg1Jn6FSQFYTJIn4av9h+ZJC2sy++Hpe1hHXFdKU57ujw+I7DgrQNbV1B2lBGsJ7hiPuIRera2xufo3/n1mODtGe7O4O0Umtc/oE5kR96xm//oR0Lg7SYT0rM9wFgoCtMK7WHcm3bG/dduKMl1HXL3lDX4Znxm2Uw5s8beTu+b9vSaPnoQzHD/2Uwcg8NR3TdtCfuP3R3ZNHWnd3hPTzUERdguC2UtfKZUE3MfEzDcRzHyY0Y9tlTjuM4Tl58TMNxHMfJxcjaU9MdNxqO4zjVwJJxjemOGw3HcZwq4bOnHMdxnFyYD4Q7juM448G7pxzHcZzc+OypqYyBVThYPfl0GNQlC0Wc9oYOH8xdfvuW0GFpO3EnpijzhvLn7Q8v4xNrF+QvPyN0ThuKpGXxzDNdYVr+2ikdmu+8Dne3RtOf6M6pa2cxmjwU+sZl9kxv2BgPTpSX4mH5dO3dHQ+s9HhGesDsuK6lMF5W1Dlv7YYwYFkmGSdraEE+XXdvDwNmjZYecHD8txJ1b8xwuqwGZtUzGpKuBs4BtpjZH1TloFWiZh1wks6U9ISk1ZIui+z/vKRV6fY7SbvK9g2X7bupVjI7juOMhyqGe70GOHNypd0/atLSkFQArgReC2wA7pd0k5n9ZiSPmf2vsvzvBU4qO0SfmZ1YC1kdx3H2l2qNaZjZ3ZKWVedo1aVWLY1TgdVmtsbMBoHrgfNGyX8BcF1NJHMcx6kChiiVmnJtwDxJK8u2i+stf15qNaaxEFhf9n0D8LJYRklLgSOAn5Ylt0taCRSBT5vZjRllLwYuBijM7Zqw0I7jOONhHA2NbWa2fPIkmTwacSD8fOA7ZlY+ErvUzDZKOhL4qaRHzOzJyoJmtgJYAdB2xKIDYPKb4zgNQxUHwicTSYcArwQOB/qAR4GVZpaxjva+1MpobAQWl31flKbFOB+4pDzBzDam/9dIupNkvCMwGo7jOHWlgV9VJZ0BXAYcDDwEbAHagTcCR0n6DnCFme0Z7Ti1Mhr3A8dIOoLEWJwPvKUyk6TjgTnAL8vS5gC9ZjYgaR6JhfynmkjtOI4zDqo45fY64HSSsY8NwMfM7OsTPOzZwLvN7OlIfc0kU3xfC9ww2kFqYjTMrCjpUuA2oABcbWaPSfoESbNoZBrt+cD1ZvvMQXgB8FVJJZKB+0+Xz7pyHMdpBAwolapjNMzsgqocaF+uMLOoC5WZFYEb8xwkl9GYaB9YKtQtwC0VaZdXfP94pNw9wIvy1vN8QWBw38lh7RtDdUsZZ2BoWX+QNn/e3iBtx554NDyeCiOvtfSE2foOjZ/Cg5bsDtKamuJt311Phx5bMzaHEcqKGX5hpSP6grS5Xd1B2rYdcWer5rXhgZsGwnz9h8cdBg9evCtIGyqG8neviztHtm8NJwEORSIfNh0RuQBAV0eo/5aIcyZA27ow0mIsomLforjD2bzDw+va2x86LQ6si3gcAm3bw4fS4JxQ15Zl4fUD6JwR3tdbI86ZbevjES1jjnwDS+JOfPMPCXXd3RveK8OR3wpA6+6wsoH54cmesTT8XQK0tYQOjjs2xrwbq4QRjYLYQKyS9CjJzNQbzGzX/hxk1Cm3ks6QdBvwX8BZwALgBOBvgUck/Z2kcbg5O47jTF/M8m11YiHwWeCPgCckfV/S+ZJyLjOQMFZLoyp9YI7jOAcEDTwQns5IvQ24TVIrSUPgfOALkn5iZhfmOc6oRsPM/mqUfbn7wBzHcaY/mhJTbgHMbFDSb4DHgVNIxo5zkcsjXNL7JR2khK9LelDS6/ZTXsdxnOmJ5dzqhKTFkv5K0oPAD0hswLlmdnLeY+SdPfU/zeyLkv6EZErs24D/C/xovEI7juNMSwysSrOnJgNJ95CMa3ybZNjhgf05Tl6jMXImzgb+bzpdtnHPjuM4Tl1o6MfiZcDPKlwaxk3eBQsfkPQjEqNxm6ROMpardxzHOWBp7O6pVwFdWTslvVrSOWMdJG9L4yLgRGCNmfVKmgu8M2dZx3GcA4MGnj0FPAL8QFI/8CCwlWQZkWNInu+3A/841kHyGo2PVzji7QL+Bcg1RasumGBo36birE1htmKGb17xyPDqHzF7R5C2ty909gJoCrMyY1t4zMGD4s3Z+R2hc1ZzU7xxt2dgTpA2a1NY10BXvK6BY0Onu5iuWY6M7dvCtJaIv9XAvHj9CzrCpW56hkKHt77ermj5jg2hrj2HhXUVjotHs4vpumVr3P1oxpYwrSnix9d/WLwRv+SgXUHa5qawrp174/XHdN1bCHWNOfEBLJkd1r9lU1eQNmtztDgWUWtgcZgGsGz2ziDt96XQaXNgd9xptGNjqOtweyjAnFm90fJz28P0HWu7onmrQoM795nZ94HvSzqGxFl7AbAH+HfgYjMLvVwj5DUaiyV9xMw+JamNZCDlof2Q23EcZ9pSR8e93JjZ74Hf72/5vGMa/xN4kaSPADcDd8aW/HAcxzmgKSnfNoUZtaUhqXzu7heBrwK/AO6SdLKZPTiZwjmO40wlNAVaGhNlrO6pKyq+7yRZe+oKkh68V0+GUI7jOFOOOjvu1YqxlhE5o1aCOI7jTG3U0APhI0g6FvhX4FAz+wNJLybxCv/7POXHWuX2raM58Uk6StIfjUtix3Gc6Upj+2mM8DXgI8AQgJk9TLJwYS7G6p6aS7IG+wPAAzw/r/do4DRgG4mXoeM4jjM1XJ5nmtl9Fe2B+Hz0CGN1T31R0pdIxi5eCbyYJAjT48DbYkumNwwGTUP7NqRa94ZXVBkzGWJryLQ2hec1a1XLlsjU8bbdYf2FgXDeOsR9Mppj0X6ApoFQhlhdw61xWfvz6lqKN0ybI7GNYvU3Dcbrj+laiKVFAjtl1TUwOzyvwxnXKqZr1gyXlu7wNbEwGHl1LGbVFfrEKDJ6WsiYMd+2J9S1ty9+D8XrD3VVMbyuMT0h7qdhwxm6FvI9h5rjbha5fy9NGaPPsfuqKeO6VIUG99MoY5uko0jbPJLeDGR45oSM6aeRrsH+43SbFCSdSTI7qwBcZWafrtj/DpLgIRvTpC+Z2VWTJY/jOM7+UK3ZU5P8TLwEWAEcL2kj8BTw1ryy1SRG+GhIKgBXkgRz2gDcL+mmSBzw/zCzS2suoOM4Tl6qYDQm+5loZmuA10iaBTSZWTxebgZ1NxrAqcDqVBEkXQ+cB1SeIMdxnAOBSX0mSvrLiu8Au4EHzGzVWOXzeoRPJguB9WXfN6RplbxJ0sOSviMputqNpIslrZS0crg70tHuOI4zicjybcC8kWdVul1cdpiqPRMzWA78eXrMhcCfAWcCX5P012MVzhu579A0Yt+t6fcTJF00DiEnys3AMjN7McnYyjdimcxshZktN7PlhY5ZNRTPcZwDHmM8y4hsG3lWpduKcdaW65mYwSLgZDP7oJl9kCTc6yEkS6e/Y6zCeVsa15AEJD88/f474APjEHI0NgLlVnIRzw/uAGBm281sZO7MVSRKOo7jNBbV8dOY7GfiIUD5XMQhEke/vor0KHmNxjwz+zbpLGQzKwLh3MH9437gGElHSGolcTK5qTyDpAVlX88lmfLrOI7TUIyje2o0JvuZ+E3gV5I+JuljJOsJfisdGB9z3CTvQHhPGnhpZF7vy0kGTiaMmRUlXUrSkikAV6fhZD8BrDSzm4D3STqXxAFlBzmaUI7jODWnCrOnJvuZaGaflPRD4A/TpD83s5Xp5zFjJOU1Gn9JYumOkvQLYD7w5rxCjoWZ3QLcUpF2ednnj5C4vY/joKCK4DhRx6SMVVJKESeiDT2zg7SBvpZo+faYSJFgORn+emzrDcdkChmvKFHftHHMixvqCzNv6g2DAA33ZziRRU6hRepXhmPVsz2dQVp/MTyAMtq2pdh5jZyq/r4wsBPErysZukbvoaaw/lhgJoCNPeF53d0b3i1Z90VU10jePX2xOxA2toa6KuJ0GdMzK10D8cwbu8O6enrDoGXtGQ/avLru7IkHBxuOOKPGdK0qVfLTmJRn4r7Hul/SOtJHlaQleZ21cz1azOxBSacBx5E8Ip4ws4yfheM4zoFHzq6nupO2UK4gGaPeAiwBfgu8ME/5XEYjdTY5G1iWlnmdJMzsn/dDZsdxnOnJ1Aiw9Eng5cDtZnaSpDOYBI/wm4F+ksDkU2NJLsdxnBozFVoawJCZbZfUJKnJzO6Q9IW8hfMajUXpfGDHcRwni6lhNHZJ6gDuBr4paQuQ2xs675TbWyW9bn+kcxzHOSDIOd22AVoj5wG9wP8Cfgg8CZyTt3Beo3Ev8D1JfZL2SNorac+4RXUcx5nOTI0gTJebWcnMimb2DTP7F+DDeQvnNRr/DLyCJHjHQWbWaWbh3EHHcZwDGJXybXXmtZG0s/IWzjumsR541MzqbyMdx3GccSPpPcBfAEdKerhsVyeJV3gu8hqNNcCd6YKFz61N0shTbmXQNLTv9Lf+OeF0uFLcN49Cd3hq1m6eF6TZ7vgBihHfqr65Mc+weP3bn4005DJm87VF3lz654R1FWfEy2tPqMOaTfPDfHvjt8tQxLfKCmH9MSdEgGee7QoTI9Hg2jP074+c1+GIH9/wzrhz39pieF2beuON8KHOUIhiRK+mjBV8Nmw+OEiziCPprIxfZkzX2D08sD1+sdf3hpkL/aFOgwdlRLSMnJZCX/xcrds8NyzfE9bfGr8sUV1j9e/ZFl+cdG9zeA5ahqaGc98k8S3gVuBT7Bume6+Z7ch7kLxG46l0a003x3Ecp5zGGOQejQKwhyRy3z5IOjiv4cjrEf5345PNcRznAKSxjcYDPC9hZZPLgCPzHGRUoyHpS2Z2qaSbiZwOMzs3TyWO4zgHBA1sNMzsiGocZ6yWxtuBS4HPVaMyx3Gc6YpoiJlRuUjXn3pV+vVOM/tB3rJjGY0nAczsrv2UzXEc58Cg8cc0AJD0aeClJHE1AN4v6Q/N7H/nKT+W0ZhfGYS8nEaePeU4jlNzpoDRIFl89kQzKwFI+gbwEFAVo1EAOsic7Ok4juM8x9QwGgBdJMGbACIBZbIZy2hsNrNP7I9EdcfCQDh9h+Qv3rI7Yid3xwPbxBgK4wpF07Joe3piM5t7D8uft3VHZPL7jvy6DnZF0iL5soIota3Np2tWYKCeBfH0oJ6tGUGkstIj9IduFlEKGX4ahafCIEQxYn4mMA5dN2f9tPPNsu8L3XQyae7JeKdck+8eKsZjKGWmV9K+IcPZqg5Mhe4pEj+NhyTdQdIgeBX7+m2MyljLiFSthSHpTElPSFotKRBQ0l9K+o2khyX9RNLSsn3Dklal202VZR3HcRqCBl57StKVkl5pZteRxNP4LnAD8Aoz+4+8xxnLaPzxBGR8jjSI05Uk65ucAFwg6YSKbA8By9Ml2L8D/FPZvj4zOzHdfJqv4ziNh1Vv7amxXrL3k98Bn5O0lmSF2/VmdpOZPTOeg4xqNMbjWj4GpwKrzWyNmQ0C15Msz1te1x1m1pt+vRdYVKW6HcdxakMVWho5X7LHL5rZF83sFcBpwHbgakm/lfQxScfmPU7eVW4nykKSRQ9H2JCmZXERyRopI7RLWinpXklvzCok6eI038rhntwxRRzHcapCleJpjPmSPRHMbJ2ZfcbMTgIuAN4IPJ63fN61p2qGpLcCy0ms4QhLzWyjpCOBn0p6xMyerCxrZiuAFQDtCxdPjSEpx3GmD/mfOvMkrSz7viJ9fkH8JftlExcuQVIzSSvmfJIhiDuBj+ctXyujsRFYXPZ9UZq2D5JeA/wNcJqZla+muzH9v0bSncBJpI6HjuM4DcH4Brm3mdnyyRMmRNJrSVoWZwP3kbRgLjazcXXL1Kp76n7gGElHSGolsXD7zIKSdBLwVeBcM9tSlj5HUlv6eR7wSuA3NZLbcRwnF6Jq3VO5XrL3g48A9wAvMLNzzexb4zUYUKOWhpkVJV0K3EbiMHi1mT0m6RPASjO7CfgsiSPhf0oCeDqdKfUC4KuSSiRG7tNm5kbDcZyGo0p+Gs+9ZJMYi/OBt0z0oGb26okeA2o4pmFmtwC3VKRdXvb5NRnl7gFeNN76FHPuWxh6lykjKEvH2rARNnNreEf0d8XL7z0izFuaFdbf9mz8EnSui9x9GTdk95JQhv4FYWSgpr64E1vnU5GARzvCyvrmxXXtXhbOISy1heXbN2XUvz5yrgphXXuXBkkADB4a6lrYG9YV0xOgdU9Yf+8h8bw9S0NdrTksP3N9XNeOjWHeYiS61N5l0eIMzQ11bd4Z3kMHPRUv39wb1t9zeFh/z5IMT8xSmHfW0/EOi1nPhHUNdYTl90R+KwDDs0MZWreGunaujRanaSg8bveixg/ClPWSPfEjV4eGGwh3HMeZslRp+k3sJbtRcKPhOI5TDabIKrcTxY2G4zhOtXCj4TiO4+RlqgRhmghuNBzHcaqEd085juM4+ajjCra1xI2G4zhOtXCj4TiO4+RhxCN8ujN9jUbEua/QFcaTG+6Ln4KW7tBhqXNdfyRnPDrZ7vZwRKxlTiSc2zPx+js2hk5cKsXvyO5FYZi35q6wriLxqHGtuyOOcGv7grTh1hnR8sMzQ12bO8Nz3fR0PBRbx4ahIK3UEnN4i0doa50TXpehgbCumMNiUn94roY64rpaZ8RpsiV0QisMxMt3Ph3WNTg71Gv30XGHuZiuxe5ZQdqMrfER2fbt4bnunxvew+oM8wFYxLmvuS9+X8V+L32Hhnl3HxctHtXVtncEabOeDa8JQHN3eF16I/VXk6zf6HRi+hoNx3GcWuJjGo7jOM548O4px3EcJz9uNBzHcZy8eEvDcRzHyY8bDcdxHCcX5suIOI7jODlxP42pTgmaKlwF1BR5Dci4ys39YXrzzt4grWVe6CORRVMhrF8ZsW5a9oR+DllzwJuKoQxNhfx3b0skME/zrtBPo7kv7pMSO4eFmK7x6fS07Ap9F0rtoe+ISnE/DTXlC1jV0hN/DYxd10J/fl0Vcamo9BF6vq6I70Ek4JRKcX+CpoiusbfbloiPAkDLzvC6FgYifhqxc5pBIeJ+lFVXcVbkkWPxx1BM11JM1z0Zfhp7Q8GaBifXTwOb/lajVjHCJ4ykMyU9IWm1pMvqLY/jOE4lVYoR3tBMCaMhqQBcCZwFnABcIOmE+krlOI5Tho1jm8JMCaMBnAqsNrM1ZjYIXA+cV2eZHMdx9kGlfFvu4yX8S9rD8rCkkzPy3Zn2xKxKt0OqpVMlU2VMYyGwvuz7BuBllZkkXQxcDNDSOac2kjmO46RMwuyps4Bj0u1lwL8SefalXGhmK6suQQVTpaWRCzNbYWbLzWx5YUa4iJvjOM6kYSQD4Xm2/JwHXGsJ9wJdkhZMivw5mSpGYyOwuOz7ojTNcRynYRjHQPg8SSvLtoszDhnrZVmYkfff0q6pj0oKp+RVianSPXU/cIykI0iMxfnAW+orkuM4TgX5GxHbzGx5FWu+0Mw2SuoEbgDeBlxbxeM/x5RoaZhZEbgUuA14HPi2mT1WX6kcx3GeZ8S5b6JTbiVdMjKgDWwmRy+LmW1M/+8FvkUyeWhSmCotDczsFuCWvPkFNFX4/Ax1R5zgekMnMogPaFkhkjfjBij0hvZ4YG/oWDQrwzEq5vCVRcy5qnfveHQNlYjpmjXIFzvuYFOoa3uGwxsRXa0pTCvEYmABvd1hXc194fmXxRWI6dqU4XRJT/iTKQ6G56/y3ntesFAui/QkZOnaH7mHW/vy3ysWqT/mdFnqiTtSEgnC1DQc/xHE6opR6I3L3x+5ru2x30uG+tYU1p95XaqBWVWCMJnZlSQuBkh6PXCppOtJBsB3m9nm8vySmoEuM9smqQU4B7h9woJkMGWMhuM4TsNTfR+MW4CzgdVAL/DOkR2SVpnZiUAbcFtqMAokBuNrVZckxY2G4zhOlai2t7eZGXBJxr4T0/89wCnVrTkbNxqO4zjVwACPEe44juPkZvrbDDcajuM41WKqL0aYBzcajuM4VaIas6caHTcajuM41WAarGCbBzcajuM4VSBx7pv+VmP6Gg2DQoUzWdum0GEpy9nHIlHDBg+ZGaQNt8U9i9p2hOnFgdAxqzkMbpbU1RXmVYYTVcy5r3VTWL6QUddwaz5dS81xXdu3hunDe0LHqqwIbwNzQicuawmP2RwG2AOgdUOoa8veMF+xLe5sFtPVMhzG2reEjoAWOWxWlMKBeTNCuWaEB2jdEy9f2hieq1je4qy4IyeE9cecNtueyXg0xG7BjAflwPzwvBZnhnK17o5XBfmu6+BBGZH/ItEfK58JVcdjhDuO4zh58ZaG4ziOkw8f03Acx3HyU521pxodNxqO4zjVwrunHMdxnFzYpIR7bTjcaDiO41QLb2k4juM4uZn+NsONhuM4TrVQafr3T9XEaKRBzr9IEkykF3iHmT1YkWcm8J/AUcAwcLOZXZbuewfwWZ4Pc/glM7tq1DpL0FQRUe2gp/LLXIqcmb2LMqKZRZj5zMReOXrnZzlnhTT3hnUd9GT+uortoSfbeHSdtXFiuvYsyHcbtu6J15PlCFfJYGfcY2+wM7+uHU9PTNfuw/Pp2rYzXk/bznz19HdlRM2LpMdm/HSuzVfPaHQvzHdeZ2yN6zpja756+ubm/61UPhOqiuHOfVXkLOCYdHsZ8K/p/0o+Z2Z3SGoFfiLpLDO7Nd33H2Z2aW3EdRzHGR/CDgjnvnxBfCfOecC1lnAv0CVpQXkGM+s1szvSz4PAgyRB1B3HcaYGZvm2KUytjMZCYH3Z9w1pWhRJXcAbgJ+UJb9J0sOSviNpcUa5iyWtlLSy2N9TBbEdx3HGQZWNhqTjJf1S0oCkD02i5LmpldHIjaRm4DrgX8xsTZp8M7DMzF4M/Bj4Rqysma0ws+Vmtry5fVZtBHYcx4HnxzTybPnZAbwP+FwVJZ0Qk2Y0JF0iaZWkVcBmoLx1sIjnB7UrWQH83sy+MJJgZtvNbGSN1KuoYRB1x3GcvKhUyrXlxcy2mNn9wGSvz5ubSTMaZnalmZ1oZicCNwJvV8LLgd1mtrmyjKS/B2YDH6hILx//OBd4fLLkdhzH2T9ydk1N8TGNWs2euoVkuu1qkim37xzZIWmVmZ0oaRHwN8BvgQeTWbrPTa19n6RzgSJJc+0dNZLbcRwnH8Z4DMI8SSvLvq8wsxXVF6r61MRomJkBl2TsOzH9v4Ek+FUsz0eAj4yv0jDgSufaMApRqSU+x3vXMWGwm775Yb6sADJzfh+2Jlv2DAZpPYvCoDgAu5dFGoEZ7cKD1oXN3Y51oa7Fjvi8+Z3HhMFuBuaE+dq3x+vvWh3q1dwbRiHauyyu654l4WWPBcea/VS8WT9zU6hrLIjVrmPi+g92Ro75bDQrXav7g7RYcKw9R7RHy3cvDHUthIeka81wtHz7s2HmgfnhvbrzqPhPeziMi8SsTRE/nzURoQAUyr/7qLB+gN7DIoG0IvNT5qyOR6xq3R5G7epbEJ7XXUfGf8OlyOXu3DDJb/n5e562mdny2A5JlwDvTr+ebWabqiBZ1XCPcMdxnCpRDT8NM7sSuHLi0kwObjQcx3GqRZXHKyQdBqwEDgJKkj4AnGBmOddBqD5uNBzHcaqBGQxXdx0RM3uGBnNydqPhOI5TLab4zKg8uNFwHMepFm40HMdxnFwY4DHCHcdxnHwY2PRfG92NhuM4TjUwqj4Q3ohMW6Mhg6ahfZuKLWsiHlud8YUN7bhDgrTBOWHTs7k3HtinbUvoxdS0MYwq03LwEdHyQ535nfua+yJyrQlWaaFwyMHR8qUTwvSYrq27M3TdGM7+064wrXDY0mj5oYNC56ymyEo7Ld1xh7fm1eEyZlq6IEgbfmGGc19E1xlb4rq2ro9EQRoInRubFi+J19UV1tWyN6yrdVd8qaHm328I0qw5XPR5eGb8pz0Qqb8jPCStazMiIDVFHOmOPDxeV+S8ajiia8SJD6D5yfC6ts4K76FiR9y5rxjxJW16apK7j3xMw3Ecx8mNGw3HcRwnH1N/McI8uNFwHMepBgaMY9nzqYobDcdxnGrhLQ3HcRwnH9VfRqQRcaPhOI5TDQzM/TQcx3Gc3LhHuOM4jpMbH9OYXJTEdP0iSSjYXuAdZvZgJN+dwAJgJETb68xsy6gHNygM7HsBS9tDx6ym4bjDmEqhc5/FfIgy7pGmPZHIeVvD0HeF/rgTGE3hpbEM575Cf9gkHt4Wqas9HmFNpdC5L6arMlre2tMd1h/TdTCua0wvi/jWFfriEd6KW7cFac1zI6EHLRKij7iuTZFofABEnBZLfeG1bhoKHe6SukLForr2xJ37itsj57Xn0EjOSIg+MnSNnFbbmRGSshBxxByOO/dFfy+xQ3bHowTGrmuhd2FYj+L3tTWF17DymVBVzHz2VA04Czgm3V4G/Gv6P8aFZrYyY5/jOE798ZbGpHMecG0aQ/xeSV2SFphZuAaG4zhOQ2NYRs/FdCKjw6NmLATWl33fkKbF+DdJqyR9NO3WCpB0saSVklYWB8IuE8dxnEljZGn0PNsUpt5GIy8XmtmLgP+Wbm+LZTKzFWa23MyWN7d11FRAx3EcrJRvm8LU3GhIuiRtMawCNgPlI4aLgGBpSzPbmP7fC3wLOLUGojqO4+TGACtZrm0qU3OjYWZXmtmJZnYicCPwdiW8HNhdOZ4hqVnSvPRzC3AO8GiNxXYcxxkdswOipVHvgfBbSKbbriaZcvvOkR2SVqWGpQ24LTUYBeB24Gu1F9VxHGd0DoSBcNk0nSImaSuwLv06DwgnfU8/XM/px4Gia731XGpm8ydyAEk/JNEjD9vM7MyJ1Fcvpq3RKEfSSjNbXm85JhvXc/pxoOh6oOg5HZgqs6ccx3GcBsCNhuM4jpObA8VorKi3ADXC9Zx+HCi6Hih6TnkOiDENx3EcpzocKC0Nx3Ecpwq40XAcx3Fyc0AYDUkvlVSU9OZ6yzJZSLpQ0sOSHpF0j6SX1FumyUDSmZKekLRa0mX1lmcykLRY0h2SfiPpMUnvr7dMk4mkgqSHJP2g3rI4YzPtjYakAvAZ4Ef1lmWSeQo4LV3Y8ZNMw4HF9FpeSRKH5QTgAkkn1FeqSaEIfNDMTgBeDlwyTfUc4f3A4/UWwsnHtDcawHuBG4DRI/1NcczsHjMbCU14L8nij9ONU4HVZrbGzAaB60liskwrzGzzSATLdJHOx8kOGTClkbQIeD1wVb1lcfIxrY2GpIXAn5JEBDyQuAi4td5CTALjib8yLZC0DDgJ+FWdRZksvgD8NTC1V/E7gJjWRoPkhvyw2RRfVnIcSDqDxGh8uN6yOBNDUgdJK/kDZhYGJ5/iSDoH2GJmD9RbFic/9V7ltupIugR4d/p1NnB9GuhvHnC2pKKZ3Vgn8apKha5nk+h4FXCWmW2vm2CTx0ZyxF+ZDqSrOt8AfNPMvltveSaJVwLnSjobaAcOkvTvZvbWOsvljMIB49wn6RrgB2b2nXrLMhlIWgL8FHi7md1Tb3kmA0nNwO+APyYxFvcDbzGzx+oqWJVJwxl/A9hhZh+oszg1QdLpwIfM7Jw6i+KMwbRraRzAXA7MBb6ctqyK023VUDMrSroUuI0ktsrV081gpLySJKTxI2mES4D/bWa31E8kx0k4YFoajuM4zsSZ7gPhjuM4ThVxo+E4juPkxo2G4ziOkxs3Go7jOE5u3Gg4juM4uXGj4dSVdDXXP6lI+4CkzKVfJK2VNE9Sl6S/mHwpQdIbJV0u6TRJv6zY1yzpWUmHS/qcpFfXQibHqQduNJx6cx1wfkXa+Wn6WHQBNTEaJOsjfRn4GbBI0tKyfa8BHjOzTcD/Aablku2OA240nPrzHeD1klrhuQX6Dgd+JumCND7Io5I+Eyn7aeAoSaskfVZSh6SfSHowLffcCriSPprG4fi5pOskfShNP0rSDyU9IOlnko6vrETSscCAmW1L1zH7NvsauueMnJmtA+ZKOqwaJ8dxGg03Gk5dMbMdwH0kMTIgeQB/G1hAEgfl1cCJwEslvbGi+GXAk2Z2opn9FdAP/KmZnQycAVyhhJcCbwJektZT7im/AnivmZ0CfIikNVHJK4EHy74/1zqS1Eay7tcNZfsfTMs4zrTDlxFxGoGRh/D30/8XAS8F7jSzrQCSvgm8CrhxlOMI+EdJryJZanshcCjJA/z7ZtYP9Eu6OT1mB/CHwH+mS68AtEWOuwDYOvLFzFamrZrjgBcAv0qN3whbSFpLjjPtcKPhNALfBz4v6WRgppk9kAbnGS8XAvOBU8xsSNJaktVTs2gCdpnZiWMct49kxeRyRgzdCwjHX9rTMo4z7fDuKafumFk3cAdwNc8/gO8DTktnSRWAC4C7KoruBTrLvs8mic8wlMYVGRms/gXwBkntaevinLTePcBTkv5fSFaXzYit/jhwdEXadcBbSbrPvl+x71jg0bE1d5yphxsNp1G4jmTMYWRAeTPJmMUdwK+BB8xsn4dzGjPkF+lA+WeBbwLLJT0CvB34bZrvfuAm4GGSiIaPALvTw1wIXCTp18BjxMPH3g2cpLI+LDN7HOgBfmpmPSPpaRyMo4GV+38qHKdx8VVunQMCSR1m1i1pJokRuHgkDnfO8l8Ebjaz28fI96fAyWb20YlJ7DiNibc0nAOFFWlsigeBG8ZjMFL+EZiZI18zcMU4j+04UwZvaTiO4zi58ZaG4ziOkxs3Go7jOE5u3Gg4juM4uXGj4TiO4+TGjYbjOI6Tm/8fb+x8aCrPfsIAAAAASUVORK5CYII=", "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..646daf827309 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ Changelog = "https://microsoft.github.io/Qcodes/changes/index.html" [project.optional-dependencies] zurichinstruments = ["zhinst-qcodes>=0.3"] minicircuits_usb_spdt = ["pythonnet>3.0.4"] -loop = ["qcodes_loop>=0.1.2"] +loop = ["qcodes_loop>=0.2.3"] test = [ "coverage[toml]>=7.10.5", "deepdiff>=8.6.1", @@ -77,16 +77,19 @@ 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.77", "types-jsonschema>=4.16.0", "types-networkx >= 3.6.1.20260512", # minimum version required for generic data types in graph "types_requests>=0.1.8", "types-tabulate>=0.1.0", "types-tqdm>=4.64.6", "types_pywin32>=305.0.0.7", - "qcodes_loop>=0.1.1", + "qcodes_loop>=0.2.3", "zhinst.qcodes>=0.5", # typecheck zhinst driver alias "libcst>=1.2.0", # refactor tests + "intersphinx_registry>=0.2603.16" # typecheck docs/conf.py ] docs = [ "autodocsumm>=0.2.9", @@ -293,6 +296,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..a9634dacc979 100644 --- a/requirements.txt +++ b/requirements.txt @@ -351,7 +351,7 @@ pyzmq==27.2.0 # via # ipykernel # jupyter-client -qcodes-loop==0.2.2 +qcodes-loop==0.2.3 # via qcodes (pyproject.toml) referencing==0.37.0 # via @@ -457,6 +457,8 @@ traitlets==5.16.1 # nbconvert # nbformat # nbsphinx +ty==0.0.77 + # 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/monitor/monitor.py b/src/qcodes/monitor/monitor.py index eb9caa7e8830..3d8fccc0e1e4 100644 --- a/src/qcodes/monitor/monitor.py +++ b/src/qcodes/monitor/monitor.py @@ -44,8 +44,8 @@ from websockets.asyncio.server import ServerConnection -WEBSOCKET_PORT = 5678 -SERVER_PORT = 3000 +WEBSOCKET_PORT: int = 5678 +SERVER_PORT: int = 3000 log = logging.getLogger(__name__) 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/parameter.py b/src/qcodes/parameters/parameter.py index 47626681be8c..04c3b8920f0c 100644 --- a/src/qcodes/parameters/parameter.py +++ b/src/qcodes/parameters/parameter.py @@ -10,6 +10,8 @@ from typing_extensions import TypedDict +from qcodes.utils import qcodes_abstractmethod + from .command import Command from .parameter_base import ( InstrumentTypeVar_co, @@ -286,6 +288,12 @@ class Parameter( """ + _get_raw_impl: Callable[[], ParamRawDataType] | None = None + """Implementation of ``get_raw`` generated from ``get_cmd``, if any.""" + + _set_raw_impl: Callable[[ParamRawDataType], None] | None = None + """Implementation of ``set_raw`` generated from ``set_cmd``, if any.""" + def __init__( self, name: str, @@ -382,9 +390,9 @@ def _set_manual_parameter( " get_raw is an error." ) elif not self._implements_get_raw and get_cmd is not False: + get_raw_impl: Callable[[], ParamRawDataType] if get_cmd is None: - # ignore typeerror since mypy does not allow setting a method dynamically - self.get_raw = MethodType(_get_manual_parameter, self) # type: ignore[method-assign] + get_raw_impl = MethodType(_get_manual_parameter, self) else: if isinstance(get_cmd, str) and instrument is None: raise TypeError( @@ -396,14 +404,14 @@ def _set_manual_parameter( exec_str_ask = getattr(instrument, "ask", None) if instrument else None # TODO get_raw should also be a method here. This should probably be done by wrapping # it with MethodType like above - # ignore typeerror since mypy does not allow setting a method dynamically - self.get_raw = Command( # type: ignore[method-assign] + get_raw_impl = Command( arg_count=0, cmd=get_cmd, exec_str=exec_str_ask, ) + self._get_raw_impl = get_raw_impl self._gettable = True - self.get = self._wrap_get(self.get_raw) + self.get = self._wrap_get(get_raw_impl) if self._implements_set_raw and set_cmd not in (None, False): raise TypeError( @@ -412,9 +420,9 @@ def _set_manual_parameter( " set_raw is an error." ) elif not self._implements_set_raw and set_cmd is not False: + set_raw_impl: Callable[[ParamRawDataType], None] if set_cmd is None: - # ignore typeerror since mypy does not allow setting a method dynamically - self.set_raw = MethodType(_set_manual_parameter, self) # type: ignore[method-assign] + set_raw_impl = MethodType(_set_manual_parameter, self) else: if isinstance(set_cmd, str) and instrument is None: raise TypeError( @@ -426,14 +434,14 @@ def _set_manual_parameter( exec_str_write = ( getattr(instrument, "write", None) if instrument else None ) - # TODO get_raw should also be a method here. This should probably be done by wrapping + # TODO set_raw should also be a method here. This should probably be done by wrapping # it with MethodType like above - # ignore typeerror since mypy does not allow setting a method dynamically - self.set_raw = Command( # type: ignore[assignment] + set_raw_impl = Command( arg_count=1, cmd=set_cmd, exec_str=exec_str_write ) + self._set_raw_impl = set_raw_impl self._settable = True - self.set = self._wrap_set(self.set_raw) + self.set = self._wrap_set(set_raw_impl) self._meta_attrs.extend(["label", "unit", "vals"]) @@ -459,6 +467,31 @@ def _set_manual_parameter( self._docstring = docstring self.__doc__ = self._build__doc__() + @qcodes_abstractmethod + def get_raw(self) -> ParamRawDataType: + """ + Call the ``get_raw`` implementation generated from ``get_cmd``. + + This method stays marked as abstract so that + :attr:`~ParameterBase._implements_get_raw` keeps reporting ``False`` + for :class:`Parameter` itself: a subclass is still expected to either + override ``get_raw`` or supply a ``get_cmd``. + """ + if self._get_raw_impl is None: + raise NotImplementedError + return self._get_raw_impl() + + @qcodes_abstractmethod + def set_raw(self, value: ParamRawDataType) -> None: + """ + Call the ``set_raw`` implementation generated from ``set_cmd``. + + See :meth:`get_raw` for why this method stays marked as abstract. + """ + if self._set_raw_impl is None: + raise NotImplementedError + self._set_raw_impl(value) + def _build__doc__(self) -> str: if len(self.validators) == 0: validator_docstrings = ["* `vals` None"] @@ -521,7 +554,7 @@ def increment(self, value: ParameterDataTypeVar) -> None: """ # this method only works with parameters that support addition # however we don't currently enforce that via typing - self.set(self.get() + value) # type: ignore[operator] + self.set(self.get() + value) # type: ignore[operator] # ty: ignore[unsupported-operator] def sweep( self, diff --git a/src/qcodes/parameters/parameter_base.py b/src/qcodes/parameters/parameter_base.py index 16d96c8b08e4..2169c173dcb3 100644 --- a/src/qcodes/parameters/parameter_base.py +++ b/src/qcodes/parameters/parameter_base.py @@ -1023,7 +1023,7 @@ def set_wrapper(value: ParameterDataTypeVar, **kwargs: Any) -> None: # a list containing only `value`. # The steps are deliberately untyped: ``get_ramp_values`` works # in terms of numbers rather than the parameter's data type. - steps: Sequence[Any] = self.get_ramp_values(value, step=self.step) # type: ignore[arg-type] + steps: Sequence[Any] = self.get_ramp_values(value, step=self.step) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] for val_step in steps: # even if the final value is valid we may be generating 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..223adb261622 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 """ @@ -75,7 +75,7 @@ Complex types that matches C types. """ -numpy_complex: tuple[type[complex_type_union], ...] = ( +numpy_complex: tuple[type[np.complexfloating], ...] = ( numpy_concrete_complex + numpy_c_complex ) """ 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/common.py b/tests/common.py index 11d4df019b2e..9c73b66b36b0 100644 --- a/tests/common.py +++ b/tests/common.py @@ -87,7 +87,9 @@ def profile[**P, T](func: Callable[P, T]) -> Callable[P, T]: """ def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: - profile_filename = func.__name__ + ".prof" + # a Callable is not necessarily a function object, so ty rejects + # reading its name here. mypy and pyright both allow it. + profile_filename = func.__name__ + ".prof" # ty: ignore[unresolved-attribute] profiler = cProfile.Profile() result = profiler.runcall(func, *args, **kwargs) profiler.dump_stats(profile_filename) diff --git a/tests/dataset/measurement/test_measurement_context_manager.py b/tests/dataset/measurement/test_measurement_context_manager.py index 412cf89c33a8..dc30c10a4780 100644 --- a/tests/dataset/measurement/test_measurement_context_manager.py +++ b/tests/dataset/measurement/test_measurement_context_manager.py @@ -85,10 +85,10 @@ def test_register_parameter_arg_types(DAC, DMM): meas.register_parameter(DMM.v1, setpoints="foo") with pytest.raises(TypeError): - meas.register_parameter(DMM.v1, basis=(DAC.ch1, 3)) # type: ignore[arg-type] + meas.register_parameter(DMM.v1, basis=(DAC.ch1, 3)) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises(TypeError): - meas.register_parameter(DMM.v1, setpoints=(DAC.ch1, 3)) # type: ignore[arg-type] + meas.register_parameter(DMM.v1, setpoints=(DAC.ch1, 3)) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_register_parameter_numbers(DAC, DMM) -> None: @@ -102,7 +102,7 @@ def test_register_parameter_numbers(DAC, DMM) -> None: for not_a_parameter in not_parameters: with pytest.raises(ValueError): - meas.register_parameter(not_a_parameter) # type: ignore[arg-type] + meas.register_parameter(not_a_parameter) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] my_param = DAC.ch1 meas.register_parameter(my_param) @@ -300,7 +300,7 @@ def test_unregister_parameter(DAC, DMM) -> None: not_parameters = [DAC, DMM, 0.0, 1] for notparam in not_parameters: with pytest.raises(ValueError): - meas.unregister_parameter(notparam) # pyright: ignore[reportArgumentType] + meas.unregister_parameter(notparam) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] # unregistering something not registered should silently "succeed" meas.unregister_parameter("totes_not_registered") @@ -979,7 +979,7 @@ def test_datasaver_foul_input(bg_writing) -> None: with meas.run(bg_writing) as datasaver: for ft in foul_stuff: with pytest.raises(ValueError): - datasaver.add_result(("foul", ft)) # type: ignore[arg-type] + datasaver.add_result(("foul", ft)) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] @settings(max_examples=10, deadline=None) diff --git a/tests/dataset/test_converters.py b/tests/dataset/test_converters.py index a832f7369633..3b1f397280b4 100644 --- a/tests/dataset/test_converters.py +++ b/tests/dataset/test_converters.py @@ -164,11 +164,11 @@ def test_construct_current_rundescriber_from_fake_v4(some_interdeps) -> None: version=4, shapes=None, ) - v4["foobar"] = {"foo": ["bar"]} # type: ignore[typeddict-unknown-key] + v4["foobar"] = {"foo": ["bar"]} # type: ignore[typeddict-unknown-key] # ty: ignore[invalid-key] rds1 = RunDescriber._from_dict(v4) rds_upgraded = from_dict_to_current(v4) v3 = v4.copy() - v3.pop("foobar") # type: ignore[typeddict-item] + v3.pop("foobar") # type: ignore[typeddict-item] # ty: ignore[invalid-key] v3["version"] = 3 assert rds1._to_dict() == v3 assert rds_upgraded._to_dict() == v3 diff --git a/tests/dataset/test_datasaver.py b/tests/dataset/test_datasaver.py index 1a4f965adec7..5545d98dc621 100644 --- a/tests/dataset/test_datasaver.py +++ b/tests/dataset/test_datasaver.py @@ -168,7 +168,7 @@ def test_saving_numeric_values_as_text(numeric_type, bg_writing) -> None: data_saver.add_result((p.name, value)) finally: data_saver.dataset.mark_completed() - data_saver.dataset.conn.close() # type: ignore[attr-defined] + data_saver.dataset.conn.close() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] @pytest.mark.usefixtures("experiment") @@ -200,4 +200,4 @@ def test_duplicated_parameter_raises() -> None: data_saver.add_result((p.name, 1), (p.name, 1)) finally: data_saver.dataset.mark_completed() - data_saver.dataset.conn.close() # type: ignore[attr-defined] + data_saver.dataset.conn.close() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] diff --git a/tests/dataset/test_dataset_basic.py b/tests/dataset/test_dataset_basic.py index d0ab9cf21ebb..284521cfe760 100644 --- a/tests/dataset/test_dataset_basic.py +++ b/tests/dataset/test_dataset_basic.py @@ -312,7 +312,7 @@ def test_load_by_id_for_none() -> None: with pytest.raises( ValueError, match=re.escape("run_id has to be a positive integer, not None.") ): - _ = load_by_id(None) # type: ignore[arg-type] + _ = load_by_id(None) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] @settings(deadline=None, max_examples=6) @@ -809,7 +809,7 @@ def test_parent_dataset_links_invalid_input() -> None: match = re.escape("Invalid input. Did not receive a list of Links") with pytest.raises(ValueError, match=match): - ds.parent_dataset_links = [ds.guid] # type: ignore[list-item] + ds.parent_dataset_links = [ds.guid] # type: ignore[list-item] # ty: ignore[invalid-assignment] match = re.escape( "Invalid input. All links must point to this dataset. " @@ -971,7 +971,7 @@ def test_get_array_parameter_data_no_nulls(array_dataset_with_nulls) -> None: expected_names = {} expected_names["val1"] = ["val1", "sp1", "sp2"] expected_names["val2"] = ["val2", "sp1"] - expected_shapes = {} + expected_shapes: dict[str, list[tuple[int, ...]]] = {} expected_values = {} if "array" in types: @@ -1010,7 +1010,7 @@ def test_get_array_parameter_data(array_dataset) -> None: expected_shapes: dict[str, list[tuple[int, ...]]] = {} expected_len = 5 expected_shapes[par_name] = [(expected_len,), (expected_len,)] - expected_values = {} + expected_values: dict[str, list[np.ndarray]] = {} expected_values[par_name] = [ np.ones(expected_len) + 1, np.linspace(5, 9, expected_len), diff --git a/tests/dataset/test_dataset_export.py b/tests/dataset/test_dataset_export.py index bf7e21911689..9ece8058438a 100644 --- a/tests/dataset/test_dataset_export.py +++ b/tests/dataset/test_dataset_export.py @@ -1648,13 +1648,13 @@ def test_multi_index_options_non_grid(mock_dataset_non_grid: DataSet) -> None: def test_multi_index_wrong_option(mock_dataset_non_grid: DataSet) -> None: with pytest.raises(ValueError, match="Invalid value for use_multi_index"): - mock_dataset_non_grid.to_xarray_dataset(use_multi_index=True) # pyright: ignore[reportArgumentType] + mock_dataset_non_grid.to_xarray_dataset(use_multi_index=True) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] with pytest.raises(ValueError, match="Invalid value for use_multi_index"): - mock_dataset_non_grid.to_xarray_dataset(use_multi_index=False) # pyright: ignore[reportArgumentType] + mock_dataset_non_grid.to_xarray_dataset(use_multi_index=False) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] with pytest.raises(ValueError, match="Invalid value for use_multi_index"): - mock_dataset_non_grid.to_xarray_dataset(use_multi_index="perhaps") # pyright: ignore[reportArgumentType] + mock_dataset_non_grid.to_xarray_dataset(use_multi_index="perhaps") # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] def test_geneate_pandas_index() -> None: diff --git a/tests/dataset/test_db_overview.py b/tests/dataset/test_db_overview.py index 5e4fdaf63bf2..218f3c93b486 100644 --- a/tests/dataset/test_db_overview.py +++ b/tests/dataset/test_db_overview.py @@ -148,7 +148,7 @@ def test_get_db_overview_extra_columns(db_conn: AtomicConnection) -> None: # An existing ad-hoc metadata column is returned ... overview = get_db_overview(conn=db_conn, extra_columns=["my_tag"]) - assert overview[1]["my_tag"] == "hello" # type: ignore[typeddict-item] + assert overview[1]["my_tag"] == "hello" # type: ignore[typeddict-item] # ty: ignore[invalid-key] # ... while a non-existent column is silently skipped. overview = get_db_overview(conn=db_conn, extra_columns=["does_not_exist"]) diff --git a/tests/dataset/test_dependencies.py b/tests/dataset/test_dependencies.py index c4deebc32239..06511f1fcb7d 100644 --- a/tests/dataset/test_dependencies.py +++ b/tests/dataset/test_dependencies.py @@ -24,7 +24,7 @@ def test_wrong_input_raises() -> None: ["p1", ParamSpec("p2", paramtype="text")], ): with pytest.raises(ValueError): - InterDependencies(pspecs) # type: ignore[arg-type] + InterDependencies(pspecs) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_init(some_paramspecbases) -> None: @@ -78,18 +78,18 @@ def test_init_validation_raises(some_paramspecbases) -> None: for tree, cause in zip(invalid_trees, causes): with pytest.raises(ValueError, match="Invalid dependencies") as ei: - InterDependencies_(dependencies=tree, inferences={}) # type: ignore[arg-type] + InterDependencies_(dependencies=tree, inferences={}) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert error_caused_by(ei, cause=cause) for tree, cause in zip(invalid_trees, causes): with pytest.raises(ValueError, match="Invalid inferences") as ei: - InterDependencies_(dependencies={}, inferences=tree) # type: ignore[arg-type] + InterDependencies_(dependencies={}, inferences=tree) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert error_caused_by(ei, cause=cause) with pytest.raises(ValueError, match="Invalid standalones") as ei: - InterDependencies_(standalones=("ps1", "ps2")) # type: ignore[arg-type] + InterDependencies_(standalones=("ps1", "ps2")) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert error_caused_by(ei, cause="Standalones must be a sequence of ParamSpecs") @@ -101,8 +101,8 @@ def test_init_validation_raises(some_paramspecbases) -> None: for inv in invalid_trees_2: with pytest.raises(ValueError, match="already exists"): InterDependencies_( - dependencies=inv["deps"], # type: ignore[arg-type] - inferences=inv["inffs"], # type: ignore[arg-type] + dependencies=inv["deps"], # type: ignore[arg-type] # ty: ignore[invalid-argument-type] + inferences=inv["inffs"], # type: ignore[arg-type] # ty: ignore[invalid-argument-type] ) diff --git a/tests/dataset/test_descriptions.py b/tests/dataset/test_descriptions.py index 248c52ebfc9c..ec03c7cfa011 100644 --- a/tests/dataset/test_descriptions.py +++ b/tests/dataset/test_descriptions.py @@ -12,7 +12,7 @@ def test_wrong_input_type_raises() -> None: for interdeps in ("interdeps", ["p1", "p2"], 0): with pytest.raises(ValueError): - RunDescriber(interdeps=interdeps) # type: ignore[arg-type] + RunDescriber(interdeps=interdeps) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_equality(some_paramspecbases) -> None: diff --git a/tests/dataset/test_measurement_extensions.py b/tests/dataset/test_measurement_extensions.py index 56fb450cc392..f9cc26a1c383 100644 --- a/tests/dataset/test_measurement_extensions.py +++ b/tests/dataset/test_measurement_extensions.py @@ -334,7 +334,7 @@ def test_dond_into_fails_with_together_sweeps( dond_into( datasaver, - TogetherSweep(sweep1, sweep2), # pyright: ignore [reportArgumentType] + TogetherSweep(sweep1, sweep2), # pyright: ignore [reportArgumentType] # ty: ignore[invalid-argument-type] meas1, ) _ = datasaver.dataset @@ -355,8 +355,8 @@ def test_dond_into_fails_with_groups(default_params, default_database_and_experi dond_into( datasaver, sweep1, - [meas1], # pyright: ignore [reportArgumentType] - [meas2], # pyright: ignore [reportArgumentType] + [meas1], # pyright: ignore [reportArgumentType] # ty: ignore[invalid-argument-type] + [meas2], # pyright: ignore [reportArgumentType] # ty: ignore[invalid-argument-type] ) _ = datasaver.dataset @@ -502,7 +502,7 @@ def test_shapes_in_dataset_definition_with_scalar_params( _ = default_database_and_experiment set1, set2, _, meas1, meas2, _ = default_params - expected_shapes = { + expected_shapes: dict[str, tuple[int, ...]] = { meas1.register_name: (11, 11), meas2.register_name: (11, 11), } @@ -530,7 +530,7 @@ def test_shapes_in_dataset_definition_with_pws( _ = default_database_and_experiment pws1, set1 = pws_params - expected_shapes = { + expected_shapes: dict[str, tuple[int, ...]] = { pws1.register_name: (11, 11), } dataset_definition = [ @@ -576,7 +576,7 @@ def test_setup_measurement_instances_sets_shapes( _ = default_database_and_experiment set1, _, _, meas1, _, _ = default_params - expected_shapes = {meas1.register_name: (5,)} + expected_shapes: dict[str, tuple[int, ...]] = {meas1.register_name: (5,)} dataset_definitions = [ DataSetDefinition( name="test_shapes", @@ -614,8 +614,8 @@ def test_shapes_with_multiple_datasets(default_params, default_database_and_expe _ = default_database_and_experiment set1, set2, set3, meas1, _, meas3 = default_params - shapes_1 = {meas1.register_name: (11, 11)} - shapes_2 = {meas3.register_name: (11, 11)} + shapes_1: dict[str, tuple[int, ...]] = {meas1.register_name: (11, 11)} + shapes_2: dict[str, tuple[int, ...]] = {meas3.register_name: (11, 11)} dataset_definition = [ DataSetDefinition( name="dataset_1", diff --git a/tests/dataset/test_parameter_with_setpoints_has_control.py b/tests/dataset/test_parameter_with_setpoints_has_control.py index 08e641a89bdb..23abb9132f44 100644 --- a/tests/dataset/test_parameter_with_setpoints_has_control.py +++ b/tests/dataset/test_parameter_with_setpoints_has_control.py @@ -29,7 +29,7 @@ def unpack_self(self, value): # type: ignore[override] res.append((controlled, controlled())) return res - p = _ControlledSetpoints(name, **kwargs) # type: ignore[arg-type] + p = _ControlledSetpoints(name, **kwargs) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] p.has_control_of.add(controlled) return p diff --git a/tests/dataset/test_paramspec.py b/tests/dataset/test_paramspec.py index ed344a012032..fea4b0e957ff 100644 --- a/tests/dataset/test_paramspec.py +++ b/tests/dataset/test_paramspec.py @@ -107,7 +107,7 @@ def test_creation(name, sp1, sp2, inff1, inff2, paramtype) -> None: invalid_types = ("np.array", "ndarray", "lala", "", Number, ndarray, 0, None) for inv_type in invalid_types: with pytest.raises(ValueError): - ParamSpec(name, inv_type) # type: ignore[arg-type] + ParamSpec(name, inv_type) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] if not inff1.isidentifier(): inff1 = "inff1" diff --git a/tests/dataset/test_snapshot.py b/tests/dataset/test_snapshot.py index b723033574e0..058bcb927543 100644 --- a/tests/dataset/test_snapshot.py +++ b/tests/dataset/test_snapshot.py @@ -58,7 +58,7 @@ def test_station_snapshot_during_measurement( # 1. Test `get_metadata('snapshot')` method # this is not part of the DatasetProtocol interface # but we test it anyway - json_snapshot_from_dataset = data_saver.dataset.get_metadata("snapshot") # type: ignore[attr-defined] + json_snapshot_from_dataset = data_saver.dataset.get_metadata("snapshot") # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] snapshot_from_dataset = json.loads(json_snapshot_from_dataset) expected_snapshot = { @@ -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/dataset/test_sqlite_base.py b/tests/dataset/test_sqlite_base.py index d01b40f41861..a6bf8865617d 100644 --- a/tests/dataset/test_sqlite_base.py +++ b/tests/dataset/test_sqlite_base.py @@ -54,7 +54,7 @@ def _make_simple_run_describer(): t = ParamSpecBase("t", "numeric") y = ParamSpecBase("y", "numeric") - paramtree = {y: (x, t)} + paramtree: dict[ParamSpecBase, tuple[ParamSpecBase, ...]] = {y: (x, t)} interdependencies = InterDependencies_(dependencies=paramtree) rundescriber = RunDescriber(interdependencies) diff --git a/tests/dataset/test_sqlite_connection.py b/tests/dataset/test_sqlite_connection.py index 62eb6e33b4b4..30b0ef2a3505 100644 --- a/tests/dataset/test_sqlite_connection.py +++ b/tests/dataset/test_sqlite_connection.py @@ -58,7 +58,7 @@ def test_atomic_raises_for_non_atomic_conn() -> None: "atomic context manager only accepts AtomicConnection " "database connection objects." ) - with pytest.raises(ValueError, match=match_str), atomic(sqlite_conn): # type: ignore[arg-type] + with pytest.raises(ValueError, match=match_str), atomic(sqlite_conn): # type: ignore[arg-type] # ty: ignore[invalid-argument-type] pass @@ -295,7 +295,7 @@ def test_atomic_transaction_on_sqlite3_connection_raises(tmp_path) -> None: ) with pytest.raises(ValueError, match=match_str): - atomic_transaction(conn, "whatever sql query") # type: ignore[arg-type] + atomic_transaction(conn, "whatever sql query") # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_connect() -> None: diff --git a/tests/dataset/test_string_data.py b/tests/dataset/test_string_data.py index 928ffbd70698..ffabb4412a3c 100644 --- a/tests/dataset/test_string_data.py +++ b/tests/dataset/test_string_data.py @@ -138,7 +138,7 @@ def test_string_with_wrong_paramtype_via_datasaver() -> None: with pytest.raises(ValueError, match=msg): data_saver.add_result(("p", "some text")) finally: - data_saver.dataset.conn.close() # type: ignore[attr-defined] + data_saver.dataset.conn.close() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] @pytest.mark.usefixtures("experiment") @@ -194,12 +194,12 @@ def test_list_of_strings(experiment) -> None: try: np.testing.assert_array_equal(actual_data, expec_data) finally: - test_set.conn.close() # type: ignore[attr-defined] + test_set.conn.close() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] @settings(suppress_health_check=(HealthCheck.function_scoped_fixture,), deadline=None) @given( - p_values=hypnumpy.arrays( + p_values=hypnumpy.arrays( # ty: ignore[no-matching-overload] dtype=hst.sampled_from( ( hypnumpy.unicode_string_dtypes(), diff --git a/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1517a_smu.py b/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1517a_smu.py index b66a514cceb4..6555efdb0025 100644 --- a/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1517a_smu.py +++ b/tests/drivers/keysight_b1500/b1500_driver_tests/test_b1517a_smu.py @@ -34,10 +34,10 @@ def test_snapshot() -> None: # We need to use `InstrumentBase` (not a bare mock) in order for # `snapshot` methods call resolution to work out mainframe = InstrumentBase(name="mainframe") - mainframe.write = MagicMock() # type: ignore[attr-defined] + mainframe.write = MagicMock() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] slot_nr = 1 smu = KeysightB1517A( - parent=mainframe, # type: ignore[arg-type] + parent=mainframe, # type: ignore[arg-type] # ty: ignore[invalid-argument-type] name="B1517A", slot_nr=slot_nr, ) diff --git a/tests/drivers/test_MercuryiPS.py b/tests/drivers/test_MercuryiPS.py index f6fada36f905..3b6d94d9fad9 100644 --- a/tests/drivers/test_MercuryiPS.py +++ b/tests/drivers/test_MercuryiPS.py @@ -92,7 +92,7 @@ def test_wrong_field_limit_raises() -> None: "mips", address="GPIB::1::INSTR", pyvisa_sim_file="MercuryiPS.yaml", - field_limits=0, # type: ignore[arg-type] + field_limits=0, # type: ignore[arg-type] # ty: ignore[invalid-argument-type] ) diff --git a/tests/drivers/test_ami430_visa.py b/tests/drivers/test_ami430_visa.py index 596c6a80da05..dbbca49f915a 100644 --- a/tests/drivers/test_ami430_visa.py +++ b/tests/drivers/test_ami430_visa.py @@ -414,7 +414,7 @@ def test_instantiation_from_badly_typed_argument( "AMI430_3D", mag_x.name, mag_y, - badly_typed_instrument_z_argument, # type: ignore[arg-type] + badly_typed_instrument_z_argument, # type: ignore[arg-type] # ty: ignore[invalid-argument-type] field_limit, ) @@ -1867,7 +1867,7 @@ def test_3d_driver_invalid_field_limit_type( mag_x, mag_y, mag_z = magnet_axes_instances request.addfinalizer(Instrument.close_all) with pytest.raises(ValueError, match="field limit should either be a number"): - AMIModel4303D("AMI430_3D", mag_x, mag_y, mag_z, None) # type: ignore[arg-type] + AMIModel4303D("AMI430_3D", mag_x, mag_y, mag_z, None) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_ramp_simultaneously(current_driver: AMIModel4303D) -> None: diff --git a/tests/drivers/test_lakeshore_372.py b/tests/drivers/test_lakeshore_372.py index b6085bc3b5f9..739b42d09444 100644 --- a/tests/drivers/test_lakeshore_372.py +++ b/tests/drivers/test_lakeshore_372.py @@ -36,7 +36,7 @@ class MockVisaInstrument: def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - self.visa_log = get_instrument_logger(self, VISA_LOGGER) # type: ignore[arg-type] + self.visa_log = get_instrument_logger(self, VISA_LOGGER) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] # This base class mixin holds two dictionaries associated with the # pyvisa_instrument.write() @@ -74,7 +74,7 @@ def write_raw(self, cmd) -> None: self.visa_log.debug(f"Query: {cmd} for command {cmd_str} with args {args}") self.cmds[cmd_str](args) else: - super().write_raw(cmd) # type: ignore[misc] + super().write_raw(cmd) # type: ignore[misc] # ty: ignore[unresolved-attribute] def ask_raw(self, cmd) -> Any: query_parts = cmd.split(" ") @@ -88,12 +88,12 @@ def ask_raw(self, cmd) -> Any: self.visa_log.debug(f"Response: {response}") return response else: - return super().ask_raw(cmd) # type: ignore[misc] + return super().ask_raw(cmd) # type: ignore[misc] # ty: ignore[unresolved-attribute] def query(name: str) -> Callable[[Callable[P, T]], Callable[P, T]]: def wrapper(func: Callable[P, T]) -> Callable[P, T]: - func.query_name = name.upper() # type: ignore[attr-defined] + func.query_name = name.upper() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] return func return wrapper @@ -101,7 +101,7 @@ def wrapper(func: Callable[P, T]) -> Callable[P, T]: def command(name: str) -> Callable[[Callable[P, T]], Callable[P, T]]: def wrapper(func: Callable[P, T]) -> Callable[P, T]: - func.command_name = name.upper() # type: ignore[attr-defined] + func.command_name = name.upper() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] return func return wrapper diff --git a/tests/drivers/test_tektronix_AWG70000A.py b/tests/drivers/test_tektronix_AWG70000A.py index 99c955aefaf4..badaceb8222e 100644 --- a/tests/drivers/test_tektronix_AWG70000A.py +++ b/tests/drivers/test_tektronix_AWG70000A.py @@ -175,7 +175,7 @@ def test_seqxfilefromfs_failing(forged_sequence) -> None: forged_sequence, [1, 1, 1], seqname="dummyname", - channel_mapping={1: None, 3: None}, # type: ignore[dict-item] + channel_mapping={1: None, 3: None}, # type: ignore[dict-item] # ty: ignore[invalid-argument-type] ) # wrong channel mapping values diff --git a/tests/helpers/test_delegate_attribues.py b/tests/helpers/test_delegate_attribues.py index 69d54084cf10..300c4871054e 100644 --- a/tests/helpers/test_delegate_attribues.py +++ b/tests/helpers/test_delegate_attribues.py @@ -20,7 +20,7 @@ class ToDict(DelegateAttributes): assert td.apples == "green" d = {"apples": "red", "oranges": "orange"} - td.d = d # type: ignore[attr-defined] + td.d = d # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] # you can get the whole dict still assert td.d == d @@ -52,7 +52,7 @@ class ToDicts(DelegateAttributes): td = ToDicts() e = {"cats": 12, "dogs": 3} - td.e = e # type: ignore[attr-defined] + td.e = e # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] # you can still access the second one when the first doesn't exist with pytest.raises(AttributeError): @@ -61,7 +61,7 @@ class ToDicts(DelegateAttributes): assert td.cats == 12 # the first beats out the second - td.d = {"cats": 42, "chickens": 1000} # type: ignore[attr-defined] + td.d = {"cats": 42, "chickens": 1000} # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert td.cats == 42 # but you can still access things only in the second @@ -89,14 +89,14 @@ class ToObject(DelegateAttributes): _ = to_obj.recipient assert to_obj.gray == "#888" - to_obj.recipient = recipient # type: ignore[attr-defined] + to_obj.recipient = recipient # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] # now you can access recipient through to_obj assert to_obj.black == "#000" # to_obj overrides but you can still access other recipient attributes # "soft" black - to_obj.black = "#444" # type: ignore[attr-defined] + to_obj.black = "#444" # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert to_obj.black == "#444" assert to_obj.white == "#fff" diff --git a/tests/helpers/test_strip_attrs.py b/tests/helpers/test_strip_attrs.py index 5dc8e39ddbd5..35f8c6cf4d92 100644 --- a/tests/helpers/test_strip_attrs.py +++ b/tests/helpers/test_strip_attrs.py @@ -23,7 +23,7 @@ def __delitem__(self, item): def test_normal() -> None: a = A() a.x = 15 - a.z = 25 # type: ignore[attr-defined] + a.z = 25 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] strip_attrs(a) @@ -38,13 +38,13 @@ def test_pathological() -> None: a = A() a.__dict__ = BadKeysDict() - a.fruit = "mango" # type: ignore[attr-defined] + a.fruit = "mango" # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] with pytest.raises(RuntimeError): a.__dict__.keys() strip_attrs(a) # no error, but the attribute is still there - assert a.fruit == "mango" # type: ignore[attr-defined] + assert a.fruit == "mango" # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] a = A() a.__dict__ = NoDelDict() diff --git a/tests/parameter/test_array_parameter.py b/tests/parameter/test_array_parameter.py index 8d9ff7cc75e5..5f6a0831d3b9 100644 --- a/tests/parameter/test_array_parameter.py +++ b/tests/parameter/test_array_parameter.py @@ -167,13 +167,13 @@ def test_full_name() -> None: # this is not allowed since instrument # here is not actually an instrument # but useful for testing - p._instrument = instrument # type: ignore[assignment] + p._instrument = instrument # type: ignore[assignment] # ty: ignore[invalid-assignment] assert str(p) == "fred" assert p.setpoint_full_names == ("barney",) # and then an instrument that really has a name p = SimpleArrayParam([6, 7], "wilma", shape=(2,), setpoint_names=("betty",)) - p._instrument = named_instrument # type: ignore[assignment] + p._instrument = named_instrument # type: ignore[assignment] # ty: ignore[invalid-assignment] assert str(p) == "astro_wilma" assert p.setpoint_full_names == ("astro_betty",) @@ -181,7 +181,7 @@ def test_full_name() -> None: p = SimpleArrayParam( [[6, 7, 8], [1, 2, 3]], "wilma", shape=(3, 2), setpoint_names=("betty", None) ) - p._instrument = named_instrument # type: ignore[assignment] + p._instrument = named_instrument # type: ignore[assignment] # ty: ignore[invalid-assignment] assert p.setpoint_full_names == ("astro_betty", None) diff --git a/tests/parameter/test_delegate_parameter.py b/tests/parameter/test_delegate_parameter.py index 55278f3f9aae..1fa997c1bb8e 100644 --- a/tests/parameter/test_delegate_parameter.py +++ b/tests/parameter/test_delegate_parameter.py @@ -84,7 +84,7 @@ def get_cmd() -> Any: get_cmd=get_cmd, ) param = cast("ObservableParam", p) - param.get_instr_val = get_cmd # type: ignore[method-assign] + param.get_instr_val = get_cmd # type: ignore[method-assign] # ty: ignore[invalid-assignment] return param yield make_parameter @@ -142,7 +142,7 @@ def test_get_set_raises(simple_param: Parameter) -> None: """ for kwargs in ({"set_cmd": None}, {"get_cmd": None}): with pytest.raises(KeyError) as e: - DelegateParameter("test_delegate_parameter", source=simple_param, **kwargs) # type: ignore[arg-type] + DelegateParameter("test_delegate_parameter", source=simple_param, **kwargs) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert str(e.value).startswith("'It is not allowed to set") @@ -320,7 +320,7 @@ def test_delegate_parameter_get_and_snapshot_with_none_source() -> None: assert delegate_param.get() is None assert delegate_param.snapshot()["value"] is None - parameter = delegate_param.cache._parameter # type: ignore[attr-defined] + parameter = delegate_param.cache._parameter # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert parameter.source.cache is none_param.cache delegate_param.source = source_param assert delegate_param.get() == 1 @@ -420,6 +420,7 @@ def test_delegate_parameter_with_changed_source_snapshot_matches_value( delegate_param.source = source_parameter calc_value = (value - offset) / scale assert delegate_param.cache.get(get_if_invalid=False) == calc_value + assert delegate_param.source is not None assert delegate_param.source.cache.get(get_if_invalid=False) == value snapshot = delegate_param.snapshot() # disregard timestamp that might be slightly different diff --git a/tests/parameter/test_elapsed_time_parameter.py b/tests/parameter/test_elapsed_time_parameter.py index 3add38e4515f..f5dc73174f3d 100644 --- a/tests/parameter/test_elapsed_time_parameter.py +++ b/tests/parameter/test_elapsed_time_parameter.py @@ -50,4 +50,4 @@ def test_elapsed_time_parameter_forbidden_kwargs() -> None: for fb_kwarg in forbidden_kwargs: match = f'Can not set "{fb_kwarg}" for an ElapsedTimeParameter' with pytest.raises(ValueError, match=match): - ElapsedTimeParameter("time", **{fb_kwarg: None}) # type: ignore[arg-type] + ElapsedTimeParameter("time", **{fb_kwarg: None}) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] diff --git a/tests/parameter/test_get_latest.py b/tests/parameter/test_get_latest.py index 856f7443933f..578b1088203c 100644 --- a/tests/parameter/test_get_latest.py +++ b/tests/parameter/test_get_latest.py @@ -64,8 +64,8 @@ def test_get_latest_unknown() -> None: local_parameter = BetterGettableParam("test_param", set_cmd=None, get_cmd=None) # fake a parameter that has a value but never been get/set to mock # an instrument. - local_parameter.cache._value = value # type: ignore[attr-defined] - local_parameter.cache._raw_value = value # type: ignore[attr-defined] + local_parameter.cache._value = value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] + local_parameter.cache._raw_value = value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert local_parameter.get_latest.get_timestamp() is None before_get = datetime.now(UTC) assert local_parameter._get_count == 0 @@ -169,7 +169,7 @@ def test_no_get_max_val_age() -> None: class LocalParameter(ParameterBase): def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) - self.set_raw = lambda x: x # type: ignore[method-assign] + self.set_raw = lambda x: x # type: ignore[method-assign] # ty: ignore[invalid-assignment] self.set = self._wrap_set(self.set_raw) localparameter = LocalParameter("test_param", instrument=None, max_val_age=1) diff --git a/tests/parameter/test_get_set_wrapping.py b/tests/parameter/test_get_set_wrapping.py index 3bbe3416d5bd..7938d72c0d77 100644 --- a/tests/parameter/test_get_set_wrapping.py +++ b/tests/parameter/test_get_set_wrapping.py @@ -69,7 +69,7 @@ def test_gettable_settable_attributes_with_get_set_raw( """Test that parameters that have get_raw,set_raw are listed as gettable/settable and reverse.""" - class GetSetParam(baseclass): # type: ignore[valid-type,misc] + class GetSetParam(baseclass): # type: ignore[valid-type,misc] # ty: ignore[unsupported-base] def __init__(self, *args: Any, initial_value: Any = None, **kwargs: Any): self._value = initial_value super().__init__(*args, **kwargs) diff --git a/tests/parameter/test_group_parameter.py b/tests/parameter/test_group_parameter.py index 9464b031e1cc..931225489b7d 100644 --- a/tests/parameter/test_group_parameter.py +++ b/tests/parameter/test_group_parameter.py @@ -112,7 +112,7 @@ def test_raise_on_get_set_cmd() -> None: kwarg = {arg: ""} with pytest.raises(ValueError) as e: - GroupParameter(name="a", **kwarg) # type: ignore[arg-type] + GroupParameter(name="a", **kwarg) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert ( str(e.value) == "A GroupParameter does not use 'set_cmd' or 'get_cmd' kwarg" diff --git a/tests/parameter/test_issequenceof.py b/tests/parameter/test_issequenceof.py index 7c710473dfe3..12ea03866acc 100644 --- a/tests/parameter/test_issequenceof.py +++ b/tests/parameter/test_issequenceof.py @@ -36,9 +36,9 @@ def test_simple_bad(args) -> None: def test_examples_raises() -> None: with pytest.raises(TypeError): - is_sequence_of([1], 1) # type: ignore[arg-type] + is_sequence_of([1], 1) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises(TypeError): - is_sequence_of([1], (1, 2)) # type: ignore[arg-type] + is_sequence_of([1], (1, 2)) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] good_depth = [ diff --git a/tests/parameter/test_keyword_only_args.py b/tests/parameter/test_keyword_only_args.py index 3ae82f64cdb2..99c2bffc5528 100644 --- a/tests/parameter/test_keyword_only_args.py +++ b/tests/parameter/test_keyword_only_args.py @@ -42,7 +42,7 @@ def test_keyword_args_work(self) -> None: def test_positional_args_rejected(self) -> None: with pytest.raises(TypeError): - _ConcreteParameterBase("test", None) # type: ignore[misc] + _ConcreteParameterBase("test", None) # type: ignore[misc] # ty: ignore[too-many-positional-arguments] class TestParameterKeywordOnly: @@ -55,7 +55,7 @@ def test_keyword_args_work(self) -> None: def test_positional_args_rejected(self) -> None: with pytest.raises(TypeError): - Parameter("test", None, set_cmd=None) # type: ignore[misc] + Parameter("test", None, set_cmd=None) # type: ignore[misc] # ty: ignore[too-many-positional-arguments] # Minimal concrete subclass of ArrayParameter for testing @@ -74,11 +74,11 @@ def test_keyword_args_work(self) -> None: def test_positional_args_rejected(self) -> None: with pytest.raises(TypeError): - _ConcreteArrayParameter("test", (3,)) # type: ignore[misc] + _ConcreteArrayParameter("test", (3,)) # type: ignore[misc] # ty: ignore[missing-argument, too-many-positional-arguments] def test_missing_shape_raises(self) -> None: with pytest.raises(TypeError): - _ConcreteArrayParameter("test") # type: ignore[misc] + _ConcreteArrayParameter("test") # type: ignore[misc] # ty: ignore[missing-argument] class TestGroupParameterKeywordOnly: @@ -90,7 +90,7 @@ def test_keyword_args_work(self) -> None: def test_positional_args_rejected(self) -> None: with pytest.raises(TypeError): - GroupParameter("test", None) # type: ignore[misc] + GroupParameter("test", None) # type: ignore[misc] # ty: ignore[too-many-positional-arguments] def _make_delegate_group() -> DelegateGroup: @@ -112,11 +112,11 @@ def test_keyword_args_work(self) -> None: def test_positional_args_rejected(self) -> None: grp = _make_delegate_group() with pytest.raises(TypeError): - GroupedParameter("test", grp) # type: ignore[misc] + GroupedParameter("test", grp) # type: ignore[misc] # ty: ignore[missing-argument, too-many-positional-arguments] def test_missing_group_raises(self) -> None: with pytest.raises(TypeError): - GroupedParameter("test") # type: ignore[misc] + GroupedParameter("test") # type: ignore[misc] # ty: ignore[missing-argument] # Minimal concrete subclass of MultiParameter for testing @@ -135,15 +135,15 @@ def test_keyword_args_work(self) -> None: def test_positional_args_rejected(self) -> None: with pytest.raises(TypeError): - _ConcreteMultiParameter("test", ("a",), ((),)) # type: ignore[misc] + _ConcreteMultiParameter("test", ("a",), ((),)) # type: ignore[misc] # ty: ignore[missing-argument, too-many-positional-arguments] def test_missing_names_raises(self) -> None: with pytest.raises(TypeError): - _ConcreteMultiParameter("test", shapes=((),)) # type: ignore[misc] + _ConcreteMultiParameter("test", shapes=((),)) # type: ignore[misc] # ty: ignore[missing-argument] def test_missing_shapes_raises(self) -> None: with pytest.raises(TypeError): - _ConcreteMultiParameter("test", names=("a",)) # type: ignore[misc] + _ConcreteMultiParameter("test", names=("a",)) # type: ignore[misc] # ty: ignore[missing-argument] class TestMultiChannelInstrumentParameterKeywordOnly: @@ -157,8 +157,8 @@ def test_keyword_args_work(self) -> None: def test_positional_args_rejected(self) -> None: with pytest.raises(TypeError): - MultiChannelInstrumentParameter( - [], # pyright: ignore[reportCallIssue] + MultiChannelInstrumentParameter( # ty: ignore[missing-argument] + [], # pyright: ignore[reportCallIssue] # ty: ignore[too-many-positional-arguments] "x", name="test", names=("a",), @@ -167,13 +167,13 @@ def test_positional_args_rejected(self) -> None: def test_missing_channels_raises(self) -> None: with pytest.raises(TypeError): - MultiChannelInstrumentParameter( # type: ignore[call-arg] + MultiChannelInstrumentParameter( # type: ignore[call-arg] # ty: ignore[missing-argument] param_name="x", name="test", names=("a",), shapes=((),) ) def test_missing_param_name_raises(self) -> None: with pytest.raises(TypeError): - MultiChannelInstrumentParameter( # type: ignore[call-arg] + MultiChannelInstrumentParameter( # type: ignore[call-arg] # ty: ignore[missing-argument] channels=[], name="test", names=("a",), shapes=((),) ) @@ -188,7 +188,7 @@ def test_keyword_args_work(self) -> None: def test_positional_args_rejected(self) -> None: with pytest.raises(TypeError): - ElapsedTimeParameter("test", "My label") # type: ignore[misc] + ElapsedTimeParameter("test", "My label") # type: ignore[misc] # ty: ignore[too-many-positional-arguments] class TestInstrumentRefParameterKeywordOnly: @@ -201,4 +201,4 @@ def test_keyword_args_work(self) -> None: def test_positional_args_rejected(self) -> None: with pytest.raises(TypeError): - InstrumentRefParameter("test", None) # type: ignore[misc] + InstrumentRefParameter("test", None) # type: ignore[misc] # ty: ignore[too-many-positional-arguments] diff --git a/tests/parameter/test_manual_parameter.py b/tests/parameter/test_manual_parameter.py index f1e21c505435..c26d3da586aa 100644 --- a/tests/parameter/test_manual_parameter.py +++ b/tests/parameter/test_manual_parameter.py @@ -27,4 +27,4 @@ def test_manual_parameter_forbidden_kwargs() -> None: for fk in forbidden_kwargs: match = f'It is not allowed to set "{fk}" for a ManualParameter' with pytest.raises(ValueError, match=match): - ManualParameter("test", **{fk: None}) # type: ignore[arg-type] + ManualParameter("test", **{fk: None}) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] diff --git a/tests/parameter/test_multi_parameter.py b/tests/parameter/test_multi_parameter.py index 5e60778a9657..fd5580bc0aee 100644 --- a/tests/parameter/test_multi_parameter.py +++ b/tests/parameter/test_multi_parameter.py @@ -211,7 +211,7 @@ def test_full_name_s() -> None: shapes=shapes, setpoint_names=setpoint_names, ) - p._instrument = instrument # type: ignore[assignment] + p._instrument = instrument # type: ignore[assignment] # ty: ignore[invalid-assignment] assert str(p) == name assert p.full_names == names assert p.setpoint_full_names == ( @@ -228,7 +228,7 @@ def test_full_name_s() -> None: shapes=shapes, setpoint_names=setpoint_names, ) - p._instrument = named_instrument # type: ignore[assignment] + p._instrument = named_instrument # type: ignore[assignment] # ty: ignore[invalid-assignment] assert str(p) == "astro_mixed_dimensions" assert p.full_names == ("astro_0D", "astro_1D", "astro_2D") diff --git a/tests/parameter/test_on_off_mapping.py b/tests/parameter/test_on_off_mapping.py index 2bb4df4f8ede..a9839576cbe7 100644 --- a/tests/parameter/test_on_off_mapping.py +++ b/tests/parameter/test_on_off_mapping.py @@ -37,14 +37,14 @@ def test_create_on_off_val_mapping_for( # this does not type check. However, hash(1) == hash(True) # so 1/0 behaves like True and False at runtime - assert val_mapping[1] is on_val # type: ignore[index] + assert val_mapping[1] is on_val # type: ignore[index] # ty: ignore[invalid-argument-type] assert val_mapping[True] is on_val assert val_mapping["1"] is on_val assert val_mapping["ON"] is on_val assert val_mapping["On"] is on_val assert val_mapping["on"] is on_val - assert val_mapping[0] is off_val # type: ignore[index] + assert val_mapping[0] is off_val # type: ignore[index] # ty: ignore[invalid-argument-type] assert val_mapping[False] is off_val assert val_mapping["0"] is off_val assert val_mapping["OFF"] is off_val diff --git a/tests/parameter/test_parameter_basics.py b/tests/parameter/test_parameter_basics.py index 18a5446e1370..aa2356f049e3 100644 --- a/tests/parameter/test_parameter_basics.py +++ b/tests/parameter/test_parameter_basics.py @@ -15,7 +15,7 @@ def test_no_name() -> None: with pytest.raises(TypeError): - Parameter() # type: ignore[call-arg] + Parameter() # type: ignore[call-arg] # ty: ignore[missing-argument] def test_default_attributes() -> None: @@ -155,12 +155,12 @@ def test_str_representation() -> None: # three cases where only name gets used for full_name for instrument in blank_instruments: p = Parameter(name="fred") - p._instrument = instrument # type: ignore[assignment] + p._instrument = instrument # type: ignore[assignment] # ty: ignore[invalid-assignment] assert str(p) == "fred" # and finally an instrument that really has a name p = Parameter(name="wilma") - p._instrument = named_instrument # type: ignore[assignment] + p._instrument = named_instrument # type: ignore[assignment] # ty: ignore[invalid-assignment] assert str(p) == "astro_wilma" @@ -196,14 +196,14 @@ def test_parameter_call() -> None: assert p() == 1 with pytest.raises(TypeError, match="takes 1 positional argument but 2 were given"): - p(1, 2) # type: ignore[call-overload] + p(1, 2) # type: ignore[call-overload] # ty: ignore[no-matching-overload] p(value=2) assert p() == 2 with pytest.raises(TypeError, match="got multiple values for argument"): - p(2, value=2) # type: ignore[call-overload] + p(2, value=2) # type: ignore[call-overload] # ty: ignore[no-matching-overload] def test_parameter_set_extra_kwargs() -> None: @@ -243,13 +243,13 @@ def test_unknown_args_to_baseparameter_raises() -> None: _ = ParameterBase( name="Foo", instrument=None, - snapshotable=False, # type: ignore[call-arg] + snapshotable=False, # type: ignore[call-arg] # ty: ignore[unknown-argument] ) def test_underlying_instrument_for_virtual_parameter() -> None: p = GettableParam("base_param", vals=vals.Numbers()) - p._instrument = named_instrument # type: ignore[assignment] + p._instrument = named_instrument # type: ignore[assignment] # ty: ignore[invalid-assignment] vp = VirtualParameter("test_param", param=p) assert vp.underlying_instrument is named_instrument # type: ignore[comparison-overlap] diff --git a/tests/parameter/test_parameter_cache.py b/tests/parameter/test_parameter_cache.py index 159cf3cbd67a..78d215f74610 100644 --- a/tests/parameter/test_parameter_cache.py +++ b/tests/parameter/test_parameter_cache.py @@ -100,8 +100,8 @@ def test_get_cache_unknown() -> None: local_parameter = BetterGettableParam("test_param", set_cmd=None, get_cmd=None) # fake a parameter that has a value but never been get/set to mock # an instrument. - local_parameter.cache._value = value # type: ignore[attr-defined] - local_parameter.cache._raw_value = value # type: ignore[attr-defined] + local_parameter.cache._value = value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] + local_parameter.cache._raw_value = value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert local_parameter.cache.timestamp is None before_get = datetime.now(UTC) assert local_parameter._get_count == 0 @@ -228,7 +228,7 @@ def test_no_get_max_val_age_runtime_error( class LocalParameter(ParameterBase): def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) - self.set_raw = lambda x: x # type: ignore[method-assign] + self.set_raw = lambda x: x # type: ignore[method-assign] # ty: ignore[invalid-assignment] self.set = self._wrap_set(self.set_raw) local_parameter = LocalParameter("test_param", instrument=None, max_val_age=1) @@ -268,13 +268,13 @@ def test_no_get_timestamp_none_runtime_error( def test_latest_dictionary_gets_updated_upon_set_of_memory_parameter() -> None: p = Parameter("p", set_cmd=None, get_cmd=None) - assert p.cache._value is None # type: ignore[attr-defined] + assert p.cache._value is None # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert p.cache.raw_value is None assert p.cache.timestamp is None p(42) - assert p.cache._value == 42 # type: ignore[attr-defined] + assert p.cache._value == 42 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert p.cache.raw_value == 42 assert p.cache.timestamp is not None @@ -320,7 +320,7 @@ def test_set_latest_works_for_plain_memory_parameter( assert p.raw_value == raw_value # Assert latest value and raw_value via private attributes for strictness - assert p.cache._value == value # type: ignore[attr-defined] + assert p.cache._value == value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert p.cache.raw_value == raw_value # Now let's get the value of the parameter to ensure that the value that @@ -341,7 +341,7 @@ def test_set_latest_works_for_plain_memory_parameter( assert p.raw_value == raw_value # Assert latest value and raw_value via private attributes for strictness - assert p.cache._value == value # type: ignore[attr-defined] + assert p.cache._value == value # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert p.cache.raw_value == raw_value diff --git a/tests/parameter/test_parameter_context_manager.py b/tests/parameter/test_parameter_context_manager.py index a735844e52d5..4569f74d7913 100644 --- a/tests/parameter/test_parameter_context_manager.py +++ b/tests/parameter/test_parameter_context_manager.py @@ -137,7 +137,7 @@ def set_instr_value(value: Any) -> None: ) # pre-conditions - assert p.cache._value is None # type: ignore[attr-defined] + assert p.cache._value is None # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert p.cache.raw_value is None assert p.cache.timestamp is None assert set_counter == 0 diff --git a/tests/parameter/test_parameter_override.py b/tests/parameter/test_parameter_override.py index 77fe787a85ee..5837cf2b0f07 100644 --- a/tests/parameter/test_parameter_override.py +++ b/tests/parameter/test_parameter_override.py @@ -30,7 +30,7 @@ def __init__(self, name, **kwargs): This instrument errors because it tries to override an attribute with a parameter. """ super().__init__(name, **kwargs) - self.voltage = self.add_parameter("voltage", set_cmd=None, get_cmd=None) + self.voltage = self.add_parameter("voltage", set_cmd=None, get_cmd=None) # ty: ignore[invalid-assignment] def voltage(self): return 0 @@ -43,7 +43,7 @@ def __init__(self, name, **kwargs): to define an interface for the instrument. """ super().__init__(name, **kwargs) - self.add_parameter("voltage", set_cmd=None, get_cmd=None) # type: ignore + self.add_parameter("voltage", set_cmd=None, get_cmd=None) @property def voltage(self): diff --git a/tests/parameter/test_parameter_ramp.py b/tests/parameter/test_parameter_ramp.py index a1d49af003e3..450edcb53864 100644 --- a/tests/parameter/test_parameter_ramp.py +++ b/tests/parameter/test_parameter_ramp.py @@ -62,6 +62,7 @@ def test_ramp_scaled(scale, value) -> None: assert p.raw_value == first_step * scale # then check the generated steps. They should not be scaled as the # scaling happens when setting them + assert p.step is not None expected_steps = np.linspace(first_step + p.step, second_step, 90) actual_steps = p.get_ramp_values(second_step, p.step) np.testing.assert_allclose(np.array(actual_steps), expected_steps) @@ -104,6 +105,7 @@ def test_ramp_parser(value) -> None: assert p.raw_value == -first_step # then check the generated steps. They should not be parsed as the # scaling happens when setting them + assert p.step is not None expected_steps = np.linspace((first_step + p.step), second_step, 90) actual_steps = p.get_ramp_values(second_step, p.step) np.testing.assert_allclose(np.array(actual_steps), expected_steps) @@ -142,6 +144,7 @@ def test_ramp_parsed_scaled(scale, value) -> None: # these are parsed in the set_wrapper np.testing.assert_allclose(np.array(p.set_values), expected_raw_steps) assert p.raw_value == -scale * first_step + assert p.step is not None expected_steps = np.linspace(first_step + p.step, second_step, 90) actual_steps = p.get_ramp_values(10, p.step) np.testing.assert_allclose(np.array(actual_steps), expected_steps) diff --git a/tests/parameter/test_parameter_validation.py b/tests/parameter/test_parameter_validation.py index c25a69afc2cf..56058e6a6b43 100644 --- a/tests/parameter/test_parameter_validation.py +++ b/tests/parameter/test_parameter_validation.py @@ -39,7 +39,7 @@ def test_number_of_validations_for_set_cache() -> None: def test_bad_validator() -> None: with pytest.raises(TypeError): - Parameter("p", vals=[1, 2, 3]) # type:ignore[arg-type] + Parameter("p", vals=[1, 2, 3]) # type:ignore[arg-type] # ty: ignore[invalid-argument-type] def test_setting_int_with_float() -> None: diff --git a/tests/parameter/test_parameter_with_setpoints.py b/tests/parameter/test_parameter_with_setpoints.py index e2fabadcd73d..3ca76e0c1b3f 100644 --- a/tests/parameter/test_parameter_with_setpoints.py +++ b/tests/parameter/test_parameter_with_setpoints.py @@ -103,7 +103,7 @@ def test_setpoints_non_parameter_raises() -> None: param_with_setpoints_1 = ParameterWithSetpoints( "param_1", get_cmd=lambda: _rng.random(n_points_1()), - setpoints=(lambda x: x,), # type: ignore[arg-type] + setpoints=(lambda x: x,), # type: ignore[arg-type] # ty: ignore[invalid-argument-type] vals=vals.Arrays(shape=(n_points_1,)), ) @@ -114,7 +114,7 @@ def test_setpoints_non_parameter_raises() -> None: ) with pytest.raises(TypeError, match=err_msg): - param_with_setpoints_1.setpoints = (lambda x: x,) # type: ignore[assignment] + param_with_setpoints_1.setpoints = (lambda x: x,) # type: ignore[assignment] # ty: ignore[invalid-assignment] def test_validation_inconsistent_shape() -> None: @@ -322,7 +322,7 @@ def test_validation_one_dim_missing() -> None: "param_6", get_cmd=lambda: _rng.random(n_points_1()), setpoints=(setpoints_1,), - vals=vals.Arrays(shape=(n_points_1, None)), # type: ignore[arg-type] + vals=vals.Arrays(shape=(n_points_1, None)), # type: ignore[arg-type] # ty: ignore[invalid-argument-type] ) expected_err_msg = ( r"One or more dimensions have unknown shape " @@ -349,7 +349,7 @@ def test_validation_one_sp_dim_missing() -> None: setpoints_1 = Parameter( "setpoints_1", get_cmd=lambda: _rng.random(n_points_1()), - vals=vals.Arrays(shape=(n_points_1, None)), # type: ignore[arg-type] + vals=vals.Arrays(shape=(n_points_1, None)), # type: ignore[arg-type] # ty: ignore[invalid-argument-type] ) param_sp_without_shape = ParameterWithSetpoints( "param_6", diff --git a/tests/parameter/test_scaled_parameter.py b/tests/parameter/test_scaled_parameter.py index 9c9a0092eb21..2bcb9f6c5506 100644 --- a/tests/parameter/test_scaled_parameter.py +++ b/tests/parameter/test_scaled_parameter.py @@ -24,7 +24,7 @@ def _make_instrument() -> "Generator[DummyInstrument, None, None]": get_cmd=None, set_cmd=None, ) - instrument.scaler = ScaledParameter( # type: ignore[attr-defined] + instrument.scaler = ScaledParameter( # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] instrument.target_parameter, division=1 ) yield instrument @@ -37,7 +37,7 @@ def test_constructor(instrument: DummyInstrument) -> None: # Require a wrapped parameter with pytest.raises(TypeError): - ScaledParameter() # type: ignore[call-arg] + ScaledParameter() # type: ignore[call-arg] # ty: ignore[missing-argument] # Require a scaling factor with pytest.raises(ValueError): diff --git a/tests/parameter/test_snapshot.py b/tests/parameter/test_snapshot.py index af3f69bf8e1e..15c7ce4da31c 100644 --- a/tests/parameter/test_snapshot.py +++ b/tests/parameter/test_snapshot.py @@ -58,13 +58,13 @@ def wrapped_func(*args: P.args, **kwargs: P.kwargs) -> T: call_count += 1 return get_func(*args, **kwargs) - wrapped_func.call_count = lambda: call_count # type: ignore[attr-defined] + wrapped_func.call_count = lambda: call_count # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] return wrapped_func p.get = wrap_in_call_counter(p.get) # pre-condition - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] else: # pre-condition assert not hasattr(p, "get") @@ -161,7 +161,7 @@ def test_snapshot_timestamp_for_valid_cache_depends_on_cache_update( # Hack cache's timestamp to simplify this test timestamp = p.cache.timestamp assert timestamp is not None - p.cache._timestamp = timestamp - timedelta(days=31) # type: ignore[attr-defined] + p.cache._timestamp = timestamp - timedelta(days=31) # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] tu = datetime.now(UTC) assert p.cache.timestamp is not None @@ -242,7 +242,7 @@ def test_snapshot_when_snapshot_value_is_false( assert "raw_value" not in s if get_cmd is not False: - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def test_snapshot_value_is_true_by_default( @@ -297,7 +297,7 @@ def test_snapshot_when_snapshot_get_is_false( assert s["raw_value"] is None if get_cmd is not False: - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def test_snapshot_of_non_gettable_parameter_mirrors_cache( @@ -348,15 +348,15 @@ def test_snapshot_of_gettable_parameter_depends_on_update( if should_get: assert s["value"] == 65 assert s["raw_value"] == 69 - assert p.get.call_count() == 1 # type: ignore[attr-defined] + assert p.get.call_count() == 1 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] elif cache_is_valid: assert s["value"] == 42 assert s["raw_value"] == 46 - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] else: assert s["value"] is None assert s["raw_value"] is None - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def test_snapshot_value() -> None: @@ -391,7 +391,7 @@ def test_normalize_snapshot_update_maps_to_canonical_values() -> None: def test_normalize_snapshot_update_rejects_unknown_string() -> None: with pytest.raises(ValueError, match="Invalid value for snapshot"): - normalize_snapshot_update("bogus") # type: ignore[arg-type] + normalize_snapshot_update("bogus") # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_snapshot_update_all_always_calls_get() -> None: @@ -403,7 +403,7 @@ def test_snapshot_update_all_always_calls_get() -> None: ) s = p.snapshot(update="All") assert s["value"] == 69 - assert p.get.call_count() == 1 # type: ignore[attr-defined] + assert p.get.call_count() == 1 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def test_snapshot_update_never_never_calls_get() -> None: @@ -415,7 +415,7 @@ def test_snapshot_update_never_never_calls_get() -> None: ) s = p.snapshot(update="Never") assert s["value"] is None - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def test_snapshot_update_only_invalid_calls_get_when_cache_invalid() -> None: @@ -427,7 +427,7 @@ def test_snapshot_update_only_invalid_calls_get_when_cache_invalid() -> None: ) s = p.snapshot(update="Only_invalid") assert s["value"] == 69 - assert p.get.call_count() == 1 # type: ignore[attr-defined] + assert p.get.call_count() == 1 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def test_snapshot_update_only_invalid_skips_get_when_cache_valid() -> None: @@ -440,7 +440,7 @@ def test_snapshot_update_only_invalid_skips_get_when_cache_valid() -> None: s = p.snapshot(update="Only_invalid") # the cached (set) value is used, ``get`` is not called assert s["value"] == 42 - assert p.get.call_count() == 0 # type: ignore[attr-defined] + assert p.get.call_count() == 0 # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] @pytest.mark.parametrize( @@ -473,8 +473,8 @@ def test_snapshot_update_string_matches_legacy_value( assert s_legacy["value"] == s_string["value"] assert s_legacy["raw_value"] == s_string["raw_value"] assert ( - p_legacy.get.call_count() # type: ignore[attr-defined] - == p_string.get.call_count() # type: ignore[attr-defined] + p_legacy.get.call_count() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] + == p_string.get.call_count() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] ) @@ -486,4 +486,4 @@ def test_snapshot_rejects_unknown_update_value() -> None: cache_is_valid=True, ) with pytest.raises(ValueError, match="Invalid value for snapshot"): - p.snapshot(update="bogus") # type: ignore[arg-type] + p.snapshot(update="bogus") # type: ignore[arg-type] # ty: ignore[no-matching-overload] diff --git a/tests/sphinx_extension/test_parse_parameter_attr.py b/tests/sphinx_extension/test_parse_parameter_attr.py index a19d457eb544..b424fb07cafe 100644 --- a/tests/sphinx_extension/test_parse_parameter_attr.py +++ b/tests/sphinx_extension/test_parse_parameter_attr.py @@ -102,7 +102,7 @@ def test_decorated_init_func() -> None: def test_decorated_class() -> None: - attr = qcodes_parameter_attr_getter(DummyDecoratedClassTestClass, "other_attr") # pyright: ignore[reportDeprecated] + attr = qcodes_parameter_attr_getter(DummyDecoratedClassTestClass, "other_attr") # pyright: ignore[reportDeprecated] # ty: ignore[deprecated] assert isinstance(attr, ParameterProxy) assert repr(attr) == '"InstanceAttribute"' diff --git a/tests/test_channels.py b/tests/test_channels.py index 1f921b8b9968..ad85303c1c05 100644 --- a/tests/test_channels.py +++ b/tests/test_channels.py @@ -156,7 +156,7 @@ def test_invalid_channel_type_raises(empty_instrument: Instrument) -> None: ChannelList( parent=empty_instrument, name="empty", - chan_type=int, # type: ignore[type-var] + chan_type=int, # type: ignore[type-var] # ty: ignore[invalid-argument-type] ) @@ -166,7 +166,7 @@ def test_invalid_multichan_type_raises(empty_instrument: Instrument) -> None: parent=empty_instrument, name="empty", chan_type=DummyChannel, - multichan_paramclass=int, # type: ignore[arg-type] + multichan_paramclass=int, # type: ignore[arg-type] # ty: ignore[invalid-argument-type] ) @@ -516,7 +516,7 @@ def test_access_channels_by_name_empty_raises(dci: DummyChannelInstrument) -> No def test_access_channel_by_name_empty_raises(dci: DummyChannelInstrument) -> None: with pytest.raises(TypeError, match="missing 1 required positional argument"): - dci.channels.get_channel_by_name() # pyright: ignore[reportCallIssue] + dci.channels.get_channel_by_name() # pyright: ignore[reportCallIssue] # ty: ignore[missing-argument] def test_delete_from_channel_list(dci_with_list: DCIWithList) -> None: @@ -728,7 +728,7 @@ def test_channel_tuple_call_method_called_as_expected( assert result is None for channel in dci.channels: # type inference ignores that this has been mocked - channel.turn_on.assert_called_with("bar") # type: ignore[union-attr] + channel.turn_on.assert_called_with("bar") # type: ignore[union-attr] # ty: ignore[unresolved-attribute] def test_channel_tuple_names(dci: DummyChannelInstrument) -> None: @@ -802,7 +802,7 @@ def test_multi_function_with_callable_method( result = multi_func("bar") assert result is None for channel in dci.channels: - channel.turn_on.assert_called_with("bar") # type: ignore[union-attr] + channel.turn_on.assert_called_with("bar") # type: ignore[union-attr] # ty: ignore[unresolved-attribute] def test_multi_function_invalid_name_raises(dci: DummyChannelInstrument) -> None: diff --git a/tests/test_command.py b/tests/test_command.py index 6ed238b355a3..83a2d9cbd66c 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -11,25 +11,25 @@ class CustomError(Exception): def test_bad_calls() -> None: with pytest.raises(TypeError): - Command() # type: ignore[call-arg] + Command() # type: ignore[call-arg] # ty: ignore[missing-argument] with pytest.raises(TypeError): - Command(cmd="") # type: ignore[call-arg] + Command(cmd="") # type: ignore[call-arg] # ty: ignore[missing-argument] with pytest.raises(TypeError): - Command(0, "", output_parser=lambda: 1) # type: ignore[arg-type, misc] + Command(0, "", output_parser=lambda: 1) # type: ignore[arg-type, misc] # ty: ignore[invalid-argument-type] with pytest.raises(TypeError): Command(1, "", input_parser=lambda: 1) with pytest.raises(TypeError): - Command(0, cmd="", exec_str="not a function") # type: ignore[arg-type] + Command(0, cmd="", exec_str="not a function") # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises(TypeError): Command( 0, cmd=lambda: 1, - no_cmd_function="not a function", # type: ignore[arg-type] + no_cmd_function="not a function", # type: ignore[arg-type] # ty: ignore[invalid-argument-type] ) diff --git a/tests/test_import.py b/tests/test_import.py index 73d312b0720f..b40392163fb6 100644 --- a/tests/test_import.py +++ b/tests/test_import.py @@ -32,7 +32,7 @@ def test_top_level_parameter_shorthand_deprecated() -> None: with pytest.warns( QCoDeSDeprecationWarning, match=r"top level.*'qcodes\.parameters'" ): - obj = qcodes.Parameter # type: ignore[attr-defined] + obj = qcodes.Parameter # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert obj is qcodes.parameters.Parameter @@ -40,13 +40,13 @@ def test_top_level_combine_shorthand_deprecated() -> None: with pytest.warns( QCoDeSDeprecationWarning, match=r"top level.*'qcodes\.parameters'" ): - obj = qcodes.combine # type: ignore[attr-defined] + obj = qcodes.combine # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert obj is qcodes.parameters.combine def test_top_level_measurement_shorthand_deprecated() -> None: with pytest.warns(QCoDeSDeprecationWarning, match=r"top level.*'qcodes\.dataset'"): - obj = qcodes.Measurement # type: ignore[attr-defined] + obj = qcodes.Measurement # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert obj is qcodes.dataset.Measurement @@ -54,19 +54,19 @@ def test_top_level_instrument_shorthand_deprecated() -> None: with pytest.warns( QCoDeSDeprecationWarning, match=r"top level.*'qcodes\.instrument'" ): - obj = qcodes.Instrument # type: ignore[attr-defined] + obj = qcodes.Instrument # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert obj is qcodes.instrument.Instrument def test_top_level_monitor_shorthand_deprecated() -> None: with pytest.warns(QCoDeSDeprecationWarning, match=r"top level.*'qcodes\.monitor'"): - obj = qcodes.Monitor # type: ignore[attr-defined] + obj = qcodes.Monitor # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert obj is qcodes.monitor.Monitor def test_top_level_station_shorthand_deprecated() -> None: with pytest.warns(QCoDeSDeprecationWarning, match=r"top level.*'qcodes\.station'"): - obj = qcodes.Station # type: ignore[attr-defined] + obj = qcodes.Station # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert obj is qcodes.station.Station @@ -85,7 +85,7 @@ def test_top_level_submodule_access_not_deprecated() -> None: def test_top_level_unknown_attribute_raises() -> None: with pytest.raises(AttributeError, match="definitely_not_a_qcodes_attribute"): - _ = qcodes.definitely_not_a_qcodes_attribute # type: ignore[attr-defined] + _ = qcodes.definitely_not_a_qcodes_attribute # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def test_instrument_parameter_reexport_deprecated() -> None: @@ -94,7 +94,7 @@ def test_instrument_parameter_reexport_deprecated() -> None: with pytest.warns( QCoDeSDeprecationWarning, match=r"'ManualParameter'.*'qcodes\.parameters'" ): - obj = qcodes.instrument.ManualParameter # type: ignore[attr-defined] + obj = qcodes.instrument.ManualParameter # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert obj is qcodes.parameters.ManualParameter @@ -127,4 +127,4 @@ def test_instrument_parameter_reexport_deprecated_all(name: str) -> None: def test_instrument_unknown_attribute_raises() -> None: with pytest.raises(AttributeError, match="definitely_not_a_qcodes_attribute"): - _ = qcodes.instrument.definitely_not_a_qcodes_attribute # type: ignore[attr-defined] + _ = qcodes.instrument.definitely_not_a_qcodes_attribute # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] diff --git a/tests/test_instrument.py b/tests/test_instrument.py index b2ba6dec987b..b23c6a68fdec 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) @@ -434,7 +436,7 @@ def test_other_exception() -> None: # in order to raise an unexpected exception, and make sure it is # passed through the call stack, let's pass an empty dict instead # of a string with instrument name - _ = find_or_create_instrument(DummyInstrument, {}) # type: ignore[arg-type] + _ = find_or_create_instrument(DummyInstrument, {}) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] @pytest.mark.usefixtures("close_before_and_after") @@ -548,7 +550,7 @@ def test_close_all_no_log_by_default(caplog: pytest.LogCaptureFixture) -> None: def test_close_all_only_accepts_keyword_arguments() -> None: """The ``close_all`` options are keyword-only.""" with pytest.raises(TypeError): - Instrument.close_all(True) # type: ignore[misc] + Instrument.close_all(True) # type: ignore[misc] # ty: ignore[too-many-positional-arguments] def test_instrument_metadata(request: FixtureRequest) -> None: diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 87da1e55699e..b4b1194d733e 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -19,7 +19,7 @@ def snapshot_base(self, update=False, params_to_skip_update=None): class HasSnapshot(Metadatable): # Users shouldn't do this... but we'll test its behavior # for completeness - def snapshot(self, update: bool | None = False) -> Snapshot: # type: ignore[misc] + def snapshot(self, update: bool | None = False) -> Snapshot: # type: ignore[misc] # ty: ignore[invalid-method-override, override-of-final-method] return {"fruit": "kiwi"} @@ -58,7 +58,7 @@ def test_load() -> None: def test_init() -> None: with pytest.raises(TypeError): - Metadatable(metadata={"2": 3}, not_metadata={4: 5}) # type: ignore[call-arg] + Metadatable(metadata={"2": 3}, not_metadata={4: 5}) # type: ignore[call-arg] # ty: ignore[unknown-argument] m = Metadatable(metadata={"2": 3}) assert m.metadata == {"2": 3} diff --git a/tests/test_station.py b/tests/test_station.py index 4698ab5ab77a..74b5c5ba9054 100644 --- a/tests/test_station.py +++ b/tests/test_station.py @@ -114,13 +114,13 @@ def test_add_component_with_no_name() -> None: """ bob = {"name", "bob"} station = Station() - station.add_component(bob) # type: ignore[arg-type] + station.add_component(bob) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert ["component0"] == list(station.components.keys()) assert bob == station.components["component0"] # type: ignore[comparison-overlap] jay = {"name", "jay"} - station.add_component(jay) # type: ignore[arg-type] + station.add_component(jay) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert ["component0", "component1"] == list(station.components.keys()) assert jay == station.components["component1"] # type: ignore[comparison-overlap] @@ -1118,7 +1118,7 @@ def test_load_all_instruments_only_names(example_station) -> None: def test_load_all_instruments_without_config_raises() -> None: station = Station() with pytest.raises(ValueError, match="Station has no config"): - station.load_all_instruments() # type: ignore[call-overload] + station.load_all_instruments() # type: ignore[call-overload] # ty: ignore[no-matching-overload] def test_station_config_created_with_multiple_config_files() -> None: diff --git a/tests/utils/test_class_strings.py b/tests/utils/test_class_strings.py index 4badd3ca4d28..d059d3aa5e21 100644 --- a/tests/utils/test_class_strings.py +++ b/tests/utils/test_class_strings.py @@ -12,5 +12,5 @@ def test_full_class() -> None: def test_named_repr() -> None: j = json.JSONEncoder() id_ = id(j) - j.name = "Peppa" # type: ignore[attr-defined] + j.name = "Peppa" # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] assert named_repr(j) == f"" diff --git a/tests/utils/test_deprecate.py b/tests/utils/test_deprecate.py index 5fa3e58006ee..164b1f4ecd2f 100644 --- a/tests/utils/test_deprecate.py +++ b/tests/utils/test_deprecate.py @@ -17,9 +17,11 @@ @pytest.fixture def deprecated_getattr() -> _FixtureT: """Create a __getattr__ with two deprecated TypeVars.""" + # these TypeVars are values handed to the deprecation shim rather than + # type variables used to parametrize anything, which ty does not allow deprecated = { - "MyT": TypeVar("MyT"), - "MyK": TypeVar("MyK", bound=int), + "MyT": TypeVar("MyT"), # ty: ignore[invalid-legacy-type-variable] + "MyK": TypeVar("MyK", bound=int), # ty: ignore[invalid-legacy-type-variable] } getattr_fn = _make_deprecated_typevars_getattr("fake.module", deprecated) return deprecated, getattr_fn @@ -64,7 +66,7 @@ def test_repeated_access_returns_same_object( def test_fallback_is_called_for_unknown_names() -> None: - deprecated: dict[str, TypeVar] = {"X": TypeVar("X")} + deprecated: dict[str, TypeVar] = {"X": TypeVar("X")} # ty: ignore[invalid-legacy-type-variable] def fallback(name: str) -> str: return f"fallback:{name}" @@ -74,7 +76,7 @@ def fallback(name: str) -> str: def test_fallback_not_called_for_deprecated_names() -> None: - deprecated: dict[str, TypeVar] = {"X": TypeVar("X")} + deprecated: dict[str, TypeVar] = {"X": TypeVar("X")} # ty: ignore[invalid-legacy-type-variable] fallback_called = False def fallback(name: str) -> str: @@ -92,6 +94,8 @@ def test_real_module_import_triggers_warning() -> None: """Test that importing a deprecated TypeVar from an actual module works.""" mod = importlib.import_module("qcodes.utils.deep_update_utils") with pytest.warns(QCoDeSDeprecationWarning, match="'K'"): - k = mod.K + # K is only served by the module level __getattr__ that this test + # exercises, so it is invisible to a type checker + k = mod.K # ty: ignore[unresolved-attribute] assert isinstance(k, TypeVar) diff --git a/tests/utils/test_isfunction.py b/tests/utils/test_isfunction.py index 81bc0268ab81..45da5b8957e5 100644 --- a/tests/utils/test_isfunction.py +++ b/tests/utils/test_isfunction.py @@ -36,7 +36,7 @@ def f2(a: object, b: object) -> NoReturn: # make sure we only accept valid arg_count with pytest.raises(TypeError): - is_function(f0, "lots") # type: ignore[arg-type] + is_function(f0, "lots") # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises(TypeError): is_function(f0, -1) diff --git a/tests/validators/test_arrays.py b/tests/validators/test_arrays.py index 02a0549f6709..607b612fe95c 100644 --- a/tests/validators/test_arrays.py +++ b/tests/validators/test_arrays.py @@ -18,7 +18,7 @@ def test_type() -> None: m = Arrays(min_value=0.0, max_value=3.2, shape=(2, 2)) for v in ["somestring", 4, 2, [[2, 0], [1, 2]]]: with pytest.raises(TypeError): - m.validate(v) # type: ignore[arg-type] + m.validate(v) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_complex_min_max_raises() -> None: @@ -31,14 +31,14 @@ def test_complex_min_max_raises() -> None: r" It is \(1\+1j\) of type " r"", ): - Arrays(min_value=1 + 1j) # type: ignore[arg-type] + Arrays(min_value=1 + 1j) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises( TypeError, match=r"max_value must be a real number. " r"It is \(1\+1j\) of type " r"", ): - Arrays(max_value=1 + 1j) # type: ignore[arg-type] + Arrays(max_value=1 + 1j) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises( TypeError, match=r"Setting min_value or max_value is " @@ -225,9 +225,9 @@ def test_valid_values() -> None: def test_shape_non_sequence_raises() -> None: with pytest.raises(ValueError): - _ = Arrays(shape=5) # type: ignore[arg-type] + _ = Arrays(shape=5) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises(ValueError): - _ = Arrays(shape=lambda: 10) # type: ignore[arg-type] + _ = Arrays(shape=lambda: 10) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_repr() -> None: diff --git a/tests/validators/test_basic.py b/tests/validators/test_basic.py index ee606a4205ac..763e3d88ded5 100644 --- a/tests/validators/test_basic.py +++ b/tests/validators/test_basic.py @@ -55,7 +55,7 @@ def test_real_anything() -> None: def test_failed_anything() -> None: with pytest.raises(TypeError): - Anything(1) # type: ignore[call-arg] + Anything(1) # type: ignore[call-arg] # ty: ignore[too-many-positional-arguments] with pytest.raises(TypeError): - Anything(values=[1, 2, 3]) # type: ignore[call-arg] + Anything(values=[1, 2, 3]) # type: ignore[call-arg] # ty: ignore[unknown-argument] diff --git a/tests/validators/test_bool.py b/tests/validators/test_bool.py index f41da91ee6aa..3ec9739f615e 100644 --- a/tests/validators/test_bool.py +++ b/tests/validators/test_bool.py @@ -54,7 +54,7 @@ def test_bool() -> None: for vv in NOTBOOLS: with pytest.raises(TypeError): - b.validate(vv) # type: ignore[arg-type] + b.validate(vv) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert repr(b) == "" diff --git a/tests/validators/test_callable.py b/tests/validators/test_callable.py index 02a75a59cb6d..8a43ae2234eb 100644 --- a/tests/validators/test_callable.py +++ b/tests/validators/test_callable.py @@ -12,7 +12,7 @@ def test_func() -> bool: c.validate(test_func) test_int = 5 with pytest.raises(TypeError): - c.validate(test_int) # type: ignore[arg-type] + c.validate(test_int) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_valid_values() -> None: diff --git a/tests/validators/test_complex.py b/tests/validators/test_complex.py index 2f0f9e45544d..ff1e4995108c 100644 --- a/tests/validators/test_complex.py +++ b/tests/validators/test_complex.py @@ -28,4 +28,4 @@ def test_complex_raises(val: float | str) -> None: n = ComplexNumbers() with pytest.raises(TypeError, match=r"is not complex;"): - n.validate(val) # type: ignore[arg-type] + n.validate(val) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] diff --git a/tests/validators/test_dict.py b/tests/validators/test_dict.py index 76746374ba76..e3cca0b67331 100644 --- a/tests/validators/test_dict.py +++ b/tests/validators/test_dict.py @@ -13,7 +13,7 @@ def test_dict() -> None: d.validate(my_dict) my_int = 5 with pytest.raises(TypeError): - d.validate(my_int) # type: ignore[arg-type] + d.validate(my_int) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_valid_values() -> None: diff --git a/tests/validators/test_enum.py b/tests/validators/test_enum.py index 72628bbc0436..1c0c5220e52b 100644 --- a/tests/validators/test_enum.py +++ b/tests/validators/test_enum.py @@ -25,7 +25,7 @@ def test_good() -> None: for v in [22, "bad data", [44, 55]]: with pytest.raises((ValueError, TypeError)): - e.validate(v) # pyright: ignore[reportArgumentType] + e.validate(v) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] assert repr(e) == f"" @@ -37,7 +37,7 @@ def test_good() -> None: def test_bad() -> None: for enum in not_enums: with pytest.raises(TypeError): - vals.Enum(*enum) # type: ignore[arg-type] + vals.Enum(*enum) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_valid_values() -> None: diff --git a/tests/validators/test_ints.py b/tests/validators/test_ints.py index 92450f80baa4..22f2ec76dec0 100644 --- a/tests/validators/test_ints.py +++ b/tests/validators/test_ints.py @@ -91,7 +91,7 @@ def test_unlimited() -> None: for vv in not_ints: with pytest.raises(TypeError): - n.validate(vv) # type: ignore[arg-type] + n.validate(vv) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_min() -> None: @@ -109,7 +109,7 @@ def test_min_raises() -> None: n = Ints(min_value=ints[-1]) for v in not_ints: with pytest.raises(TypeError): - n.validate(v) # type: ignore[arg-type] + n.validate(v) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_max() -> None: @@ -127,7 +127,7 @@ def test_max_raises() -> None: n = Ints(max_value=ints[-1]) for v in not_ints: with pytest.raises(TypeError): - n.validate(v) # type: ignore[arg-type] + n.validate(v) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_range() -> None: @@ -142,7 +142,7 @@ def test_range() -> None: for vv in not_ints: with pytest.raises(TypeError): - n.validate(vv) # type: ignore[arg-type] + n.validate(vv) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert repr(n) == "" assert n.is_numeric @@ -150,17 +150,17 @@ def test_range() -> None: def test_failed_numbers() -> None: with pytest.raises(TypeError): - Ints(1, 2, 3) # type: ignore[call-arg] + Ints(1, 2, 3) # type: ignore[call-arg] # ty: ignore[too-many-positional-arguments] with pytest.raises(TypeError): Ints(1, 1) # min >= max for val in not_ints: with pytest.raises((TypeError, OverflowError)): - Ints(max_value=val) # type: ignore[arg-type] + Ints(max_value=val) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises((TypeError, OverflowError)): - Ints(min_value=val) # type: ignore[arg-type] + Ints(min_value=val) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_valid_values() -> None: diff --git a/tests/validators/test_lists.py b/tests/validators/test_lists.py index 92eb8311fa37..722e237facb5 100644 --- a/tests/validators/test_lists.py +++ b/tests/validators/test_lists.py @@ -15,7 +15,7 @@ def test_type() -> None: v2 = 234 with pytest.raises(TypeError): - list_validator.validate(v2) # type: ignore[arg-type] + list_validator.validate(v2) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_elt_vals() -> None: @@ -25,7 +25,7 @@ def test_elt_vals() -> None: v2 = [0, 1, 11] with pytest.raises(ValueError): - list_validator.validate(v2) # type: ignore[arg-type] + list_validator.validate(v2) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_valid_values() -> None: diff --git a/tests/validators/test_literal.py b/tests/validators/test_literal.py index c549e8003afd..e001d461ea2a 100644 --- a/tests/validators/test_literal.py +++ b/tests/validators/test_literal.py @@ -15,10 +15,10 @@ def test_literal_validator() -> None: a123_val.validate(1) with pytest.raises(ValueError, match="5 is not a member of "): - a123_val.validate(5, context="Outside range") # pyright: ignore[reportArgumentType] + a123_val.validate(5, context="Outside range") # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] with pytest.raises(ValueError, match="some_str is not a member of "): - a123_val.validate("some_str", context="Wrong type") # pyright: ignore[reportArgumentType] + a123_val.validate("some_str", context="Wrong type") # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] def test_literal_validator_repr() -> None: diff --git a/tests/validators/test_multi_type.py b/tests/validators/test_multi_type.py index 334f2a6922c4..a48a1f34673e 100644 --- a/tests/validators/test_multi_type.py +++ b/tests/validators/test_multi_type.py @@ -87,15 +87,15 @@ def test_bad() -> None: # combiner == 'OR' for args in [[], [1], [Strings(), True]]: with pytest.raises(TypeError): - MultiType(*args) # type: ignore[misc] + MultiType(*args) # type: ignore[misc] # ty: ignore[invalid-argument-type] # combiner == 'OR' for args in [[], [1], [Strings(), True]]: with pytest.raises(TypeError): - MultiType(*args, combiner="OR") # type: ignore[misc] + MultiType(*args, combiner="OR") # type: ignore[misc] # ty: ignore[invalid-argument-type] # combiner == 'AND' for args in [[], [1], [Strings(), True]]: with pytest.raises(TypeError): - MultiType(*args, combiner="AND") # type: ignore[misc] + MultiType(*args, combiner="AND") # type: ignore[misc] # ty: ignore[invalid-argument-type] def test_valid_values() -> None: diff --git a/tests/validators/test_multi_type_and.py b/tests/validators/test_multi_type_and.py index 8d323ea1453a..b943ed723344 100644 --- a/tests/validators/test_multi_type_and.py +++ b/tests/validators/test_multi_type_and.py @@ -44,4 +44,4 @@ def test_good() -> None: def test_bad() -> None: for args in ([], [1], [Strings(), True]): with pytest.raises(TypeError): - MultiTypeAnd(*args) # type: ignore[misc] + MultiTypeAnd(*args) # type: ignore[misc] # ty: ignore[invalid-argument-type] diff --git a/tests/validators/test_multi_type_or.py b/tests/validators/test_multi_type_or.py index 52ac0c1ced7d..c21c9b2b453f 100644 --- a/tests/validators/test_multi_type_or.py +++ b/tests/validators/test_multi_type_or.py @@ -38,7 +38,7 @@ def test_good() -> None: def test_bad() -> None: for args in ([], [1], [Strings(), True]): with pytest.raises(TypeError): - MultiTypeOr(*args) # type: ignore[misc] + MultiTypeOr(*args) # type: ignore[misc] # ty: ignore[invalid-argument-type] def test_valid_values() -> None: diff --git a/tests/validators/test_multiples.py b/tests/validators/test_multiples.py index cf3f894ca9ed..6a79f5b37056 100644 --- a/tests/validators/test_multiples.py +++ b/tests/validators/test_multiples.py @@ -108,11 +108,11 @@ def test_divisors() -> None: for vvv in not_multiples: with pytest.raises(TypeError): - n.validate(vvv) # type:ignore[arg-type] + n.validate(vvv) # type:ignore[arg-type] # ty: ignore[invalid-argument-type] for dd in not_divisors: with pytest.raises(TypeError): - n = Multiples(divisor=dd) # type:ignore[arg-type] + n = Multiples(divisor=dd) # type:ignore[arg-type] # ty: ignore[invalid-argument-type] n = Multiples(divisor=3, min_value=1, max_value=10) assert repr(n) == "" diff --git a/tests/validators/test_numbers.py b/tests/validators/test_numbers.py index 79a1acf1929f..0aca7efd874b 100644 --- a/tests/validators/test_numbers.py +++ b/tests/validators/test_numbers.py @@ -139,7 +139,7 @@ def test_range() -> None: def test_failed_numbers() -> None: with pytest.raises(TypeError): - Numbers(1, 2, 3) # type: ignore[call-arg] + Numbers(1, 2, 3) # type: ignore[call-arg] # ty: ignore[too-many-positional-arguments] with pytest.raises(TypeError): Numbers(1, 1) # min >= max diff --git a/tests/validators/test_sequence.py b/tests/validators/test_sequence.py index 269460565dd8..97d47092c567 100644 --- a/tests/validators/test_sequence.py +++ b/tests/validators/test_sequence.py @@ -12,7 +12,7 @@ def test_type() -> None: v2 = 234 with pytest.raises(TypeError): - sequence_validator.validate(v2) # type: ignore[arg-type] + sequence_validator.validate(v2) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_elt_vals() -> None: diff --git a/tests/validators/test_string.py b/tests/validators/test_string.py index f196afaf6f42..d7e0186110fd 100644 --- a/tests/validators/test_string.py +++ b/tests/validators/test_string.py @@ -49,7 +49,7 @@ def test_unlimited() -> None: for vv in not_strings: with pytest.raises(TypeError): - s.validate(vv) # type: ignore[arg-type] + s.validate(vv) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert repr(s) == "" @@ -66,7 +66,7 @@ def test_min() -> None: for vv in not_strings: with pytest.raises(TypeError): - s.validate(vv) # type: ignore[arg-type] + s.validate(vv) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] s = Strings(min_length=100) assert repr(s) == "=100>" @@ -85,7 +85,7 @@ def test_max() -> None: s = Strings(max_length=100) for vv in not_strings: with pytest.raises(TypeError): - s.validate(vv) # type: ignore[arg-type] + s.validate(vv) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert repr(s) == "" @@ -102,7 +102,7 @@ def test_range() -> None: for vv in not_strings: with pytest.raises(TypeError): - s.validate(vv) # type: ignore[arg-type] + s.validate(vv) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] assert repr(s) == "" @@ -112,7 +112,7 @@ def test_range() -> None: def test_failed_strings() -> None: with pytest.raises(TypeError): - Strings(1, 2, 3) # type: ignore[call-arg] + Strings(1, 2, 3) # type: ignore[call-arg] # ty: ignore[too-many-positional-arguments] with pytest.raises(TypeError): Strings(10, 9) @@ -121,14 +121,14 @@ def test_failed_strings() -> None: Strings(max_length=0) with pytest.raises(TypeError): - Strings(min_length=1e12) # type: ignore[arg-type] + Strings(min_length=1e12) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] for length in [-1, 3.5, "2", None]: with pytest.raises(TypeError): - Strings(max_length=length) # type: ignore[arg-type] + Strings(max_length=length) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] with pytest.raises(TypeError): - Strings(min_length=length) # type: ignore[arg-type] + Strings(min_length=length) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_valid_values() -> None: 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