diff --git a/docs/changes/newsfragments/+channeltuple-covariant.improved b/docs/changes/newsfragments/+channeltuple-covariant.improved new file mode 100644 index 000000000000..1faf7f9e6df3 --- /dev/null +++ b/docs/changes/newsfragments/+channeltuple-covariant.improved @@ -0,0 +1,4 @@ +``ChannelTuple`` is now covariant in its channel type, so type checkers accept a +``ChannelTuple[SubChannel]`` where a ``ChannelTuple[BaseChannel]`` is expected. +``ChannelTuple.index`` and ``ChannelTuple.count`` now accept any object, matching +``collections.abc.Sequence``. ``ChannelList`` remains invariant because it is mutable. diff --git a/pyproject.toml b/pyproject.toml index 69005f478890..94b42aa0f809 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -210,6 +210,14 @@ reportUnnecessaryTypeIgnoreComment = "none" # but not type ignore comments since mypy already checks if type ignores are necessary reportUnnecessaryContains = "none" +# mypy does not check the tests, so pyright is the only checker that can +# report unnecessary type ignore comments there. +[[tool.pyright.executionEnvironments]] +root = "tests" +# keep resolving ``import tests.…`` from the project root +extraPaths = ["."] +reportUnnecessaryTypeIgnoreComment = "error" + [tool.pytest.ini_options] minversion = "7.2" testpaths = "tests" diff --git a/src/qcodes/instrument/channel.py b/src/qcodes/instrument/channel.py index 13e47178125c..289736bb56e3 100644 --- a/src/qcodes/instrument/channel.py +++ b/src/qcodes/instrument/channel.py @@ -3,7 +3,14 @@ from __future__ import annotations import sys -from collections.abc import Callable, Iterable, Iterator, MutableSequence, Sequence +from collections.abc import ( + Callable, + Iterable, + Iterator, + Mapping, + MutableSequence, + Sequence, +) from typing import TYPE_CHECKING, Any, Generic, Self, cast, overload from typing_extensions import TypeVar @@ -193,13 +200,16 @@ def __init__( self._snapshotable = snapshotable self._paramclass = multichan_paramclass - self._channel_mapping: dict[str, InstrumentModuleType] = {} + # The storage is typed as read-only (``Sequence`` and ``Mapping``) so + # that type checkers infer ``ChannelTuple`` as covariant in its + # channel type. ``ChannelList`` keeps its own mutable references to + # the same objects. + self._channels: Sequence[InstrumentModuleType] # provide lookup of channels by name - # If a list of channels is not provided, define a list to store - # channels. This will eventually become a locked tuple. - self._channels: list[InstrumentModuleType] + self._channel_mapping: Mapping[str, InstrumentModuleType] if chan_list is None: self._channels = [] + self._channel_mapping = {} else: self._channels = list(chan_list) self._channel_mapping = { @@ -331,7 +341,7 @@ def name_parts(self) -> list[str]: def index( self, - obj: InstrumentModuleType, + obj: object, start: int = 0, stop: int = sys.maxsize, ) -> int: @@ -346,7 +356,7 @@ def index( """ return self._channels.index(obj, start, stop) - def count(self, obj: InstrumentModuleType) -> int: + def count(self, obj: object) -> int: """Returns number of instances of the given object in the list Args: @@ -703,6 +713,15 @@ def __init__( super().__init__( parent, name, chan_type, chan_list, snapshotable, multichan_paramclass ) + # Mutable views of the storage that ``ChannelTuple`` exposes read-only. + # ``_channels`` and ``_channel_mapping`` refer to these same objects, + # so they must only be modified in place, never reassigned. + self._mutable_channels: list[InstrumentModuleType] = list(self._channels) + self._mutable_channel_mapping: dict[str, InstrumentModuleType] = dict( + self._channel_mapping + ) + self._channels = self._mutable_channels + self._channel_mapping = self._mutable_channel_mapping if len(self._channels) > 0: self._locked = True else: @@ -717,10 +736,8 @@ def __delitem__(self, key: slice) -> None: ... def __delitem__(self, key: int | slice) -> None: if self._locked: raise AttributeError("Cannot delete from a locked channel list") - self._channels.__delitem__(key) - self._channel_mapping = { - channel.short_name: channel for channel in self._channels - } + self._mutable_channels.__delitem__(key) + self._rebuild_channel_mapping() @overload def __setitem__(self, index: int, value: InstrumentModuleType) -> None: ... @@ -741,15 +758,20 @@ 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] + self._mutable_channels[index] = value # type: ignore[assignment] # mypy does not know that InstrumentModuleType is a TypeVar bound to # InstrumentModule so complains here else: assert not isinstance(value, InstrumentModule) - self._channels[index] = value - self._channel_mapping = { - channel.short_name: channel for channel in self._channels - } + self._mutable_channels[index] = value + self._rebuild_channel_mapping() + + def _rebuild_channel_mapping(self) -> None: + # Update in place so that ``_channel_mapping`` stays the same object. + self._mutable_channel_mapping.clear() + self._mutable_channel_mapping.update( + {channel.short_name: channel for channel in self._mutable_channels} + ) def append(self, obj: InstrumentModuleType) -> None: """ @@ -768,8 +790,8 @@ def append(self, obj: InstrumentModuleType) -> None: f"type. Adding {type(obj).__name__} to a " f"list of {self._chan_type.__name__}." ) - self._channel_mapping[obj.short_name] = obj - self._channels.append(obj) + self._mutable_channel_mapping[obj.short_name] = obj + self._mutable_channels.append(obj) def clear(self) -> None: """ @@ -777,9 +799,8 @@ def clear(self) -> None: """ if self._locked: raise AttributeError("Cannot clear a locked ChannelList") - # when not locked the _channels seq is a list - self._channels.clear() - self._channel_mapping.clear() + self._mutable_channels.clear() + self._mutable_channel_mapping.clear() def remove(self, obj: InstrumentModuleType) -> None: """ @@ -792,8 +813,8 @@ def remove(self, obj: InstrumentModuleType) -> None: if self._locked: raise AttributeError("Cannot remove from a locked channel list") else: - self._channels.remove(obj) - self._channel_mapping.pop(obj.short_name) + self._mutable_channels.remove(obj) + self._mutable_channel_mapping.pop(obj.short_name) def extend(self, objects: Iterable[InstrumentModuleType]) -> None: """ @@ -811,8 +832,10 @@ def extend(self, objects: Iterable[InstrumentModuleType]) -> None: objects_tuple = tuple(objects) if not all(isinstance(obj, self._chan_type) for obj in objects_tuple): raise TypeError("All items in a channel list must be of the same type.") - self._channels.extend(objects_tuple) - self._channel_mapping.update({obj.short_name: obj for obj in objects_tuple}) + self._mutable_channels.extend(objects_tuple) + self._mutable_channel_mapping.update( + {obj.short_name: obj for obj in objects_tuple} + ) def insert(self, index: int, obj: InstrumentModuleType) -> None: """ @@ -830,8 +853,8 @@ def insert(self, index: int, obj: InstrumentModuleType) -> None: f"All items in a channel list must be of the same " f"type. Adding {type(obj).__name__} to a list of {self._chan_type.__name__}." ) - self._channels.insert(index, obj) - self._channel_mapping[obj.short_name] = obj + self._mutable_channels.insert(index, obj) + self._mutable_channel_mapping[obj.short_name] = obj def get_validator(self) -> ChannelTupleValidator: """ diff --git a/tests/dataset/measurement/test_measurement_context_manager.py b/tests/dataset/measurement/test_measurement_context_manager.py index bac839bb6287..7ccad4eaad4c 100644 --- a/tests/dataset/measurement/test_measurement_context_manager.py +++ b/tests/dataset/measurement/test_measurement_context_manager.py @@ -2132,10 +2132,10 @@ def test_datasaver_2d_multi_parameters_array( # 30 points in each setpoint value list this_sp_val: npt.NDArray = np.array( - reduce(list.__add__, [[n] * 3 for n in range(5, 10)], []) # type: ignore[arg-type] + reduce(list.__add__, [[n] * 3 for n in range(5, 10)], []) ) that_sp_val: npt.NDArray = np.array( - reduce(list.__add__, [[n] for n in range(9, 12)], []) * 5 # type: ignore[arg-type] + reduce(list.__add__, [[n] for n in range(9, 12)], []) * 5 ) assert isinstance(ds, DataSet) diff --git a/tests/dataset/test_converters.py b/tests/dataset/test_converters.py index 579fda908056..b1b5650e5d19 100644 --- a/tests/dataset/test_converters.py +++ b/tests/dataset/test_converters.py @@ -182,7 +182,7 @@ def test_construct_current_rundescriber_from_fake_v4(some_interdeps) -> None: 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") v3["version"] = 3 assert rds1._to_dict() == v3 assert rds_upgraded._to_dict() == v3 diff --git a/tests/dataset/test_dataset_basic.py b/tests/dataset/test_dataset_basic.py index 1b35782d8ca0..d801852d5c10 100644 --- a/tests/dataset/test_dataset_basic.py +++ b/tests/dataset/test_dataset_basic.py @@ -324,7 +324,7 @@ def test_load_by_id_for_none() -> None: experiment_name=hst.text(min_size=1), sample_name=hst.text(min_size=1), dataset_name=hst.text( - hst.characters(categories=_unicode_categories), # type: ignore[arg-type] + hst.characters(categories=_unicode_categories), min_size=1, ), ) diff --git a/tests/dataset/test_dependencies.py b/tests/dataset/test_dependencies.py index d43699c8c9d3..78c4d1099232 100644 --- a/tests/dataset/test_dependencies.py +++ b/tests/dataset/test_dependencies.py @@ -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"], + inferences=inv["inffs"], ) diff --git a/tests/dataset/test_parameter_with_setpoints_has_control.py b/tests/dataset/test_parameter_with_setpoints_has_control.py index d2aae82e39f5..31073b8885da 100644 --- a/tests/dataset/test_parameter_with_setpoints_has_control.py +++ b/tests/dataset/test_parameter_with_setpoints_has_control.py @@ -24,7 +24,7 @@ def _make_controlled_setpoints( """Create a ParameterWithSetpoints that infers ``controlled`` via unpack_self.""" class _ControlledSetpoints(ParameterWithSetpoints): - def unpack_self(self, value): # type: ignore[override] + def unpack_self(self, value): res = super().unpack_self(value) res.append((controlled, controlled())) return res diff --git a/tests/drivers/test_lakeshore_325.py b/tests/drivers/test_lakeshore_325.py index 1eec724ff916..da5a3fcee158 100644 --- a/tests/drivers/test_lakeshore_325.py +++ b/tests/drivers/test_lakeshore_325.py @@ -13,9 +13,7 @@ min_size=1, max_size=5, unique=True, - ).map( - sorted # type: ignore[arg-type] - ) + ).map(sorted) ) def test_decode_sensor_status(list_of_codes) -> None: """ diff --git a/tests/helpers/test_delegate_attribues.py b/tests/helpers/test_delegate_attribues.py index 69d54084cf10..6c0e63fb41db 100644 --- a/tests/helpers/test_delegate_attribues.py +++ b/tests/helpers/test_delegate_attribues.py @@ -195,7 +195,7 @@ def prop(self) -> int: # ``missing`` does not exist; accessing it raises AttributeError. # Before this fix, Python fell through to ``__getattr__`` which # reported "no attribute 'prop'" and hid the real cause. - return self.missing # type: ignore[attr-defined] + return self.missing obj = WithFaultyProperty() with pytest.raises(AttributeError, match="missing"): @@ -238,7 +238,7 @@ class Plain(DelegateAttributes): # ``__name__`` is not defined on ``Plain`` instances, so accessing it # should raise ``AttributeError``, never ``TypeError``. with pytest.raises(AttributeError): - _ = obj.__name__ # type: ignore[attr-defined] + _ = obj.__name__ # ``inspect.iscoroutinefunction`` internally does # ``getattr(obj, '__name__', None)`` — this must not raise. diff --git a/tests/parameter/test_delegate_parameter.py b/tests/parameter/test_delegate_parameter.py index 55278f3f9aae..edbe859c6980 100644 --- a/tests/parameter/test_delegate_parameter.py +++ b/tests/parameter/test_delegate_parameter.py @@ -79,12 +79,12 @@ def get_cmd() -> Any: p = Parameter( *args, - **kwargs, # type: ignore[misc] + **kwargs, set_cmd=set_cmd, get_cmd=get_cmd, ) param = cast("ObservableParam", p) - param.get_instr_val = get_cmd # type: ignore[method-assign] + param.get_instr_val = get_cmd return param yield make_parameter @@ -380,7 +380,7 @@ def test_initial_cache_value_not_applied_to_temporary_base_cache() -> None: cache_set_call_count = 0 original_cache_set = _Cache.set - def counting_set(self: _Cache, value: object) -> None: # type: ignore[type-arg] + def counting_set(self: _Cache, value: object) -> None: nonlocal cache_set_call_count cache_set_call_count += 1 original_cache_set(self, value) diff --git a/tests/parameter/test_get_set_wrapping.py b/tests/parameter/test_get_set_wrapping.py index 3bbe3416d5bd..5443fe36dfe2 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): 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_parameter_basics.py b/tests/parameter/test_parameter_basics.py index 18a5446e1370..74d4ef06f6a8 100644 --- a/tests/parameter/test_parameter_basics.py +++ b/tests/parameter/test_parameter_basics.py @@ -252,7 +252,7 @@ def test_underlying_instrument_for_virtual_parameter() -> None: p._instrument = named_instrument # type: ignore[assignment] vp = VirtualParameter("test_param", param=p) - assert vp.underlying_instrument is named_instrument # type: ignore[comparison-overlap] + assert vp.underlying_instrument is named_instrument def test_get_cmd_str_no_instrument_raises() -> None: diff --git a/tests/parameter/test_parameter_on_set_callback.py b/tests/parameter/test_parameter_on_set_callback.py index f60aeded2979..eb433684044b 100644 --- a/tests/parameter/test_parameter_on_set_callback.py +++ b/tests/parameter/test_parameter_on_set_callback.py @@ -22,14 +22,14 @@ THREAD_SLEEP = 0.01 -@pytest.fixture(autouse=True) # type: ignore[misc] +@pytest.fixture(autouse=True) def _reset_callback() -> "Generator[None, None, None]": """Reset the callback after each test""" yield ParameterBase.global_on_set_callback = None -@pytest.fixture() # type: ignore[misc] +@pytest.fixture() def basic_parameter( basic_callback: "Callable[[ParameterBase, Any], None]", ) -> Parameter: @@ -39,7 +39,7 @@ def basic_parameter( return param -@pytest.fixture(scope="function") # type: ignore[misc] +@pytest.fixture(scope="function") def basic_callback( captured_params: list[tuple[ParameterBase, Any]], ) -> "Callable[[ParameterBase, Any], None]": @@ -51,13 +51,13 @@ def callback(param: ParameterBase, value: Any) -> None: return callback -@pytest.fixture(scope="function") # type: ignore[misc] +@pytest.fixture(scope="function") def captured_params() -> list[tuple[ParameterBase, Any]]: """Fixture for capturing callback parameters""" return [] -@pytest.fixture(autouse=True, scope="function") # type: ignore[misc] +@pytest.fixture(autouse=True, scope="function") def cleanup_db_connections(): """Clean up any open SQLite connections after each test""" yield @@ -120,7 +120,7 @@ def test_callback(p: ParameterBase, value: Any) -> None: class TestValidationBehavior: """Tests for validation-related functionality""" - @pytest.mark.parametrize( # type: ignore[misc] + @pytest.mark.parametrize( "test_input,validator,should_callback", [ pytest.param(5, validators.Numbers(0, 10), True, id="valid_number"), diff --git a/tests/test_channels.py b/tests/test_channels.py index 910c9bec5492..632726eca99c 100644 --- a/tests/test_channels.py +++ b/tests/test_channels.py @@ -97,7 +97,7 @@ def test_channels_call_function( caplog.clear() # the return type of __getattr__ is very broad since it can return both # a MultiInstrumentParameter an attribute a callable etc - dci.channels.log_my_name() # type: ignore[call-arg] + dci.channels.log_my_name() mssgs = [rec.message for rec in caplog.records] names = [ch.name.replace("dci_", "") for ch in dci.channels] assert mssgs == names @@ -175,7 +175,7 @@ def test_channel_tuple_validator_requires_channel_tuple( dci: DummyChannelInstrument, ) -> None: with pytest.raises(ValueError, match="channel_list must be a ChannelTuple"): - ChannelTupleValidator(dci.A) # type: ignore[arg-type] + ChannelTupleValidator(dci.A) def test_wrong_chan_type_raises(empty_instrument: Instrument) -> None: @@ -304,6 +304,34 @@ def test_channel_type_can_be_inferred( assert_type(dci.channels[0], DummyChannel) +class _SubDummyChannel(DummyChannel): + pass + + +def test_channel_tuple_is_covariant(dci: DummyChannelInstrument) -> None: + # Guarded by pyright in CI: the assignment below only type checks while + # ChannelTuple is covariant in its channel type. + sub_channels: ChannelTuple[_SubDummyChannel] = ChannelTuple( + dci, "subs", _SubDummyChannel, [_SubDummyChannel(dci, "ChanA", "A")] + ) + channels: ChannelTuple[DummyChannel] = sub_channels + assert channels.get_channel_by_name("ChanA") is sub_channels[0] + + # index and count accept any object, as for collections.abc.Sequence. + assert channels.count(object()) == 0 + with pytest.raises(ValueError): + channels.index(object()) + + +def test_channel_list_is_invariant(dci: DummyChannelInstrument) -> None: + # ChannelList is mutable, so it must stay invariant. Pyright reports an + # unnecessary ignore in the tests, so this fails if it becomes covariant. + sub_channels = ChannelList(dci, "subs", _SubDummyChannel) + channels: ChannelList[DummyChannel] = sub_channels # pyright: ignore[reportAssignmentType] + with pytest.raises(TypeError, match="All items in a channel list"): + channels.append(DummyChannel(dci, "ChanA", "A")) + + def test_add_none_channel_tuple_to_channel_tuple_raises( dci: DummyChannelInstrument, ) -> None: @@ -420,12 +448,12 @@ def test_remove_channel(dci_with_list: DCIWithList) -> None: chan_a = dci_with_list.A # the return type of __getattr__ is very broad since it can return both # a MultiInstrumentParameter an attribute a callable etc - original_length = len(channels.temperature()) # type: ignore[call-arg] + original_length = len(channels.temperature()) channels.remove(chan_a) with pytest.raises(AttributeError): getattr(channels, chan_a.short_name) assert len(channels) == original_length - 1 - assert len(channels.temperature()) == original_length - 1 # type: ignore[call-arg] + assert len(channels.temperature()) == original_length - 1 # the return type of __getattr__ is very broad since it can return both # a MultiInstrumentParameter an attribute a callable etc @@ -480,7 +508,7 @@ def test_combine_channels(dci: DummyChannelInstrument, setpoints: list[float]) - chan.temperature(setpoints[i]) expected = (*setpoints[0:2], 0, 0, *setpoints[2:]) - assert dci.channels.temperature() == expected # type: ignore[call-arg] + assert dci.channels.temperature() == expected # the return type of __getattr__ is very broad since it can return both # a MultiInstrumentParameter, an attribute a callable etc. @@ -587,6 +615,42 @@ def test_set_element_locked_raises(dci_with_list: DCIWithList) -> None: assert dci_with_list.channels[0] is not dci_with_list.channels[1] +def test_channel_list_lookup_by_name_tracks_mutations( + dci_with_list: DCIWithList, +) -> None: + # ChannelList keeps mutable storage alongside the read-only storage of + # ChannelTuple. Lookups implemented on ChannelTuple must see every mutation. + channels = dci_with_list.channels + foo = DummyChannel(dci_with_list, name="foo", channel="foo") + bar = DummyChannel(dci_with_list, name="bar", channel="bar") + baz = DummyChannel(dci_with_list, name="baz", channel="baz") + + channels.append(foo) + channels.extend([bar]) + assert tuple(channels.get_channels_by_name("foo", "bar")) == (foo, bar) + assert channels.index(bar) == len(channels) - 1 + + channels[-1] = baz + assert channels.get_channel_by_name("baz") is baz + with pytest.raises(KeyError): + channels.get_channel_by_name("bar") + + del channels[-1] + channels.remove(foo) + assert channels.count(foo) == 0 + for name in ("foo", "baz"): + with pytest.raises(KeyError): + channels.get_channel_by_name(name) + + channels.clear() + assert len(channels) == 0 + assert len(channels._channel_mapping) == 0 + + channels.insert(0, foo) + assert channels.get_channel_by_name("foo") is foo + assert channels.to_channel_tuple().get_channel_by_name("foo") is foo + + @settings(suppress_health_check=(HealthCheck.function_scoped_fixture,), deadline=1000) @given(myindexs=hst.lists(elements=hst.integers(0, 7), min_size=1)) def test_access_channels_by_name( @@ -717,7 +781,7 @@ def test_get_attr_on_empty_channellist_works_as_expected( def test_channel_tuple_call_method_basic_test(dci: DummyChannelInstrument) -> None: - result = dci.channels.turn_on() # type: ignore[call-arg] + result = dci.channels.turn_on() # the return type of __getattr__ is very broad since it can return both # a MultiInstrumentParameter an attribute a callable etc assert result is None @@ -729,7 +793,7 @@ def test_channel_tuple_call_method_called_as_expected( for channel in dci.channels: channel.turn_on = mocker.MagicMock(return_value=1) - result = dci.channels.turn_on("bar") # type: ignore[call-arg] + result = dci.channels.turn_on("bar") # the return type of __getattr__ is very broad since it can return both # a MultiInstrumentParameter an attribute a callable etc # We never return the result (same for Function) diff --git a/tests/test_logger.py b/tests/test_logger.py index c70e0bf1e728..2295f5ffcb90 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -511,7 +511,7 @@ def test_driver_class_level_applies_to_submodules() -> None: inst = ScopedDummyChannelInstrument("class_level_channels") channel = inst.submodules["A"] - assert channel.log.logger.getEffectiveLevel() == logging.DEBUG # type: ignore[union-attr] + assert channel.log.logger.getEffectiveLevel() == logging.DEBUG def test_scope_is_fixed_when_root_is_created( @@ -586,17 +586,11 @@ def test_submodule_logger_is_child_of_instrument_logger() -> None: assert inst.log.logger.name == f"{class_logger_name}.scoped_channels" # DummyChannelInstrument adds the channel ChanA under the key "A" - assert channel_a.name_parts == ["scoped_channels", "ChanA"] # type: ignore[union-attr] - assert ( - channel_a.log.logger.name # type: ignore[union-attr] - == f"{class_logger_name}.scoped_channels.A" - ) - assert ( - channel_b.log.logger.name # type: ignore[union-attr] - == f"{class_logger_name}.scoped_channels.B" - ) - assert channel_a.log.logger.parent is inst.log.logger # type: ignore[union-attr] - assert channel_b.log.logger.parent is inst.log.logger # type: ignore[union-attr] + assert channel_a.name_parts == ["scoped_channels", "ChanA"] + assert channel_a.log.logger.name == f"{class_logger_name}.scoped_channels.A" + assert channel_b.log.logger.name == f"{class_logger_name}.scoped_channels.B" + assert channel_a.log.logger.parent is inst.log.logger + assert channel_b.log.logger.parent is inst.log.logger def test_submodule_logger_is_named_after_the_key_it_was_added_with() -> None: @@ -620,8 +614,8 @@ def test_submodule_without_key_logger_is_named_after_the_module() -> None: class_logger_name = scoped_class_logger_name(ScopedDummyChannelOnlyInstrument) assert "ChanA_a" not in inst.submodules - assert channel.log.logger.name == f"{class_logger_name}.channel_list_only.ChanA_a" # type: ignore[union-attr] - assert channel.log.logger.parent is inst.log.logger # type: ignore[union-attr] + assert channel.log.logger.name == f"{class_logger_name}.channel_list_only.ChanA_a" + assert channel.log.logger.parent is inst.log.logger def test_logger_of_nested_modules_follows_the_key_of_their_parent() -> None: @@ -660,14 +654,14 @@ def test_nested_submodule_logger_is_child_of_its_parent_module() -> None: inst = ScopedDummyChannelInstrument("nested_modules") channel = inst.submodules["A"] nested = InstrumentModule(channel, "nested") # type: ignore[arg-type] - channel.add_submodule("nested", nested) # type: ignore[union-attr] + channel.add_submodule("nested", nested) assert nested.name_parts == ["nested_modules", "ChanA", "nested"] assert nested.log.logger.name == ( f"{scoped_class_logger_name(ScopedDummyChannelInstrument)}" ".nested_modules.A.nested" ) - assert nested.log.logger.parent is channel.log.logger # type: ignore[union-attr] + assert nested.log.logger.parent is channel.log.logger def test_instrument_level_applies_to_its_submodules() -> None: @@ -679,8 +673,8 @@ def test_instrument_level_applies_to_its_submodules() -> None: inst.log.logger.setLevel(logging.DEBUG) - assert channel.log.logger.getEffectiveLevel() == logging.DEBUG # type: ignore[union-attr] - assert other.submodules["A"].log.logger.getEffectiveLevel() == logging.WARNING # type: ignore[union-attr] + assert channel.log.logger.getEffectiveLevel() == logging.DEBUG + assert other.submodules["A"].log.logger.getEffectiveLevel() == logging.WARNING def test_submodule_level_can_be_set_for_one_module() -> None: @@ -689,18 +683,18 @@ def test_submodule_level_can_be_set_for_one_module() -> None: inst_a = ScopedDummyChannelInstrument("submodule_level_a") inst_b = ScopedDummyChannelInstrument("submodule_level_b") - inst_a.submodules["A"].log.logger.setLevel(logging.DEBUG) # type: ignore[union-attr] + inst_a.submodules["A"].log.logger.setLevel(logging.DEBUG) - assert inst_a.submodules["A"].log.logger.getEffectiveLevel() == logging.DEBUG # type: ignore[union-attr] - assert inst_a.submodules["B"].log.logger.getEffectiveLevel() == logging.WARNING # type: ignore[union-attr] - assert inst_b.submodules["A"].log.logger.getEffectiveLevel() == logging.WARNING # type: ignore[union-attr] + assert inst_a.submodules["A"].log.logger.getEffectiveLevel() == logging.DEBUG + assert inst_a.submodules["B"].log.logger.getEffectiveLevel() == logging.WARNING + assert inst_b.submodules["A"].log.logger.getEffectiveLevel() == logging.WARNING def test_submodule_of_unscoped_instrument_uses_shared_logger() -> None: inst = DummyChannelInstrument("unscoped_channels") channel = inst.submodules["A"] - assert channel.log.logger.name == SHARED_INSTRUMENT_LOGGER_NAME # type: ignore[union-attr] + assert channel.log.logger.name == SHARED_INSTRUMENT_LOGGER_NAME def test_visa_log_is_shared_by_default() -> None: diff --git a/tests/test_station.py b/tests/test_station.py index 4607199ebd1b..97086f422614 100644 --- a/tests/test_station.py +++ b/tests/test_station.py @@ -117,13 +117,13 @@ def test_add_component_with_no_name() -> None: station.add_component(bob) # type: ignore[arg-type] assert ["component0"] == list(station.components.keys()) - assert bob == station.components["component0"] # type: ignore[comparison-overlap] + assert bob == station.components["component0"] jay = {"name", "jay"} station.add_component(jay) # type: ignore[arg-type] assert ["component0", "component1"] == list(station.components.keys()) - assert jay == station.components["component1"] # type: ignore[comparison-overlap] + assert jay == station.components["component1"] def test_remove_component() -> None: diff --git a/tests/test_sweep_values.py b/tests/test_sweep_values.py index a7b4a517852a..e682f77249ad 100644 --- a/tests/test_sweep_values.py +++ b/tests/test_sweep_values.py @@ -36,13 +36,13 @@ def test_errors(c0, c1, c2) -> None: # fails if the parameter has no setter with pytest.raises(TypeError): - c2[0:0.1:0.01] # type: ignore[misc] + c2[0:0.1:0.01] # validates every step value against the parameter's Validator with pytest.raises(ValueError): c0[5:15:1] with pytest.raises(ValueError): - c0[5.0:15.0:1.0] # type: ignore[misc] + c0[5.0:15.0:1.0] with pytest.raises(ValueError): c0[-12] with pytest.raises(ValueError): @@ -74,7 +74,7 @@ def test_valid(c0) -> None: assert 2 not in c0_sv # in-place and copying addition - c0_sv += c0[1.5:1.8:0.1] # type: ignore[misc] + c0_sv += c0[1.5:1.8:0.1] c0_sv2 = c0_sv + c0[2] assert list(c0_sv) == [1, 1.5, 1.6, 1.7] assert list(c0_sv2) == [1, 1.5, 1.6, 1.7, 2] @@ -120,7 +120,7 @@ def test_base() -> None: def test_snapshot(c0) -> None: assert c0[0].snapshot() == {"parameter": c0.snapshot(), "values": [{"item": 0}]} - assert c0[0:5:0.3].snapshot()["values"] == [ # type: ignore[misc] + assert c0[0:5:0.3].snapshot()["values"] == [ {"first": 0, "last": 4.8, "num": 17, "type": "linear"} ]