From 2a654270d507f6b62bc7deb511e6b2810f6a745d Mon Sep 17 00:00:00 2001 From: Wouter van Reeven Date: Tue, 3 Mar 2026 16:34:48 +0100 Subject: [PATCH 1/2] Switch from black, flake8 and isort to ruff. --- .ts_pre_commit_config.yaml | 8 +- bin/command_test_csc | 1 + bin/get_component_info | 1 + bin/run_test_csc | 1 + doc/news/OSW-1886.misc.rst | 1 + measure_read_speed.py | 32 +- measure_write_speed.py | 20 +- python/lsst/ts/salobj/async_s3_bucket.py | 20 +- .../lsst/ts/salobj/base_config_test_case.py | 34 +-- python/lsst/ts/salobj/base_csc.py | 78 ++--- python/lsst/ts/salobj/base_csc_test_case.py | 34 +-- python/lsst/ts/salobj/base_script.py | 16 +- python/lsst/ts/salobj/check_schema.py | 12 +- python/lsst/ts/salobj/configurable_csc.py | 36 +-- python/lsst/ts/salobj/controller.py | 24 +- python/lsst/ts/salobj/create_topics.py | 3 +- python/lsst/ts/salobj/csc_commander.py | 84 ++--- python/lsst/ts/salobj/csc_utils.py | 8 +- python/lsst/ts/salobj/delete_topics.py | 36 +-- .../lsst/ts/salobj/make_mock_write_topics.py | 7 +- python/lsst/ts/salobj/sal_enums.py | 9 +- python/lsst/ts/salobj/sal_info.py | 185 +++-------- python/lsst/ts/salobj/sal_log_handler.py | 10 +- python/lsst/ts/salobj/testcsc.py | 21 +- python/lsst/ts/salobj/testcsccommander.py | 4 +- python/lsst/ts/salobj/testscript.py | 9 +- python/lsst/ts/salobj/testutils.py | 10 +- python/lsst/ts/salobj/topics/base_topic.py | 4 +- .../ts/salobj/topics/controller_command.py | 16 +- python/lsst/ts/salobj/topics/read_topic.py | 43 +-- .../lsst/ts/salobj/topics/remote_command.py | 36 +-- python/lsst/ts/salobj/topics/write_topic.py | 17 +- python/lsst/ts/salobj/type_hints.py | 10 +- python/lsst/ts/salobj/validator.py | 8 +- tests/test_async_s3_bucket.py | 35 +-- tests/test_base_script.py | 74 ++--- tests/test_basics.py | 29 +- tests/test_config_files.py | 17 +- tests/test_controller.py | 33 +- tests/test_controller_logging.py | 53 +--- tests/test_csc_commander.py | 16 +- tests/test_csc_communication.py | 137 +++------ tests/test_csc_configuration.py | 45 +-- tests/test_csc_constructor.py | 13 +- tests/test_csc_make_from_cmd_line.py | 13 +- tests/test_csc_simulation_mode.py | 5 +- tests/test_csc_utils.py | 21 +- tests/test_hierarchical_update.py | 13 +- tests/test_queue_capacity_checker.py | 39 +-- tests/test_remote.py | 141 +++------ tests/test_sal_info.py | 58 ++-- tests/test_speed.py | 60 ++-- tests/test_topics.py | 287 ++++++------------ tests/test_validator.py | 5 +- 54 files changed, 579 insertions(+), 1353 deletions(-) create mode 100644 doc/news/OSW-1886.misc.rst diff --git a/.ts_pre_commit_config.yaml b/.ts_pre_commit_config.yaml index a2e0692a3..44592cf42 100644 --- a/.ts_pre_commit_config.yaml +++ b/.ts_pre_commit_config.yaml @@ -1,10 +1,10 @@ -black: true +black: false check-xml: true check-yaml: true clang-format: false -flake8: true +flake8: false format-xmllint: false -isort: true +isort: false mypy: true -ruff: false +ruff: true towncrier: true diff --git a/bin/command_test_csc b/bin/command_test_csc index 2d15a9d7c..315577395 100755 --- a/bin/command_test_csc +++ b/bin/command_test_csc @@ -26,6 +26,7 @@ For most CSCs running a commander can be quite dangerous so please pick a script name that is not easily confused with the script that runs the CSC. """ + from lsst.ts.salobj import command_test_csc command_test_csc() diff --git a/bin/get_component_info b/bin/get_component_info index 50e2d8469..569943410 100755 --- a/bin/get_component_info +++ b/bin/get_component_info @@ -24,6 +24,7 @@ Run with option ``--help`` for more information. """ + from lsst.ts.salobj import get_component_info get_component_info() diff --git a/bin/run_test_csc b/bin/run_test_csc index fd0d4a161..e580b26be 100755 --- a/bin/run_test_csc +++ b/bin/run_test_csc @@ -21,6 +21,7 @@ # along with this program. If not, see . """An example of how to run a CSC""" + from lsst.ts.salobj import run_test_csc run_test_csc() diff --git a/doc/news/OSW-1886.misc.rst b/doc/news/OSW-1886.misc.rst new file mode 100644 index 000000000..b0f80075f --- /dev/null +++ b/doc/news/OSW-1886.misc.rst @@ -0,0 +1 @@ +Switched from black, flake8 and isort to ruff. diff --git a/measure_read_speed.py b/measure_read_speed.py index 3df18a67a..b76ff22da 100755 --- a/measure_read_speed.py +++ b/measure_read_speed.py @@ -3,12 +3,11 @@ import asyncio import numpy as np + from lsst.ts import salobj, utils -async def measure_read_speed( - component_name: str, sal_index: int, attr_name: str, num_messages: int -) -> None: +async def measure_read_speed(component_name: str, sal_index: int, attr_name: str, num_messages: int) -> None: """Measure read speed and latency for a specified SAL component and topic. Parameters @@ -25,12 +24,11 @@ async def measure_read_speed( """ if num_messages < 2: raise ValueError(f"num_messages={num_messages} must be >= 2") - async with salobj.Domain() as domain, salobj.SalInfo( - domain=domain, name=component_name, index=sal_index - ) as salinfo: - topic = salobj.topics.ReadTopic( - salinfo=salinfo, attr_name=attr_name, max_history=0 - ) + async with ( + salobj.Domain() as domain, + salobj.SalInfo(domain=domain, name=component_name, index=sal_index) as salinfo, + ): + topic = salobj.topics.ReadTopic(salinfo=salinfo, attr_name=attr_name, max_history=0) await salinfo.start() print("Reader is ready") @@ -50,9 +48,7 @@ async def measure_read_speed( latencies[i] = utils.current_tai() - data.private_sndStamp dt = utils.current_tai() - t0 read_speed = (num_messages - 1) / dt - num_lost = ( - 1 + (data.private_seqNum - initial_data.private_seqNum) - num_messages - ) + num_lost = 1 + (data.private_seqNum - initial_data.private_seqNum) - num_messages print(f"Read {read_speed:0.0f} samples/second ({num_messages} samples)") print( f"Latency mean = {latencies.mean():0.3f}, stdev = {latencies.std():0.3f}, " @@ -62,15 +58,9 @@ async def measure_read_speed( parser = argparse.ArgumentParser("Measure read speed and latency for one SAL topic") -parser.add_argument( - "component_name_index", help="SAL component name[:sal_index], e.g. Test:1" -) -parser.add_argument( - "topic_attr_name", help="Topic attribute name, e.g. evt_summaryState" -) -parser.add_argument( - "-n", "--number", type=int, default=2000, help="Number of messages to read" -) +parser.add_argument("component_name_index", help="SAL component name[:sal_index], e.g. Test:1") +parser.add_argument("topic_attr_name", help="Topic attribute name, e.g. evt_summaryState") +parser.add_argument("-n", "--number", type=int, default=2000, help="Number of messages to read") args = parser.parse_args() component_name, sal_index = salobj.name_to_name_index(args.component_name_index) asyncio.run( diff --git a/measure_write_speed.py b/measure_write_speed.py index 6ce89c26f..47f0fb97f 100755 --- a/measure_write_speed.py +++ b/measure_write_speed.py @@ -58,9 +58,7 @@ async def measure_write_speed( bool: lambda: random.choice([False, True]), int: lambda: random.randint(0, 100), float: lambda: random.randrange(-1000, 1000), - str: lambda: "".join( - random.choice(string.ascii_letters) for _ in range(256) - ), + str: lambda: "".join(random.choice(string.ascii_letters) for _ in range(256)), } if isinstance(value, list): arr_len = len(value) @@ -87,15 +85,9 @@ async def measure_write_speed( await asyncio.sleep(1) -parser = argparse.ArgumentParser( - "Measure the speed of writing messages for one SAL topic" -) -parser.add_argument( - "component_name_index", help="SAL component name[:sal_index], e.g. Test:1" -) -parser.add_argument( - "topic_attr_name", help="Topic attribute name, e.g. evt_summaryState" -) +parser = argparse.ArgumentParser("Measure the speed of writing messages for one SAL topic") +parser.add_argument("component_name_index", help="SAL component name[:sal_index], e.g. Test:1") +parser.add_argument("topic_attr_name", help="Topic attribute name, e.g. evt_summaryState") parser.add_argument( "-i", "--interval", @@ -103,9 +95,7 @@ async def measure_write_speed( default=0, help="Interval between each message (seconds)", ) -parser.add_argument( - "-n", "--number", type=int, default=2000, help="Number of messages to write" -) +parser.add_argument("-n", "--number", type=int, default=2000, help="Number of messages to write") args = parser.parse_args() component_name, sal_index = salobj.name_to_name_index(args.component_name_index) asyncio.run( diff --git a/python/lsst/ts/salobj/async_s3_bucket.py b/python/lsst/ts/salobj/async_s3_bucket.py index 43b059ffa..09125c2b4 100644 --- a/python/lsst/ts/salobj/async_s3_bucket.py +++ b/python/lsst/ts/salobj/async_s3_bucket.py @@ -162,9 +162,7 @@ def make_bucket_name(s3instance: str, s3category: str = "LFA") -> str: raise ValueError(f"{argname}={arg} invalid") bucket_name = f"rubinobs-{s3category}-{s3instance}".lower() if len(bucket_name) > 63: - raise ValueError( - f"Bucket name {bucket_name!r} too long: len={len(bucket_name)} > 63 chars" - ) + raise ValueError(f"Bucket name {bucket_name!r} too long: len={len(bucket_name)} > 63 chars") return bucket_name @staticmethod @@ -295,13 +293,9 @@ async def upload( """ loop = asyncio.get_running_loop() await loop.run_in_executor(None, self._sync_upload, fileobj, key, callback) - return ( - f"{self.service_resource.meta.client.meta.endpoint_url}/{self.name}/{key}" - ) + return f"{self.service_resource.meta.client.meta.endpoint_url}/{self.name}/{key}" - async def download( - self, key: str, callback: Callable[[int], None] | None = None - ) -> io.BytesIO: + async def download(self, key: str, callback: Callable[[int], None] | None = None) -> io.BytesIO: """Download a file-like object from the bucket. Parameters @@ -369,9 +363,7 @@ def _sync_upload( ) -> None: self.bucket.upload_fileobj(Fileobj=fileobj, Key=key, Callback=callback) - def _sync_download( - self, key: str, callback: Callable[[int], None] | None - ) -> io.BytesIO: + def _sync_download(self, key: str, callback: Callable[[int], None] | None) -> io.BytesIO: fileobj = io.BytesIO() self.bucket.download_fileobj(Key=key, Fileobj=fileobj, Callback=callback) # Rewind the fileobj so read returns the data. @@ -379,6 +371,4 @@ def _sync_download( return fileobj def _sync_size(self, key: str) -> int: - return self.bucket.meta.client.head_object(Bucket=self.name, Key=key)[ - "ContentLength" - ] + return self.bucket.meta.client.head_object(Bucket=self.name, Key=key)["ContentLength"] diff --git a/python/lsst/ts/salobj/base_config_test_case.py b/python/lsst/ts/salobj/base_config_test_case.py index 13a592604..319715869 100644 --- a/python/lsst/ts/salobj/base_config_test_case.py +++ b/python/lsst/ts/salobj/base_config_test_case.py @@ -28,6 +28,7 @@ import unittest import yaml + from lsst.ts.xml import type_hints from .configurable_csc import ConfigurableCsc @@ -130,9 +131,9 @@ def get_config_dir( assert config_package_root.is_dir() version = schema["title"].split()[-1] - assert version.startswith( - "v" - ), f"version={version} from schema title {schema['title']} does not start with 'v'" + assert version.startswith("v"), ( + f"version={version} from schema title {schema['title']} does not start with 'v'" + ) config_dir = pathlib.Path(config_package_root) / sal_name / version assert config_dir.is_dir() @@ -167,18 +168,14 @@ def check_config_files( config_validator = StandardValidator(schema) schema_version = schema["title"].split()[-1] - assert schema_version.startswith( - "v" - ), f"version={schema_version} from schema title {schema['title']} does not start with 'v'" + assert schema_version.startswith("v"), ( + f"version={schema_version} from schema title {schema['title']} does not start with 'v'" + ) config_files = list(config_dir.glob("*.yaml")) if exclude_glob: files_to_exclude = set(config_dir.glob(exclude_glob)) - config_files = [ - filename - for filename in config_files - if filename not in files_to_exclude - ] + config_files = [filename for filename in config_files if filename not in files_to_exclude] found_init = False site_files = [] override_files = [""] @@ -195,15 +192,11 @@ def check_config_files( if not site_files: site_files = [""] except Exception as e: - raise AssertionError( - f"Failed on {config_dir=}, {schema_version=}: {e!r}" - ) from e + raise AssertionError(f"Failed on {config_dir=}, {schema_version=}: {e!r}") from e try: # Check each site and each site with each override file - for site_file, override_file in itertools.product( - site_files, override_files - ): + for site_file, override_file in itertools.product(site_files, override_files): ConfigurableCsc.read_config_files( config_validator=config_validator, config_dir=config_dir, @@ -211,8 +204,7 @@ def check_config_files( ) except Exception as e: raise AssertionError( - f"Failed on {config_dir=}, {schema_version=}, " - f"{site_file=}, {override_file=}: {e!r}" + f"Failed on {config_dir=}, {schema_version=}, {site_file=}, {override_file=}: {e!r}" ) from e def check_standard_config_files( @@ -260,9 +252,7 @@ def check_standard_config_files( if config_dir is None: if config_package_root is None: - raise RuntimeError( - "config_package_root must be specified if config_dir is None." - ) + raise RuntimeError("config_package_root must be specified if config_dir is None.") if sal_name is None: raise ValueError("sal_name must be specified if config_dir is None") config_dir = self.get_config_dir( diff --git a/python/lsst/ts/salobj/base_csc.py b/python/lsst/ts/salobj/base_csc.py index 31678d2b8..aa595e926 100644 --- a/python/lsst/ts/salobj/base_csc.py +++ b/python/lsst/ts/salobj/base_csc.py @@ -276,13 +276,16 @@ async def check_for_duplicate_heartbeat( # to the heartbeat topic for a short time. if num_messages < 1: raise ValueError(f"{num_messages=} must be positive") - async with Domain() as domain, Remote( - domain=domain, - name=self.salinfo.name, - index=self.salinfo.index, - readonly=True, - include=["heartbeat"], - ) as remote: + async with ( + Domain() as domain, + Remote( + domain=domain, + name=self.salinfo.name, + index=self.salinfo.index, + readonly=True, + include=["heartbeat"], + ) as remote, + ): try: data = await remote.evt_heartbeat.next( # type: ignore[attr-defined] flush=True, timeout=self.heartbeat_interval * 3 @@ -336,9 +339,7 @@ async def start_phase2(self) -> None: # Use the current state instead of default_initial_state # because of Hexapod and Rotator, which do not know their # current state until they connect to the low-level controller. - command_state_list = state_transition_dict[ - (self.summary_state, self._initial_state) - ] + command_state_list = state_transition_dict[(self.summary_state, self._initial_state)] state_transition_commands = [item[0] for item in command_state_list] try: for command in state_transition_commands: @@ -352,9 +353,7 @@ async def start_phase2(self) -> None: # will have a different timestamp than the previous one. await asyncio.sleep(0.001) except Exception: - self.log.exception( - f"Failed in start on state transition command {command}; continuing." - ) + self.log.exception(f"Failed in start on state transition command {command}; continuing.") async def close_tasks(self) -> None: """Shut down pending tasks. Called by `close`.""" @@ -412,7 +411,8 @@ def make_from_cmd_line( # when index is an int or bool. choices = [int(item.value) for item in index] # type: ignore names_str = ", ".join( - f"{item.value}: {item.name.lower()}" for item in index # type: ignore + f"{item.value}: {item.name.lower()}" + for item in index # type: ignore ) help_text = f"SAL index, one of: {names_str}" parser.add_argument("index", type=int, help=help_text, choices=choices) @@ -428,10 +428,7 @@ def make_from_cmd_line( dest="initial_state", help="initial state", ) - add_simulate_arg = ( - cls.valid_simulation_modes is not None - and len(cls.valid_simulation_modes) > 1 - ) + add_simulate_arg = cls.valid_simulation_modes is not None and len(cls.valid_simulation_modes) > 1 if add_simulate_arg: assert cls.valid_simulation_modes is not None # make mypy happy if 0 in cls.valid_simulation_modes: @@ -443,9 +440,7 @@ def make_from_cmd_line( # Make --simulate a flag that takes no value and stores # the other value, if specified. simulation_help = ( - "Run in simulation mode?" - if cls.simulation_help is None - else cls.simulation_help + "Run in simulation mode?" if cls.simulation_help is None else cls.simulation_help ) nonzero_value = (set(cls.valid_simulation_modes) - set([0])).pop() parser.add_argument( @@ -458,11 +453,7 @@ def make_from_cmd_line( else: # There are more than 2 simulation modes or none of them is 0. # Make --simulate an argument that requires a value. - simulation_help = ( - "Simulation mode" - if cls.simulation_help is None - else cls.simulation_help - ) + simulation_help = "Simulation mode" if cls.simulation_help is None else cls.simulation_help parser.add_argument( "--simulate", type=int, @@ -497,9 +488,7 @@ def make_from_cmd_line( return csc @classmethod - async def amain( - cls, index: int | enum.IntEnum | bool | None, **kwargs: typing.Any - ) -> None: + async def amain(cls, index: int | enum.IntEnum | bool | None, **kwargs: typing.Any) -> None: """Make a CSC from command-line arguments and run it. Parameters @@ -546,9 +535,7 @@ def add_arguments(cls, parser: argparse.ArgumentParser) -> None: pass @classmethod - def add_kwargs_from_args( - cls, args: argparse.Namespace, kwargs: dict[str, typing.Any] - ) -> None: + def add_kwargs_from_args(cls, args: argparse.Namespace, kwargs: dict[str, typing.Any]) -> None: """Add constructor keyword arguments based on parsed arguments. Parameters @@ -608,9 +595,7 @@ async def do_standby(self, data: type_hints.BaseMsgType) -> None: data : ``cmd_standby.DataType`` Command data """ - await self._do_change_state( - data, "standby", [State.DISABLED, State.FAULT], State.STANDBY - ) + await self._do_change_state(data, "standby", [State.DISABLED, State.FAULT], State.STANDBY) async def do_start(self, data: type_hints.BaseMsgType) -> None: """Transition from `State.STANDBY` to `State.DISABLED`. @@ -811,16 +796,13 @@ async def fault(self, code: int | None, report: str, traceback: str = "") -> Non force_output=True, ) except BaseException: - self.log.exception( - f"Failed to output errorCode: code={code!r}; report={report!r}" - ) + self.log.exception(f"Failed to output errorCode: code={code!r}; report={report!r}") self.log.critical(f"Fault! errorCode={code}, errorReport={report!r}") try: await self._report_summary_state() except BaseException: self.log.exception( - "_report_summary_state failed while going to FAULT; " - "some code may not have run." + "_report_summary_state failed while going to FAULT; some code may not have run." ) await self.evt_summaryState.set_write( # type: ignore summaryState=self._summary_state, @@ -921,28 +903,20 @@ async def _do_change_state( try: await getattr(self, f"begin_{cmd_name}")(data) except base.ExpectedError as e: - self.log.error( - f"begin_{cmd_name} failed; remaining in state {curr_state!r}: {e}" - ) + self.log.error(f"begin_{cmd_name} failed; remaining in state {curr_state!r}: {e}") raise except Exception: - self.log.exception( - f"begin_{cmd_name} failed; remaining in state {curr_state!r}" - ) + self.log.exception(f"begin_{cmd_name} failed; remaining in state {curr_state!r}") raise self._summary_state = new_state try: await getattr(self, f"end_{cmd_name}")(data) except base.ExpectedError as e: - self.log.error( - f"end_{cmd_name} failed; reverting to state {curr_state!r}: {e}" - ) + self.log.error(f"end_{cmd_name} failed; reverting to state {curr_state!r}: {e}") raise except Exception: self._summary_state = curr_state - self.log.exception( - f"end_{cmd_name} failed; reverting to state {curr_state!r}" - ) + self.log.exception(f"end_{cmd_name} failed; reverting to state {curr_state!r}") raise await self.handle_summary_state() await self._report_summary_state() diff --git a/python/lsst/ts/salobj/base_csc_test_case.py b/python/lsst/ts/salobj/base_csc_test_case.py index e44a10afb..43b9034ba 100644 --- a/python/lsst/ts/salobj/base_csc_test_case.py +++ b/python/lsst/ts/salobj/base_csc_test_case.py @@ -215,9 +215,7 @@ async def make_csc( index=self.csc.salinfo.index, ) if self._broker_configuration is None: - self._broker_configuration = ( - self.csc.salinfo.get_broker_client_configuration() - ) + self._broker_configuration = self.csc.salinfo.get_broker_client_configuration() if self._schema_registry_url is None: self._schema_registry_url = self.csc.salinfo.schema_registry_url @@ -335,9 +333,9 @@ async def assert_next_sample( read_value = type(expected_value)(read_value) except Exception: pass - assert ( - read_value == expected_value - ), f"Failed on field {field_name}: read {read_value!r} != expected {expected_value!r}" + assert read_value == expected_value, ( + f"Failed on field {field_name}: read {read_value!r} != expected {expected_value!r}" + ) return data async def check_bin_script( @@ -402,9 +400,7 @@ async def check_bin_script( args += ["--override", override] args += cmdline_args - async with Domain() as domain, Remote( - domain=domain, name=name, index=index - ) as self.remote: + async with Domain() as domain, Remote(domain=domain, name=name, index=index) as self.remote: print("check_bin_script running:", " ".join(args)) self.csc_start_time = utils.current_tai() process = await asyncio.create_subprocess_exec( @@ -477,9 +473,7 @@ async def check_standard_state_transitions( # Start in STANDBY state. assert self.csc.summary_state == sal_enums.State.STANDBY await self.assert_next_summary_state(sal_enums.State.STANDBY) - await self.check_bad_commands( - good_commands=("start", "exitControl", "setLogLevel") + skip_commands - ) + await self.check_bad_commands(good_commands=("start", "exitControl", "setLogLevel") + skip_commands) # Send start; new state is DISABLED. await self.remote.cmd_start.set_start( # type: ignore @@ -487,20 +481,14 @@ async def check_standard_state_transitions( ) assert self.csc.summary_state == sal_enums.State.DISABLED await self.assert_next_summary_state(sal_enums.State.DISABLED) - await self.check_bad_commands( - good_commands=("enable", "standby", "setLogLevel") + skip_commands - ) + await self.check_bad_commands(good_commands=("enable", "standby", "setLogLevel") + skip_commands) # Send enable; new state is ENABLED. await self.remote.cmd_enable.start(timeout=timeout) # type: ignore assert self.csc.summary_state == sal_enums.State.ENABLED await self.assert_next_summary_state(sal_enums.State.ENABLED) - all_enabled_commands = tuple( - sorted(set(("disable", "setLogLevel")) | set(enabled_commands)) - ) - await self.check_bad_commands( - good_commands=all_enabled_commands + skip_commands - ) + all_enabled_commands = tuple(sorted(set(("disable", "setLogLevel")) | set(enabled_commands))) + await self.check_bad_commands(good_commands=all_enabled_commands + skip_commands) # Send disable; new state is DISABLED. await self.remote.cmd_disable.start(timeout=timeout) # type: ignore @@ -547,7 +535,5 @@ async def check_bad_commands( with self.subTest(command=command): # type: ignore cmd_attr = getattr(self.remote, f"cmd_{command}") print(f"{command=}") - with testutils.assertRaisesAckError( - ack=sal_enums.SalRetCode.CMD_FAILED - ): + with testutils.assertRaisesAckError(ack=sal_enums.SalRetCode.CMD_FAILED): await cmd_attr.start(timeout=STD_TIMEOUT) diff --git a/python/lsst/ts/salobj/base_script.py b/python/lsst/ts/salobj/base_script.py index 6bf895c1f..11184f01c 100644 --- a/python/lsst/ts/salobj/base_script.py +++ b/python/lsst/ts/salobj/base_script.py @@ -35,6 +35,7 @@ from collections.abc import Sequence import yaml + from lsst.ts import utils from lsst.ts.xml import type_hints from lsst.ts.xml.enums.Script import ( @@ -385,9 +386,7 @@ async def checkpoint(self, name: str = "") -> None: if self._run_task is None: raise RuntimeError("checkpoint error: state is RUNNING but no run_task") if self._run_task.done(): - raise RuntimeError( - "checkpoint error: state is RUNNING but run_task is done" - ) + raise RuntimeError("checkpoint error: state is RUNNING but run_task is done") self.num_checkpoints += 1 self.last_checkpoint = name @@ -507,9 +506,7 @@ def assert_state(self, action: str, states: Sequence[ScriptState]) -> None: raise base.ExpectedError(f"Cannot {action}: script is exiting") if self.state.state not in states: states_str = ", ".join(s.name for s in states) - raise base.ExpectedError( - f"Cannot {action}: state={self.state_name} instead of {states_str}" - ) + raise base.ExpectedError(f"Cannot {action}: state={self.state_name} instead of {states_str}") async def do_configure(self, data: type_hints.BaseMsgType) -> None: """Configure the currently loaded script. @@ -545,8 +542,7 @@ async def do_configure(self, data: type_hints.BaseMsgType) -> None: if self.config_validator is None: if config_yaml: raise RuntimeError( - "This script has no configuration so " - f"config={config_yaml} must be empty." + f"This script has no configuration so config={config_yaml} must be empty." ) config = types.SimpleNamespace() else: @@ -802,7 +798,5 @@ async def _exit(self) -> None: except Exception as e: if not isinstance(e, base.ExpectedError): self.log.exception("Error in run") - await self.set_state( - ScriptState.FAILED, reason=f"failed in _exit: {e}", keep_old_reason=True - ) + await self.set_state(ScriptState.FAILED, reason=f"failed in _exit: {e}", keep_old_reason=True) asyncio.create_task(self.close(exception=e)) diff --git a/python/lsst/ts/salobj/check_schema.py b/python/lsst/ts/salobj/check_schema.py index ee80d488c..5e249dabb 100644 --- a/python/lsst/ts/salobj/check_schema.py +++ b/python/lsst/ts/salobj/check_schema.py @@ -30,6 +30,7 @@ SchemaRegistryClient, SchemaRegistryError, ) + from lsst.ts.xml import subsystems from lsst.ts.xml.component_info import ComponentInfo @@ -197,8 +198,7 @@ def check_schema() -> None: parser.add_argument( "components", nargs="*", - help="Names of SAL components, e.g. 'Script ScriptQueue'. " - "Ignored if --all is specified", + help="Names of SAL components, e.g. 'Script ScriptQueue'. Ignored if --all is specified", ) parser.add_argument( "--all", @@ -206,13 +206,9 @@ def check_schema() -> None: help="Create topics for all components.", ) - parser.add_argument( - "-b", "--backward", action="store_true", help="Use backward compatibility" - ) + parser.add_argument("-b", "--backward", action="store_true", help="Use backward compatibility") - parser.add_argument( - "-v", "--verbose", action="store_true", help="Add debug prints." - ) + parser.add_argument("-v", "--verbose", action="store_true", help="Add debug prints.") args = parser.parse_args() diff --git a/python/lsst/ts/salobj/configurable_csc.py b/python/lsst/ts/salobj/configurable_csc.py index 384be2b47..db3ba36af 100644 --- a/python/lsst/ts/salobj/configurable_csc.py +++ b/python/lsst/ts/salobj/configurable_csc.py @@ -31,6 +31,7 @@ import typing import yaml + from lsst.ts import utils from lsst.ts.xml import type_hints @@ -167,9 +168,7 @@ def __init__( config_dir = pathlib.Path(config_dir) if not config_dir.is_dir(): - raise ValueError( - f"config_dir={config_dir} does not exist or is not a directory" - ) + raise ValueError(f"config_dir={config_dir} does not exist or is not a directory") self.config_dir = config_dir # Interval between reading the config dir (seconds) @@ -221,9 +220,7 @@ def config_dir(self) -> pathlib.Path: def config_dir(self, config_dir: str | pathlib.Path) -> None: config_dir = pathlib.Path(config_dir).resolve() if not config_dir.is_dir(): - raise ValueError( - f"config_dir={config_dir} does not exist or is not a directory" - ) + raise ValueError(f"config_dir={config_dir} does not exist or is not a directory") self._config_dir = config_dir @classmethod @@ -287,9 +284,7 @@ def read_config_files( try: config_data = yaml.safe_load(config_raw_data) except Exception as e: - raise base.ExpectedError( - f"Could not parse data in {filepath} as a dict: {e!r}" - ) + raise base.ExpectedError(f"Could not parse data in {filepath} as a dict: {e!r}") if config_data is not None: hierarchical_update( main=config_dict, @@ -406,17 +401,11 @@ def _make_config_label_dict(self) -> dict[str, str]: if valid: output_dict[label] = config_name if invalid_labels: - self.log.warning( - f"Ignoring invalid labels {invalid_labels} in {labels_path}" - ) + self.log.warning(f"Ignoring invalid labels {invalid_labels} in {labels_path}") if invalid_files: - self.log.warning( - f"Ignoring invalid config file names {invalid_files} in {labels_path}" - ) + self.log.warning(f"Ignoring invalid config file names {invalid_files} in {labels_path}") if missing_files: - self.log.warning( - f"Ignoring missing config files {missing_files} in {labels_path}" - ) + self.log.warning(f"Ignoring missing config files {missing_files} in {labels_path}") return output_dict async def _report_summary_state(self) -> None: @@ -541,14 +530,11 @@ def _get_default_config_dir(self, name: str) -> pathlib.Path: try: config_pkg_dir: type_hints.PathType = os.environ[config_env_var_name] except KeyError: - raise RuntimeError( - f"Environment variable {config_env_var_name} not defined" - ) + raise RuntimeError(f"Environment variable {config_env_var_name} not defined") config_pkg_dir = pathlib.Path(config_pkg_dir).resolve() if not config_pkg_dir.is_dir(): raise RuntimeError( - f"{config_pkg_dir!r} = ${config_env_var_name} " - "does not exists or is not a directory" + f"{config_pkg_dir!r} = ${config_env_var_name} does not exists or is not a directory" ) config_dir = config_pkg_dir / name / self.schema_version @@ -574,9 +560,7 @@ def add_arguments(cls, parser: argparse.ArgumentParser) -> None: ) @classmethod - def add_kwargs_from_args( - cls, args: argparse.Namespace, kwargs: dict[str, typing.Any] - ) -> None: + def add_kwargs_from_args(cls, args: argparse.Namespace, kwargs: dict[str, typing.Any]) -> None: kwargs["config_dir"] = args.configdir if hasattr(args, "override"): kwargs["override"] = args.override diff --git a/python/lsst/ts/salobj/controller.py b/python/lsst/ts/salobj/controller.py index 39737cdbc..1fe5deaee 100644 --- a/python/lsst/ts/salobj/controller.py +++ b/python/lsst/ts/salobj/controller.py @@ -244,9 +244,7 @@ def __init__( domain = Domain() try: - self.salinfo = SalInfo( - domain=domain, name=name, index=index, write_only=write_only - ) + self.salinfo = SalInfo(domain=domain, name=name, index=index, write_only=write_only) new_identity = self.salinfo.name_index self.salinfo.identity = new_identity domain.default_identity = new_identity @@ -332,9 +330,7 @@ async def start(self) -> None: try: self._assign_cmd_callbacks() except Exception: - self.log.exception( - "Failed in start on _assign_cmd_callbacks; quitting." - ) + self.log.exception("Failed in start on _assign_cmd_callbacks; quitting.") await self.close() return @@ -351,9 +347,7 @@ async def start_phase2(self) -> None: def domain(self) -> Domain: return self.salinfo.domain - async def close( - self, exception: Exception | None = None, cancel_start: bool = True - ) -> None: + async def close(self, exception: Exception | None = None, cancel_start: bool = True) -> None: """Shut down, clean up resources and set done_task done. May be called multiple times. The first call closes the Controller; @@ -475,19 +469,13 @@ def _assert_do_methods_present( if not allow_missing_callbacks: unsupported_commands = set(command_names) - set(supported_command_names) if unsupported_commands: - needed_do_str = ", ".join( - f"do_{name}" for name in sorted(unsupported_commands) - ) + needed_do_str = ", ".join(f"do_{name}" for name in sorted(unsupported_commands)) err_msgs.append(f"must add {needed_do_str} methods") extra_commands = sorted( - set(supported_command_names) - - set(command_names) - - set(valid_extra_commands) + set(supported_command_names) - set(command_names) - set(valid_extra_commands) ) if extra_commands: - extra_do_str = ", ".join( - f"do_{name}" for name in sorted(extra_commands) - ) + extra_do_str = ", ".join(f"do_{name}" for name in sorted(extra_commands)) err_msgs.append(f"must remove {extra_do_str} methods") if not err_msgs: return diff --git a/python/lsst/ts/salobj/create_topics.py b/python/lsst/ts/salobj/create_topics.py index 99829289d..69bdca59a 100644 --- a/python/lsst/ts/salobj/create_topics.py +++ b/python/lsst/ts/salobj/create_topics.py @@ -37,8 +37,7 @@ def create_topics() -> None: parser.add_argument( "components", nargs="*", - help="Names of SAL components, e.g. 'Script ScriptQueue'. " - "Ignored if --all is specified", + help="Names of SAL components, e.g. 'Script ScriptQueue'. Ignored if --all is specified", ) parser.add_argument( "--all", diff --git a/python/lsst/ts/salobj/csc_commander.py b/python/lsst/ts/salobj/csc_commander.py index 32a6abc91..332c41db5 100644 --- a/python/lsst/ts/salobj/csc_commander.py +++ b/python/lsst/ts/salobj/csc_commander.py @@ -52,9 +52,7 @@ } -async def stream_as_generator( - stream: typing.TextIO, exit_str: str = "" -) -> AsyncGenerator[str, None]: +async def stream_as_generator(stream: typing.TextIO, exit_str: str = "") -> AsyncGenerator[str, None]: """Await lines of text from stdin or another text input stream. Example usage: @@ -237,17 +235,11 @@ def __init__( telemetry_fields_compare_digits: dict[str, int] | None = None, ) -> None: self.domain = domain.Domain() - self.remote = remote.Remote( - domain=self.domain, name=name, index=index, exclude=exclude - ) + self.remote = remote.Remote(domain=self.domain, name=name, index=index, exclude=exclude) self.fields_to_ignore = frozenset(fields_to_ignore) - self.telemetry_fields_to_not_compare = frozenset( - telemetry_fields_to_not_compare - ) + self.telemetry_fields_to_not_compare = frozenset(telemetry_fields_to_not_compare) self.telemetry_fields_compare_digits = ( - {} - if telemetry_fields_compare_digits is None - else telemetry_fields_compare_digits + {} if telemetry_fields_compare_digits is None else telemetry_fields_compare_digits ) self.tasks: set[asyncio.Future] = set() self.help_dict: dict[str, str] = dict() @@ -270,9 +262,7 @@ def __init__( topic = getattr(self.remote, topic_attr_name) callback = getattr(self, f"{topic_attr_name}_callback", None) if callback is None: - callback = functools.partial( - self.telemetry_callback, name=telemetry_name - ) + callback = functools.partial(self.telemetry_callback, name=telemetry_name) setattr(topic, "callback", callback) # Dict of command name: RemoteCommand topic: @@ -289,8 +279,7 @@ def __init__( sync_do_member_names = [ member[0] for member in all_members - if member[0].startswith("do_") - and not inspect.iscoroutinefunction(member[1]) + if member[0].startswith("do_") and not inspect.iscoroutinefunction(member[1]) ] if sync_do_member_names: warnings.warn( @@ -369,15 +358,9 @@ def get_public_data(self, data: type_hints.BaseMsgType) -> dict[str, typing.Any] data : `BaseMsgType` Message. """ - return dict( - (key, value) - for key, value in vars(data).items() - if self.field_is_public(key) - ) + return dict((key, value) for key, value in vars(data).items() if self.field_is_public(key)) - def get_rounded_public_data( - self, data: type_hints.BaseMsgType, digits: int = 2 - ) -> dict[str, typing.Any]: + def get_rounded_public_data(self, data: type_hints.BaseMsgType, digits: int = 2) -> dict[str, typing.Any]: """Get a dict of field_name: value for public fields of a DDS sample with float values rounded. """ @@ -404,9 +387,7 @@ def get_telemetry_comparison_dict( ``telemetry_fields_compare_digits``. """ return { - key: round_any( - value, digits=self.telemetry_fields_compare_digits.get(key, digits) - ) + key: round_any(value, digits=self.telemetry_fields_compare_digits.get(key, digits)) for key, value in public_dict.items() if key not in self.telemetry_fields_to_not_compare } @@ -428,9 +409,7 @@ async def evt_logMessage_callback(self, data: type_hints.BaseMsgType) -> None: public_data = {key: getattr(data, key) for key in ("name", "level", "message")} if data.traceback: # type: ignore public_data["traceback"] = data.traceback # type: ignore - self.output( - f"{data.private_sndStamp:0.3f}: logMessage: {self.format_dict(public_data)}" - ) + self.output(f"{data.private_sndStamp:0.3f}: logMessage: {self.format_dict(public_data)}") async def evt_summaryState_callback(self, data: type_hints.BaseMsgType) -> None: state_int: int = data.summaryState # type: ignore @@ -438,13 +417,9 @@ async def evt_summaryState_callback(self, data: type_hints.BaseMsgType) -> None: state_repr: str = repr(sal_enums.State(state_int)) except Exception: state_repr = f"{state_int} (not a known state!)" - self.output( - f"{data.private_sndStamp:0.3f}: summaryState: summaryState={state_repr}" - ) + self.output(f"{data.private_sndStamp:0.3f}: summaryState: summaryState={state_repr}") - async def telemetry_callback( - self, data: type_hints.BaseMsgType, name: str, digits: int = 2 - ) -> None: + async def telemetry_callback(self, data: type_hints.BaseMsgType, name: str, digits: int = 2) -> None: """Default callback for telemetry topics. Print the telemetry information if it has changed enough @@ -475,9 +450,7 @@ async def telemetry_callback( """ prev_value_name = f"previous_{name}" public_dict = self.get_public_data(data) - comparison_dict = self.get_telemetry_comparison_dict( - public_dict=public_dict, digits=digits - ) + comparison_dict = self.get_telemetry_comparison_dict(public_dict=public_dict, digits=digits) if comparison_dict != getattr(self, prev_value_name, None): setattr(self, prev_value_name, comparison_dict) formatted_data = self.format_dict(public_dict) @@ -541,9 +514,7 @@ def cast( """ if isinstance(name, tuple): if len(name) != 2: - raise RuntimeError( - "Cannot parse {name} as (name, casting function)" - ) + raise RuntimeError("Cannot parse {name} as (name, casting function)") arg_name, cast_func = name return (arg_name, cast_func(arg)) else: @@ -580,14 +551,11 @@ def get_commands_help(self) -> list[str]: help_strings.append(f"{command_name} {field_names_str}") other_command_names = sorted( - command_name - for command_name in self.help_dict - if command_name not in self.command_dict + command_name for command_name in self.help_dict if command_name not in self.command_dict ) help_strings += ["", "Other Commands:"] help_strings += [ - f"{command_name} {self.help_dict[command_name]}" - for command_name in other_command_names + f"{command_name} {self.help_dict[command_name]}" for command_name in other_command_names ] return help_strings @@ -613,10 +581,7 @@ async def run_command_topic(self, command_name: str, args: Sequence[str]) -> Non sample = command.DataType() kwargs = self.get_public_data(sample) if len(kwargs) != len(args): - raise ValueError( - f"Command {command_name} requires " - f"{len(kwargs)} arguments; got {len(args)}" - ) + raise ValueError(f"Command {command_name} requires {len(kwargs)} arguments; got {len(args)}") for (name, default_value), str_value in zip(kwargs.items(), args): try: if isinstance(default_value, bool): @@ -702,9 +667,7 @@ async def start(self) -> None: self.remote.evt_summaryState.callback = summary_state_callback # type: ignore @classmethod - async def amain( - cls, *, index: int | enum.IntEnum | bool | None, **kwargs: typing.Any - ) -> None: + async def amain(cls, *, index: int | enum.IntEnum | bool | None, **kwargs: typing.Any) -> None: """Construct the commander and run it. Parse the command line to construct the commander, @@ -759,9 +722,7 @@ def add_arguments(cls, parser: argparse.ArgumentParser) -> None: pass @classmethod - def add_kwargs_from_args( - cls, args: argparse.Namespace, kwargs: dict[str, typing.Any] - ) -> None: + def add_kwargs_from_args(cls, args: argparse.Namespace, kwargs: dict[str, typing.Any]) -> None: """Add constructor keyword arguments based on parsed arguments. Parameters @@ -821,13 +782,12 @@ def make_from_cmd_line( # when index is an int or bool. choices = [int(item.value) for item in index] # type: ignore names_str = ", ".join( - f"{item.value}: {item.name.lower()}" for item in index # type: ignore + f"{item.value}: {item.name.lower()}" + for item in index # type: ignore ) help_text = f"SAL index, one of: {names_str}" parser.add_argument("index", type=int, help=help_text, choices=choices) - parser.add_argument( - "-e", "--enable", action="store_true", help="Enable the CSC?" - ) + parser.add_argument("-e", "--enable", action="store_true", help="Enable the CSC?") cls.add_arguments(parser) args = parser.parse_args() diff --git a/python/lsst/ts/salobj/csc_utils.py b/python/lsst/ts/salobj/csc_utils.py index 04f3fc6c9..0b5677965 100644 --- a/python/lsst/ts/salobj/csc_utils.py +++ b/python/lsst/ts/salobj/csc_utils.py @@ -79,9 +79,7 @@ def make_state_transition_dict() -> StateTransitionDictType: _STATE_TRANSITION_DICT = make_state_transition_dict() -def get_expected_summary_states( - initial_state: State | int, final_state: State | int -) -> list[State]: +def get_expected_summary_states(initial_state: State | int, final_state: State | int) -> list[State]: """Return all summary states expected when transitioning from one state to another. """ @@ -163,9 +161,7 @@ async def set_summary_state( try: await cmd.start(timeout=timeout) except Exception as e: - raise RuntimeError( - f"Error on cmd=cmd_{command}, initial_state={current_state}: {e}" - ) from e + raise RuntimeError(f"Error on cmd=cmd_{command}, initial_state={current_state}: {e}") from e states.append(resulting_state) finally: remote.cmd_start.data.configurationOverride = old_override # type: ignore diff --git a/python/lsst/ts/salobj/delete_topics.py b/python/lsst/ts/salobj/delete_topics.py index 042c9fb33..cf47c5fbb 100644 --- a/python/lsst/ts/salobj/delete_topics.py +++ b/python/lsst/ts/salobj/delete_topics.py @@ -51,8 +51,7 @@ def from_args(cls) -> Self: parser.add_argument( "components", nargs="*", - help="Names of SAL components, e.g. 'Script ScriptQueue'. " - "Ignored if --all is specified", + help="Names of SAL components, e.g. 'Script ScriptQueue'. Ignored if --all is specified", ) parser.add_argument( "--all", @@ -108,9 +107,7 @@ def __init__( log: logging.Logger | None = None, ) -> None: self.log = ( - logging.getLogger(type(self).__name__) - if log is None - else log.getChild(type(self).__name__) + logging.getLogger(type(self).__name__) if log is None else log.getChild(type(self).__name__) ) self.admin_client = admin_client self.schema_registry_client = schema_registry_client @@ -136,9 +133,7 @@ async def new(cls) -> Self: schema_registry_client = SchemaRegistryClient(dict(url=schema_registry_url)) - return cls( - admin_client=admin_client, schema_registry_client=schema_registry_client - ) + return cls(admin_client=admin_client, schema_registry_client=schema_registry_client) def retrieve_topic_summary(self) -> dict[str, dict[str, str]]: """Retrieve a summary of the topics in the broker. @@ -231,9 +226,7 @@ def delete_topics(self, topics_to_delete: list[str]) -> None: for topic in topics_to_delete: self.assert_topic_has_no_consumers(topic=topic) - delete_futures = self.admin_client.delete_topics( - topics_to_delete, operation_timeout=60 - ) + delete_futures = self.admin_client.delete_topics(topics_to_delete, operation_timeout=60) for topic, future in delete_futures.items(): try: future.result() @@ -274,18 +267,14 @@ def assert_topic_has_no_consumers(self, topic: str) -> None: for desc in group_descs.values(): for member in desc.result().members: topics = [tp.topic for tp in member.assignment.topic_partitions] - assert ( - topic not in topics - ), f"Found consumer in group '{group_id}' assigned to topic '{topic}'." + assert topic not in topics, ( + f"Found consumer in group '{group_id}' assigned to topic '{topic}'." + ) def execute(self, delete_topics_args: DeleteTopicsArgs | None = None) -> None: """Execute the delete topic operation.""" - args = ( - DeleteTopicsArgs.from_args() - if delete_topics_args is None - else delete_topics_args - ) + args = DeleteTopicsArgs.from_args() if delete_topics_args is None else delete_topics_args if args.log_level is not None: logging.basicConfig(level=getattr(logging, args.log_level)) @@ -304,9 +293,7 @@ def execute(self, delete_topics_args: DeleteTopicsArgs | None = None) -> None: topics_to_delete = [ topic for topic in topics_summary - if ( - args.all_topics or topics_summary[topic]["component"] in args.components - ) + if (args.all_topics or topics_summary[topic]["component"] in args.components) and ( (args.all_topics and topics_summary[topic]["subname"] != "sal") or topics_summary[topic]["subname"] == args.subname @@ -316,10 +303,7 @@ def execute(self, delete_topics_args: DeleteTopicsArgs | None = None) -> None: schema_to_delete = [ subject for subject in schema_summary - if ( - args.all_topics - or schema_summary[subject]["component"] in args.components - ) + if (args.all_topics or schema_summary[subject]["component"] in args.components) and ( (args.all_topics and schema_summary[subject]["subname"] != "sal") or schema_summary[subject]["subname"] == args.subname diff --git a/python/lsst/ts/salobj/make_mock_write_topics.py b/python/lsst/ts/salobj/make_mock_write_topics.py index 3e267574e..762516d8f 100644 --- a/python/lsst/ts/salobj/make_mock_write_topics.py +++ b/python/lsst/ts/salobj/make_mock_write_topics.py @@ -58,13 +58,10 @@ async def make_mock_write_topics( if not attr_names: raise ValueError("You must provide one or more topic attr_names") - async with Domain() as domain, SalInfo( - domain=domain, name=name, index=index - ) as salinfo: + async with Domain() as domain, SalInfo(domain=domain, name=name, index=index) as salinfo: if index is None: index = 1 if salinfo.indexed else 0 topics_dict = { - attr_name: MockWriteTopic(salinfo=salinfo, attr_name=attr_name) - for attr_name in attr_names + attr_name: MockWriteTopic(salinfo=salinfo, attr_name=attr_name) for attr_name in attr_names } yield types.SimpleNamespace(**topics_dict) diff --git a/python/lsst/ts/salobj/sal_enums.py b/python/lsst/ts/salobj/sal_enums.py index 334dd8a54..26d669027 100644 --- a/python/lsst/ts/salobj/sal_enums.py +++ b/python/lsst/ts/salobj/sal_enums.py @@ -1,4 +1,5 @@ -from lsst.ts.xml.sal_enums import SalRetCode # noqa: F401 -from lsst.ts.xml.sal_enums import State # noqa: F401 -from lsst.ts.xml.sal_enums import as_salRetCode # noqa: F401 -from lsst.ts.xml.sal_enums import as_state # noqa: F401; noqa: F401 +from lsst.ts.xml.sal_enums import ( + SalRetCode, # noqa + State, # noqa + as_salRetCode, # noqa +) diff --git a/python/lsst/ts/salobj/sal_info.py b/python/lsst/ts/salobj/sal_info.py index 504f41bb4..ab873a5ff 100644 --- a/python/lsst/ts/salobj/sal_info.py +++ b/python/lsst/ts/salobj/sal_info.py @@ -58,6 +58,7 @@ SerializationError, ) from fastavro.read import SchemaResolutionError + from lsst.ts import utils from lsst.ts.xml import sal_enums, type_hints from lsst.ts.xml.component_info import ComponentInfo @@ -236,9 +237,7 @@ def __init__( raise TypeError(f"domain {domain!r} must be an lsst.ts.salobj.Domain") if index is not None: if not (isinstance(index, int) or isinstance(index, enum.IntEnum)): - raise TypeError( - f"index {index!r} must be an integer, enum.IntEnum, or None" - ) + raise TypeError(f"index {index!r} must be an integer, enum.IntEnum, or None") self.isopen = False self._closing = False self.domain = domain @@ -262,8 +261,8 @@ def __init__( # Dict of kafka topic name: dict of index: data # Only used for indexed components. - self._history_index_data: dict[str, dict[int, type_hints.BaseDdsDataType]] = ( - collections.defaultdict(dict) + self._history_index_data: dict[str, dict[int, type_hints.BaseDdsDataType]] = collections.defaultdict( + dict ) self._consumer: Consumer | None = None @@ -271,37 +270,23 @@ def __init__( # Dict of kafka topic name: (deserializer, serialization context) # for read topics. - self.deserializers_and_contexts: dict[ - str, tuple[AvroDeserializer, SerializationContext] - ] = dict() + self.deserializers_and_contexts: dict[str, tuple[AvroDeserializer, SerializationContext]] = dict() # Dict of kafka topic name: (serializer, serialization context) # for write topics. - self._serializers_and_contexts: dict[ - str, tuple[AvroSerializer, SerializationContext, str] - ] = dict() + self._serializers_and_contexts: dict[str, tuple[AvroSerializer, SerializationContext, str]] = dict() topic_subname = os.environ.get("LSST_TOPIC_SUBNAME", None) if not topic_subname: - raise RuntimeError( - "You must define environment variable LSST_TOPIC_SUBNAME" - ) + raise RuntimeError("You must define environment variable LSST_TOPIC_SUBNAME") - self.kafka_broker_addr = os.environ.get( - "LSST_KAFKA_BROKER_ADDR", DEFAULT_LSST_KAFKA_BROKER_ADDR - ) + self.kafka_broker_addr = os.environ.get("LSST_KAFKA_BROKER_ADDR", DEFAULT_LSST_KAFKA_BROKER_ADDR) self.schema_registry_url = os.environ.get( "LSST_SCHEMA_REGISTRY_URL", DEFAULT_LSST_SCHEMA_REGISTRY_URL ) - self.sasl_plain_username: None | str = os.environ.get( - "LSST_KAFKA_SECURITY_USERNAME", None - ) - self.sasl_plain_password: None | str = os.environ.get( - "LSST_KAFKA_SECURITY_PASSWORD", None - ) + self.sasl_plain_username: None | str = os.environ.get("LSST_KAFKA_SECURITY_USERNAME", None) + self.sasl_plain_password: None | str = os.environ.get("LSST_KAFKA_SECURITY_PASSWORD", None) self.replication_factor = int( - os.environ.get( - "LSST_KAFKA_REPLICATION_FACTOR", DEFAULT_LSST_KAFKA_REPLICATION_FACTOR - ) + os.environ.get("LSST_KAFKA_REPLICATION_FACTOR", DEFAULT_LSST_KAFKA_REPLICATION_FACTOR) ) self.component_info = ComponentInfo(topic_subname=topic_subname, name=name) @@ -310,9 +295,7 @@ def __init__( # of the initialization. name_index = self.name_index group_id_identity = ( - f"{self.identity}-{name_index}" - if self.identity != name_index - else f"{name_index}" + f"{self.identity}-{name_index}" if self.identity != name_index else f"{name_index}" ) self.group_id = f"{group_id_identity}-{get_random_string()}" self.command_names = tuple( @@ -376,13 +359,9 @@ def __init__( self._run_kafka_result = utils.make_done_future() if self.index != 0 and not self.indexed: - raise ValueError( - f"Index={index!r} must be 0 or None; {name} is not an indexed SAL component" - ) + raise ValueError(f"Index={index!r} must be 0 or None; {name} is not an indexed SAL component") if len(self.command_names) > 0: - self._ackcmd_type = self.component_info.topics[ - "ack_ackcmd" - ].make_dataclass() + self._ackcmd_type = self.component_info.topics["ack_ackcmd"].make_dataclass() domain.add_salinfo(self) @@ -459,9 +438,7 @@ def running(self) -> bool: def started(self) -> bool: """Return True if successfully started, False otherwise.""" return ( - self.start_task.done() - and not self.start_task.cancelled() - and self.start_task.exception() is None + self.start_task.done() and not self.start_task.cancelled() and self.start_task.exception() is None ) def assert_started(self) -> None: @@ -655,9 +632,7 @@ def add_writer(self, topic: topics.WriteTopic) -> None: if self.start_called: raise RuntimeError("Cannot add topics after the start called") if topic.topic_info.kafka_name in self._write_topics: - raise ValueError( - f"Write topic {topic.topic_info.kafka_name} already present" - ) + raise ValueError(f"Write topic {topic.topic_info.kafka_name} already present") self._write_topics[topic.topic_info.kafka_name] = topic async def start(self) -> None: @@ -746,18 +721,10 @@ def _blocking_setup_kafka(self) -> None: * _serializers_and_contexts """ self._blocking_create_topics() - self._schema_registry_client = SchemaRegistryClient( - dict(url=self.schema_registry_url) - ) - self._blocking_register_schema( - schema_registry_client=self._schema_registry_client - ) - self._blocking_create_deserializers( - schema_registry_client=self._schema_registry_client - ) - self._blocking_create_serializers( - schema_registry_client=self._schema_registry_client - ) + self._schema_registry_client = SchemaRegistryClient(dict(url=self.schema_registry_url)) + self._blocking_register_schema(schema_registry_client=self._schema_registry_client) + self._blocking_create_deserializers(schema_registry_client=self._schema_registry_client) + self._blocking_create_serializers(schema_registry_client=self._schema_registry_client) self._blocking_create_producer() self._blocking_create_consumer() @@ -769,9 +736,7 @@ def _blocking_create_topics(self) -> None: # and self._write_topics. topic_infos = { topic.topic_info.kafka_name: topic.topic_info - for topic in itertools.chain( - self._read_topics.values(), self._write_topics.values() - ) + for topic in itertools.chain(self._read_topics.values(), self._write_topics.values()) } if not topic_infos: self.log.warning(f"{self} has no topics") @@ -783,11 +748,7 @@ def _blocking_create_topics(self) -> None: topic=topic_info.kafka_name, num_partitions=topic_info.partitions, replication_factor=self.replication_factor, - config=( - {"cleanup.policy": "compact"} - if topic_info.attr_name.startswith("evt_") - else {} - ), + config=({"cleanup.policy": "compact"} if topic_info.attr_name.startswith("evt_") else {}), ) for topic_info in topic_infos.values() ] @@ -807,9 +768,7 @@ def _blocking_create_topics(self) -> None: topics_list = broker_client.list_topics() - topics_to_create = [ - topic for topic in new_topic_list if topic.topic not in topics_list.topics - ] + topics_to_create = [topic for topic in new_topic_list if topic.topic not in topics_list.topics] while topics_to_create: create_result = broker_client.create_topics(new_topic_list) @@ -825,9 +784,7 @@ def _blocking_create_topics(self) -> None: ): continue else: - self.log.exception( - f"Failed to create topic {kafka_name}: {exception!r}" - ) + self.log.exception(f"Failed to create topic {kafka_name}: {exception!r}") raise exception # The existence of the poll method is not documented, but failing # to call it causes tests/test_speed.py test_write to fail. @@ -835,11 +792,7 @@ def _blocking_create_topics(self) -> None: topics_list = broker_client.list_topics() - topics_to_create = [ - topic - for topic in new_topic_list - if topic.topic not in topics_list.topics - ] + topics_to_create = [topic for topic in new_topic_list if topic.topic not in topics_list.topics] def get_broker_client_configuration(self) -> dict[str, typing.Any]: """Get the broker client configuration. @@ -853,10 +806,7 @@ def get_broker_client_configuration(self) -> dict[str, typing.Any]: "bootstrap.servers": self.kafka_broker_addr, } - if ( - self.sasl_plain_username is not None - and self.sasl_plain_password is not None - ): + if self.sasl_plain_username is not None and self.sasl_plain_password is not None: broker_client_configuration["security.protocol"] = os.environ.get( "LSST_KAFKA_SECURITY_PROTOCOL", DEFAULT_SECURITY_PROTOCOL ) @@ -869,9 +819,7 @@ def get_broker_client_configuration(self) -> dict[str, typing.Any]: if "LSST_KAFKA_BROKER_CLIENT_CONFIGURATION" in os.environ: with open(os.environ["LSST_KAFKA_BROKER_CLIENT_CONFIGURATION"]) as fp: additional_broker_client_configuration = yaml.safe_load(fp) - broker_client_configuration.update( - additional_broker_client_configuration - ) + broker_client_configuration.update(additional_broker_client_configuration) return broker_client_configuration @@ -911,9 +859,7 @@ def _blocking_create_consumer(self) -> None: self._consumer = Consumer(consumer_configuration) read_topic_names = list(self._read_topics.keys()) - self._consumer.subscribe( - read_topic_names, on_assign=self._blocking_on_assign_callback - ) + self._consumer.subscribe(read_topic_names, on_assign=self._blocking_on_assign_callback) def _blocking_create_producer(self) -> None: """Create self._producer. @@ -963,20 +909,14 @@ async def flush_loop(self) -> None: self._producer.flush() await asyncio.sleep(self._flush_period) - def _blocking_register_schema( - self, schema_registry_client: SchemaRegistryClient - ) -> None: + def _blocking_register_schema(self, schema_registry_client: SchemaRegistryClient) -> None: """Register Avro schemas for all topics.""" - for topic in itertools.chain( - self._read_topics.values(), self._write_topics.values() - ): + for topic in itertools.chain(self._read_topics.values(), self._write_topics.values()): topic_info = topic.topic_info schema = Schema(json.dumps(topic_info.make_avro_schema()), "AVRO") schema_registry_client.register_schema(topic_info.avro_subject, schema) - def _blocking_create_deserializers( - self, schema_registry_client: SchemaRegistryClient - ) -> None: + def _blocking_create_deserializers(self, schema_registry_client: SchemaRegistryClient) -> None: """Create Kafka deserializers for read topics. Set self._deserializers_and_contexts @@ -989,17 +929,13 @@ def _blocking_create_deserializers( schema_registry_client=schema_registry_client, schema_str=json.dumps(topic.topic_info.make_avro_schema()), ), - SerializationContext( - topic=topic.topic_info.kafka_name, field=MessageField.VALUE - ), + SerializationContext(topic=topic.topic_info.kafka_name, field=MessageField.VALUE), ) for topic in self._read_topics.values() } self._deserializers_and_contexts = deserializers_and_contexts - def _blocking_create_serializers( - self, schema_registry_client: SchemaRegistryClient - ) -> None: + def _blocking_create_serializers(self, schema_registry_client: SchemaRegistryClient) -> None: """Create Kafka serializers for write topics. Set self._serializers_and_contexts @@ -1013,9 +949,7 @@ def _blocking_create_serializers( schema_str=json.dumps(topic.topic_info.make_avro_schema()), conf={"auto.register.schemas": False}, ), - SerializationContext( - topic=topic.topic_info.kafka_name, field=MessageField.VALUE - ), + SerializationContext(topic=topic.topic_info.kafka_name, field=MessageField.VALUE), ( "" if topic.topic_info.attr_name.startswith("tel_") @@ -1034,9 +968,7 @@ def _blocking_create_serializers( } self._serializers_and_contexts = serializers_and_contexts - def _blocking_on_assign_callback( - self, consumer: Consumer, partitions: list[TopicPartition] - ) -> None: + def _blocking_on_assign_callback(self, consumer: Consumer, partitions: list[TopicPartition]) -> None: """Set partition offsets to read historical data. Intended as the Consumer.subscribe on_assign callback function. @@ -1082,9 +1014,7 @@ def _blocking_on_assign_callback( history_offsets: dict[str, int] = dict() for partition in partitions: - min_offset, max_offset = self._consumer.get_watermark_offsets( - partition, cached=False - ) + min_offset, max_offset = self._consumer.get_watermark_offsets(partition, cached=False) # print( # f"{self.index} {partition.topic} " # f"{min_offset=}, {max_offset=}" @@ -1149,9 +1079,7 @@ def _blocking_write( def callback(err: KafkaError, _: Message) -> None: if err: - self.loop.call_soon_threadsafe( - future.set_exception, KafkaException(err) - ) + self.loop.call_soon_threadsafe(future.set_exception, KafkaException(err)) else: dt = time.monotonic() - t0 self.loop.call_soon_threadsafe(future.set_result, None) @@ -1204,9 +1132,7 @@ def _close_kafka(self) -> None: else: self.log.info(f"Ignoring {kafka_error=}.") except Exception: - self.log.exception( - f"Error while waiting for consumer group {self.group_id} to be deleted." - ) + self.log.exception(f"Error while waiting for consumer group {self.group_id} to be deleted.") async def _read_loop(self) -> None: """Read and process messages.""" @@ -1243,9 +1169,7 @@ async def _read_loop(self) -> None: and self._history_offsets_retrieved and not self._history_offsets ): - started_duration = ( - time.monotonic() - self.read_history_start_monotonic - ) + started_duration = time.monotonic() - self.read_history_start_monotonic self.log.info(f"Started in {started_duration:0.2f} seconds") self.start_task.set_result(None) continue @@ -1350,11 +1274,7 @@ def _process_message( ) schema_resolution_errors[kafka_name] = 0 - elif ( - schema_resolution_errors[kafka_name] - % SCHEMA_RESOLUTION_LOG_ERROR_THRESHOLD - == 0 - ): + elif schema_resolution_errors[kafka_name] % SCHEMA_RESOLUTION_LOG_ERROR_THRESHOLD == 0: self.log.error( f"Failed to deserialize {schema_resolution_errors[kafka_name]} samples of " f"{kafka_name}. Check schema compatibility!" @@ -1395,10 +1315,7 @@ def _process_message( self._history_index_data[kafka_name][data.salIndex] = data if offset >= history_offset: - - self.log.debug( - f"{self.group_id=}::Finished handling historical data for {kafka_name=}." - ) + self.log.debug(f"{self.group_id=}::Finished handling historical data for {kafka_name=}.") # We're done with history for this topic del self._history_offsets[kafka_name] @@ -1414,20 +1331,14 @@ def _process_message( read_topic._queue_data([data]) if not self._history_offsets: - read_history_duration = ( - time.monotonic() - self.read_history_start_monotonic - ) - self.log.info( - f"Reading historic data took {read_history_duration:0.2f} seconds" - ) + read_history_duration = time.monotonic() - self.read_history_start_monotonic + self.log.info(f"Reading historic data took {read_history_duration:0.2f} seconds") if not self.start_task.done(): self.start_task.set_result(None) return sequential_read_errors - async def write_data( - self, topic_info: TopicInfo, data_dict: dict[str, typing.Any] - ) -> None: + async def write_data(self, topic_info: TopicInfo, data_dict: dict[str, typing.Any]) -> None: """Write a message. Parameters @@ -1441,14 +1352,10 @@ async def write_data( try: future = self.loop.create_future() - await self.loop.run_in_executor( - self.pool, self._blocking_write, topic_info, data_dict, future - ) + await self.loop.run_in_executor(self.pool, self._blocking_write, topic_info, data_dict, future) except Exception: - self.log.exception( - f"write_data(topic_info={topic_info}, data_dict={data_dict} failed" - ) + self.log.exception(f"write_data(topic_info={topic_info}, data_dict={data_dict} failed") raise async def __aenter__(self) -> SalInfo: diff --git a/python/lsst/ts/salobj/sal_log_handler.py b/python/lsst/ts/salobj/sal_log_handler.py index ae55d1b95..038c4de50 100644 --- a/python/lsst/ts/salobj/sal_log_handler.py +++ b/python/lsst/ts/salobj/sal_log_handler.py @@ -55,9 +55,7 @@ def __init__(self, controller: Controller) -> None: self.loop = asyncio.get_running_loop() self.main_thread_id = threading.get_ident() self.futures: list[MixedFutureType] = list() - self._pre_start_records: collections.deque[logging.LogRecord] = ( - collections.deque(maxlen=MAX_LEN) - ) + self._pre_start_records: collections.deque[logging.LogRecord] = collections.deque(maxlen=MAX_LEN) super().__init__() def close(self) -> None: @@ -110,8 +108,7 @@ def emit(self, record: logging.LogRecord) -> None: self.futures = [f for f in self.futures if not f.done()] + [new_future] except Exception as e: print( - f"SalLogHandler.emit of level={record.levelno}, " - f"message={message!r} failed: {e!r}", + f"SalLogHandler.emit of level={record.levelno}, message={message!r} failed: {e!r}", file=sys.stderr, ) finally: @@ -144,7 +141,6 @@ async def _async_emit( ) except Exception as e: print( - f"SalLogHandler._async_emit of level={level}, " - f"message={message!r} failed: {e!r}", + f"SalLogHandler._async_emit of level={level}, message={message!r} failed: {e!r}", file=sys.stderr, ) diff --git a/python/lsst/ts/salobj/testcsc.py b/python/lsst/ts/salobj/testcsc.py index 42def9a0c..f631b3fe8 100644 --- a/python/lsst/ts/salobj/testcsc.py +++ b/python/lsst/ts/salobj/testcsc.py @@ -28,6 +28,7 @@ from collections.abc import Sequence import numpy as np + from lsst.ts.xml import type_hints from . import __version__ @@ -275,9 +276,7 @@ def assert_arrays_equal(self, arrays1: typing.Any, arrays2: typing.Any) -> None: if not np.array_equal( # type: ignore field_arr1, field_arr2, equal_nan=is_float ): - raise AssertionError( - f"arrays1.{field} = {field_arr1} != {field_arr2} = arrays2.{field}" - ) + raise AssertionError(f"arrays1.{field} = {field_arr1} != {field_arr2} = arrays2.{field}") def assert_scalars_equal(self, scalars1: typing.Any, scalars2: typing.Any) -> None: """Assert that two scalars data structs are equal. @@ -299,9 +298,7 @@ def assert_scalars_equal(self, scalars1: typing.Any, scalars2: typing.Any) -> No if not np.array_equal( # type: ignore field_val1, field_val2, equal_nan=is_float ): - raise AssertionError( - f"scalars1.{field} = {field_val1} != {field_val2} = scalars2.{field}" - ) + raise AssertionError(f"scalars1.{field} = {field_val1} != {field_val2} = scalars2.{field}") def make_random_arrays_dict(self) -> dict[str, typing.Any]: """Make a random arrays data dict.""" @@ -313,15 +310,11 @@ def make_random_arrays_dict(self) -> dict[str, typing.Any]: field_type = self.field_type[field] nelts = len(getattr(blank_arrays_data, field)) iinfo = np.iinfo(field_type) - arrays_dict[field] = np.random.randint( - iinfo.min, iinfo.max, size=(nelts,), dtype=field_type - ) + arrays_dict[field] = np.random.randint(iinfo.min, iinfo.max, size=(nelts,), dtype=field_type) for field in ("float0", "double0"): field_type = self.field_type[field] nelts = len(getattr(blank_arrays_data, field)) - arrays_dict[field] = np.array( - np.random.uniform(-1e5, 1e5, size=(nelts,)), dtype=field_type - ) + arrays_dict[field] = np.array(np.random.uniform(-1e5, 1e5, size=(nelts,)), dtype=field_type) return arrays_dict def make_random_scalars_dict(self) -> dict[str, typing.Any]: @@ -333,9 +326,7 @@ def make_random_scalars_dict(self) -> dict[str, typing.Any]: for field in self.int_fields: field_type = self.field_type[field] iinfo = np.iinfo(field_type) - scalars_dict[field] = np.random.randint( - iinfo.min, iinfo.max, dtype=field_type - ) + scalars_dict[field] = np.random.randint(iinfo.min, iinfo.max, dtype=field_type) for field in ("float0", "double0"): field_type = self.field_type[field] scalars_dict[field] = field_type(np.random.uniform(-1e5, 1e5)) diff --git a/python/lsst/ts/salobj/testcsccommander.py b/python/lsst/ts/salobj/testcsccommander.py index 37d705fe5..8eb4ef310 100644 --- a/python/lsst/ts/salobj/testcsccommander.py +++ b/python/lsst/ts/salobj/testcsccommander.py @@ -82,9 +82,7 @@ async def do_setArrays(self, args: Sequence[str]) -> None: field_type = self.array_field_types[field] vals = [field_type(val) for val in valstr.split(",")] # type: ignore if len(vals) > ARR_LEN: - raise RuntimeError( - f"Field {field} has more than {ARR_LEN} values: {vals}" - ) + raise RuntimeError(f"Field {field} has more than {ARR_LEN} values: {vals}") elif len(vals) < ARR_LEN: n_to_append = ARR_LEN - len(vals) vals += [field_type("0")] * n_to_append # type: ignore diff --git a/python/lsst/ts/salobj/testscript.py b/python/lsst/ts/salobj/testscript.py index 16ff7e9de..33302e8fa 100644 --- a/python/lsst/ts/salobj/testscript.py +++ b/python/lsst/ts/salobj/testscript.py @@ -26,6 +26,7 @@ import typing import yaml + from lsst.ts import salobj from .base_script import BaseScript @@ -121,9 +122,7 @@ async def run(self) -> None: self.log.info("Run started") await self.checkpoint("start") if self.config.fail_run: - raise salobj.ExpectedError( - f"Failed in run after wait: fail_run={self.config.fail_run}" - ) + raise salobj.ExpectedError(f"Failed in run after wait: fail_run={self.config.fail_run}") await asyncio.sleep(self.config.wait_time) await self.checkpoint("end") self.log.info("Run succeeded") @@ -131,7 +130,5 @@ async def run(self) -> None: async def cleanup(self) -> None: self.log.info("Cleanup started") if self.config.fail_cleanup: - raise salobj.ExpectedError( - f"Failed in cleanup: fail_cleanup={self.config.fail_cleanup}" - ) + raise salobj.ExpectedError(f"Failed in cleanup: fail_cleanup={self.config.fail_cleanup}") self.log.info("Cleanup succeeded") diff --git a/python/lsst/ts/salobj/testutils.py b/python/lsst/ts/salobj/testutils.py index 61c8311e9..ab4fe0ae2 100644 --- a/python/lsst/ts/salobj/testutils.py +++ b/python/lsst/ts/salobj/testutils.py @@ -66,9 +66,7 @@ def assertRaisesAckError( if error is not None and e.ackcmd.error != error: raise AssertionError(f"ackcmd.error={e.ackcmd.error} instead of {error}") if result_contains is not None and result_contains not in e.ackcmd.result: - raise AssertionError( - f"ackcmd.result={e.ackcmd.result} does not contain {result_contains}" - ) + raise AssertionError(f"ackcmd.result={e.ackcmd.result} does not contain {result_contains}") @contextlib.contextmanager @@ -123,11 +121,7 @@ def set_test_topic_subname(randomize: bool = False) -> None: """ pid = os.getpid() length = (pid.bit_length() + 7) // 8 - root_value = ( - pid.to_bytes(length=length, byteorder="big") - if not randomize - else os.urandom(12) - ) + root_value = pid.to_bytes(length=length, byteorder="big") if not randomize else os.urandom(12) topic_subname_suffix = ( base64.urlsafe_b64encode(root_value) diff --git a/python/lsst/ts/salobj/topics/base_topic.py b/python/lsst/ts/salobj/topics/base_topic.py index 2327fd33d..1b095b392 100644 --- a/python/lsst/ts/salobj/topics/base_topic.py +++ b/python/lsst/ts/salobj/topics/base_topic.py @@ -67,9 +67,7 @@ def __init__(self, *, salinfo: SalInfo, attr_name: str) -> None: self._type = self.topic_info.make_dataclass() except Exception as e: - raise RuntimeError( - f"Failed to create topic {salinfo.name}.{attr_name}" - ) from e + raise RuntimeError(f"Failed to create topic {salinfo.name}.{attr_name}") from e @property def attr_name(self) -> str: diff --git a/python/lsst/ts/salobj/topics/controller_command.py b/python/lsst/ts/salobj/topics/controller_command.py index d0d4dbf27..6b6155b74 100644 --- a/python/lsst/ts/salobj/topics/controller_command.py +++ b/python/lsst/ts/salobj/topics/controller_command.py @@ -85,9 +85,7 @@ class ControllerCommand(read_topic.ReadTopic): then do the same as `ExpectedError` and also log a traceback. """ - def __init__( - self, salinfo: SalInfo, name: str, queue_len: int = read_topic.DEFAULT_QUEUE_LEN - ) -> None: + def __init__(self, salinfo: SalInfo, name: str, queue_len: int = read_topic.DEFAULT_QUEUE_LEN) -> None: super().__init__( salinfo=salinfo, attr_name="cmd_" + name, @@ -98,9 +96,7 @@ def __init__( if salinfo._ackcmd_writer is None: self.salinfo._ackcmd_writer = AckCmdWriter(salinfo=salinfo) - async def ack( - self, data: type_hints.BaseMsgType, ackcmd: type_hints.AckCmdDataType - ) -> None: + async def ack(self, data: type_hints.BaseMsgType, ackcmd: type_hints.AckCmdDataType) -> None: """Acknowledge a command by writing a new state. Parameters @@ -130,9 +126,7 @@ async def ack( timeout=ackcmd.timeout, ) - async def ack_in_progress( - self, data: type_hints.BaseMsgType, timeout: float, result: str = "" - ) -> None: + async def ack_in_progress(self, data: type_hints.BaseMsgType, timeout: float, result: str = "") -> None: """Ackowledge this command as "in progress". Parameters @@ -192,9 +186,7 @@ async def next( # type: ignore[override] # noqa """ return await super().next(flush=False, timeout=timeout) - async def _ack_if_running( - self, data: type_hints.BaseMsgType, ackcmd: type_hints.AckCmdDataType - ) -> None: + async def _ack_if_running(self, data: type_hints.BaseMsgType, ackcmd: type_hints.AckCmdDataType) -> None: """Wrapper around self.ack that logs a warning if not salinfo.running. This allows methods of this class to safely acknowledge commands, diff --git a/python/lsst/ts/salobj/topics/read_topic.py b/python/lsst/ts/salobj/topics/read_topic.py index 99b7df663..0be6a0609 100644 --- a/python/lsst/ts/salobj/topics/read_topic.py +++ b/python/lsst/ts/salobj/topics/read_topic.py @@ -118,9 +118,7 @@ class QueueCapacityChecker: def __init__(self, descr: str, log: logging.Logger, queue_len: int) -> None: if queue_len < MIN_QUEUE_LEN: - raise ValueError( - f"queue_len {queue_len} must be >= MIN_QUEUE_LEN={MIN_QUEUE_LEN}" - ) + raise ValueError(f"queue_len {queue_len} must be >= MIN_QUEUE_LEN={MIN_QUEUE_LEN}") self.descr = descr self.log = log self.queue_len = queue_len @@ -132,9 +130,7 @@ def __init__(self, descr: str, log: logging.Logger, queue_len: int) -> None: if queue_len >= 20: warn_thresholds.append(queue_len // 2) self.warn_thresholds = tuple(sorted(warn_thresholds)) - self._reset_thresholds = tuple( - warn_thresh // 2 for warn_thresh in self.warn_thresholds - ) + self._reset_thresholds = tuple(warn_thresh // 2 for warn_thresh in self.warn_thresholds) self.warn_threshold: int | None = self.warn_thresholds[0] self.reset_threshold: int | None = None @@ -158,16 +154,12 @@ def check_nitems(self, nitems: int) -> bool: if nitems >= self.queue_len: self.warn_threshold = None self.reset_threshold = self._reset_thresholds[-1] - self.log.error( - f"{self.descr} is full ({self.queue_len} elements); data may be lost" - ) + self.log.error(f"{self.descr} is full ({self.queue_len} elements); data may be lost") else: index = bisect.bisect_right(self.warn_thresholds, nitems) self.warn_threshold = self.warn_thresholds[index] self.reset_threshold = self._reset_thresholds[index - 1] - self.log.warning( - f"{self.descr} is filling: {nitems} of {self.queue_len} elements" - ) + self.log.warning(f"{self.descr} is filling: {nitems} of {self.queue_len} elements") return True elif self.reset_threshold is not None and nitems <= self.reset_threshold: # Reset to lower warning and reset thresholds @@ -272,21 +264,14 @@ def __init__( raise ValueError(f"max_history={max_history} must be >= 0") if salinfo.indexed and salinfo.index == 0 and max_history > 1: raise ValueError( - f"max_history={max_history} must be 0 or 1 " - "for an indexed component read with index=0" + f"max_history={max_history} must be 0 or 1 for an indexed component read with index=0" ) if queue_len <= MIN_QUEUE_LEN: - raise ValueError( - f"queue_len={queue_len} must be >= MIN_QUEUE_LEN={MIN_QUEUE_LEN}" - ) + raise ValueError(f"queue_len={queue_len} must be >= MIN_QUEUE_LEN={MIN_QUEUE_LEN}") if max_history > queue_len: - raise ValueError( - f"max_history={max_history} must be <= queue_len={queue_len}" - ) + raise ValueError(f"max_history={max_history} must be <= queue_len={queue_len}") self._max_history = int(max_history) - self._data_queue: collections.deque[type_hints.BaseMsgType] = collections.deque( - maxlen=queue_len - ) + self._data_queue: collections.deque[type_hints.BaseMsgType] = collections.deque(maxlen=queue_len) self._current_data: type_hints.BaseMsgType | None = None # Task that `next` waits on. # Its result is set to the oldest message on the queue. @@ -357,9 +342,7 @@ def callback(self, func: CallbackType | None) -> None: if func is not None: if not callable(func): raise TypeError(f"callback {func} not callable") - if not inspect.iscoroutinefunction( - func - ) and not asyncio.iscoroutinefunction( + if not inspect.iscoroutinefunction(func) and not asyncio.iscoroutinefunction( func.__call__ # type: ignore ): # TODO DM-37502: modify this to raise (and update doc string) @@ -542,9 +525,7 @@ def get_oldest(self) -> type_hints.BaseMsgType | None: return self._data_queue.popleft() return None - async def next( - self, *, flush: bool, timeout: float | None = None - ) -> type_hints.BaseMsgType: + async def next(self, *, flush: bool, timeout: float | None = None) -> type_hints.BaseMsgType: """Pop and return the oldest message from the queue, waiting for data if the queue is empty. @@ -607,9 +588,7 @@ async def _callback_loop(self) -> None: result = self._run_callback(data) if self.allow_multiple_callbacks: # Purge done callback tasks and add a new one. - self._callback_tasks = { - task for task in self._callback_tasks if not task.done() - } + self._callback_tasks = {task for task in self._callback_tasks if not task.done()} self._callback_tasks.add(asyncio.create_task(result)) else: await result diff --git a/python/lsst/ts/salobj/topics/remote_command.py b/python/lsst/ts/salobj/topics/remote_command.py index 91e3919a2..8904c562d 100644 --- a/python/lsst/ts/salobj/topics/remote_command.py +++ b/python/lsst/ts/salobj/topics/remote_command.py @@ -57,12 +57,8 @@ class AckCmdReader(read_topic.ReadTopic): SAL component. """ - def __init__( - self, salinfo: SalInfo, queue_len: int = read_topic.DEFAULT_QUEUE_LEN - ) -> None: - super().__init__( - salinfo=salinfo, attr_name="ack_ackcmd", max_history=0, queue_len=queue_len - ) + def __init__(self, salinfo: SalInfo, queue_len: int = read_topic.DEFAULT_QUEUE_LEN) -> None: + super().__init__(salinfo=salinfo, attr_name="ack_ackcmd", max_history=0, queue_len=queue_len) def _queue_one_item(self, data: type_hints.BaseMsgType) -> None: """Queue an ackcmd message if its ``identity`` and ``origin`` match @@ -138,9 +134,7 @@ def __init__( # we should see at most 3 acks, but leave room for one more, # just in case - self._ack_queue: collections.deque[type_hints.AckCmdDataType] = ( - collections.deque(maxlen=4) - ) + self._ack_queue: collections.deque[type_hints.AckCmdDataType] = collections.deque(maxlen=4) self._last_ackcmd: type_hints.AckCmdDataType | None = None def add_ackcmd(self, ackcmd: type_hints.AckCmdDataType) -> bool: @@ -165,9 +159,7 @@ def close(self) -> None: """Stop pending tasks.""" self._wait_task.cancel() - async def next_ackcmd( - self, timeout: float = DEFAULT_TIMEOUT - ) -> type_hints.AckCmdDataType: + async def next_ackcmd(self, timeout: float = DEFAULT_TIMEOUT) -> type_hints.AckCmdDataType: """Get next command acknowledgement of interest. If ``wait_done`` true then return the final command acknowledgement, @@ -200,9 +192,7 @@ async def next_ackcmd( If the command acknowledgement does not arrive in time. """ try: - self._wait_task = asyncio.create_task( - self._basic_next_ackcmd(timeout=timeout) - ) + self._wait_task = asyncio.create_task(self._basic_next_ackcmd(timeout=timeout)) ackcmd = await self._wait_task if ackcmd.ack in self.failed_ack_codes: raise base.AckError(msg="Command failed", ackcmd=ackcmd) @@ -228,9 +218,7 @@ async def _basic_next_ackcmd(self, timeout: float) -> type_hints.AckCmdDataType: t0 = time.monotonic() elapsed_time: float = 0 while True: - ackcmd = await asyncio.wait_for( - self._get_next_ackcmd(), timeout=timeout - elapsed_time - ) + ackcmd = await asyncio.wait_for(self._get_next_ackcmd(), timeout=timeout - elapsed_time) if not self.wait_done or ackcmd.ack in self.done_ack_codes: return ackcmd if ackcmd.ack == sal_enums.SalRetCode.CMD_INPROGRESS: @@ -340,9 +328,7 @@ async def next_ackcmd( """ cmd_info = self.salinfo._running_cmds.get(ackcmd.private_seqNum, None) if cmd_info is None: - raise RuntimeError( - f"Command private_seqNum={ackcmd.private_seqNum} is unknown or finished" - ) + raise RuntimeError(f"Command private_seqNum={ackcmd.private_seqNum} is unknown or finished") cmd_info.wait_done = wait_done return await cmd_info.next_ackcmd(timeout=timeout) @@ -492,13 +478,9 @@ async def start( f"{self.attr_name} a command with seq_num={seq_num} is already running. " "This may indicate a bug in ts_salobj SalInfo or RemoteCommand." ) - cmd_info = CommandInfo( - remote_command=self, seq_num=seq_num, wait_done=wait_done - ) + cmd_info = CommandInfo(remote_command=self, seq_num=seq_num, wait_done=wait_done) self.salinfo._running_cmds[seq_num] = cmd_info - await self.salinfo.write_data( - topic_info=self.topic_info, data_dict=vars(data) - ) + await self.salinfo.write_data(topic_info=self.topic_info, data_dict=vars(data)) finally: self._in_start = False diff --git a/python/lsst/ts/salobj/topics/write_topic.py b/python/lsst/ts/salobj/topics/write_topic.py index 08f7a8810..f75fe5d64 100644 --- a/python/lsst/ts/salobj/topics/write_topic.py +++ b/python/lsst/ts/salobj/topics/write_topic.py @@ -29,6 +29,7 @@ from collections.abc import Generator import numpy as np + from lsst.ts import utils from lsst.ts.xml import type_hints @@ -237,9 +238,7 @@ def set(self, **kwargs: typing.Any) -> bool: data_dict = vars(self.data) unknown_fields = kwargs.keys() - data_dict.keys() if unknown_fields: - raise AttributeError( - f"{self.attr_name} has no fields {sorted(unknown_fields)}" - ) + raise AttributeError(f"{self.attr_name} has no fields {sorted(unknown_fields)}") for field_name, value in kwargs.items(): if value is None: @@ -260,18 +259,14 @@ def set(self, **kwargs: typing.Any) -> bool: equal_nan=is_float, # type: ignore ) except Exception as e: - raise TypeError( - f"Cannot set {self.attr_name}.{field_name}={value!r}; wrong type." - ) from e + raise TypeError(f"Cannot set {self.attr_name}.{field_name}={value!r}; wrong type.") from e data_dict[field_name] = value # Check the data by creating a DataType, because no checking is done # when directly setting attributes of a dataclass. self.data = self.DataType(**data_dict) return did_change - async def set_write( - self, *, force_output: bool | None = None, **kwargs: typing.Any - ) -> SetWriteResult: + async def set_write(self, *, force_output: bool | None = None, **kwargs: typing.Any) -> SetWriteResult: """Set zero or more fields of ``self.data`` and write if any field changed or if output forced. @@ -304,9 +299,7 @@ async def set_write( if did_change: do_output = True else: - do_output = ( - self.default_force_output if force_output is None else force_output - ) + do_output = self.default_force_output if force_output is None else force_output if do_output: data = await self.write() else: diff --git a/python/lsst/ts/salobj/type_hints.py b/python/lsst/ts/salobj/type_hints.py index d6a2a4665..d79b3ce23 100644 --- a/python/lsst/ts/salobj/type_hints.py +++ b/python/lsst/ts/salobj/type_hints.py @@ -1,4 +1,6 @@ -from lsst.ts.xml.type_hints import AckCmdDataType # noqa: F401 F403 -from lsst.ts.xml.type_hints import BaseDdsDataType # noqa: F401 F403 -from lsst.ts.xml.type_hints import BaseMsgType # noqa: F401 F403 -from lsst.ts.xml.type_hints import PathType # noqa: F401 F403 +from lsst.ts.xml.type_hints import ( + AckCmdDataType, # noqa: F401 F403 + BaseDdsDataType, # noqa: F401 F403 + BaseMsgType, # noqa: F401 F403 + PathType, # noqa: F401 F403 +) diff --git a/python/lsst/ts/salobj/validator.py b/python/lsst/ts/salobj/validator.py index 4195adbdf..753041093 100644 --- a/python/lsst/ts/salobj/validator.py +++ b/python/lsst/ts/salobj/validator.py @@ -151,15 +151,11 @@ def set_defaults( for error in validate_properties(validator, properties, instance, schema): yield error - WrappedValidator = jsonschema.validators.extend( - StandardValidatorClass, {"properties": set_defaults} - ) + WrappedValidator = jsonschema.validators.extend(StandardValidatorClass, {"properties": set_defaults}) WrappedValidator.check_schema(schema) self.defaults_validator = WrappedValidator(schema=schema) - def validate( - self, data_dict: dict[str, typing.Any] | None - ) -> dict[str, typing.Any]: + def validate(self, data_dict: dict[str, typing.Any] | None) -> dict[str, typing.Any]: """Validate data. Set missing values based on defaults in the schema, diff --git a/tests/test_async_s3_bucket.py b/tests/test_async_s3_bucket.py index 2e3ac138c..babc9778f 100644 --- a/tests/test_async_s3_bucket.py +++ b/tests/test_async_s3_bucket.py @@ -25,6 +25,7 @@ import astropy.time import moto import pytest + from lsst.ts import salobj, utils @@ -97,12 +98,8 @@ def upload_callback(nbytes: int) -> None: def download_callback(nbytes: int) -> None: downloaded_nbytes.append(nbytes) - await self.bucket.upload( - fileobj=self.fileobj, key=self.key, callback=upload_callback - ) - roundtrip_fileobj = await self.bucket.download( - key=self.key, callback=download_callback - ) + await self.bucket.upload(fileobj=self.fileobj, key=self.key, callback=upload_callback) + roundtrip_fileobj = await self.bucket.download(key=self.key, callback=download_callback) roundtrip_data = roundtrip_fileobj.getbuffer() assert self.file_data == roundtrip_data assert len(uploaded_nbytes) >= 1 @@ -163,8 +160,7 @@ async def test_make_key(self) -> None: salname=salname, salindexname=salindexname, generator=generator, date=date ) expected_key = ( - "Foo:Blue/testFiberSpecBlue/2020/04/01/" - "Foo:Blue_testFiberSpecBlue_2020-04-02T11:59:59.999.dat" + "Foo:Blue/testFiberSpecBlue/2020/04/01/Foo:Blue_testFiberSpecBlue_2020-04-02T11:59:59.999.dat" ) assert key == expected_key @@ -174,8 +170,7 @@ async def test_make_key(self) -> None: salname=salname, salindexname=salindexname, generator=generator, date=date ) expected_key = ( - "Foo:Blue/testFiberSpecBlue/2020/04/02/" - "Foo:Blue_testFiberSpecBlue_2020-04-02T12:00:00.000.dat" + "Foo:Blue/testFiberSpecBlue/2020/04/02/Foo:Blue_testFiberSpecBlue_2020-04-02T12:00:00.000.dat" ) assert key == expected_key @@ -183,19 +178,13 @@ async def test_make_key(self) -> None: key = salobj.AsyncS3Bucket.make_key( salname=salname, salindexname=None, generator=generator, date=date ) - expected_key = ( - "Foo/testFiberSpecBlue/2020/04/02/" - "Foo_testFiberSpecBlue_2020-04-02T12:00:00.000.dat" - ) + expected_key = "Foo/testFiberSpecBlue/2020/04/02/Foo_testFiberSpecBlue_2020-04-02T12:00:00.000.dat" assert key == expected_key # Repeat the test with an integer sal index name - key = salobj.AsyncS3Bucket.make_key( - salname=salname, salindexname=5, generator=generator, date=date - ) + key = salobj.AsyncS3Bucket.make_key(salname=salname, salindexname=5, generator=generator, date=date) expected_key = ( - "Foo:5/testFiberSpecBlue/2020/04/02/" - "Foo:5_testFiberSpecBlue_2020-04-02T12:00:00.000.dat" + "Foo:5/testFiberSpecBlue/2020/04/02/Foo:5_testFiberSpecBlue_2020-04-02T12:00:00.000.dat" ) assert key == expected_key @@ -207,10 +196,7 @@ async def test_make_key(self) -> None: date=date, other="othertext", ) - expected_key = ( - "Foo:5/testFiberSpecBlue/2020/04/02/" - "Foo:5_testFiberSpecBlue_othertext.dat" - ) + expected_key = "Foo:5/testFiberSpecBlue/2020/04/02/Foo:5_testFiberSpecBlue_othertext.dat" assert key == expected_key # Repeat the test with a specified value for "suffix" @@ -222,8 +208,7 @@ async def test_make_key(self) -> None: suffix="suffixtext", ) expected_key = ( - "Foo:5/testFiberSpecBlue/2020/04/02/" - "Foo:5_testFiberSpecBlue_2020-04-02T12:00:00.000suffixtext" + "Foo:5/testFiberSpecBlue/2020/04/02/Foo:5_testFiberSpecBlue_2020-04-02T12:00:00.000suffixtext" ) assert key == expected_key diff --git a/tests/test_base_script.py b/tests/test_base_script.py index 3fa62c2aa..5e1d76c46 100644 --- a/tests/test_base_script.py +++ b/tests/test_base_script.py @@ -31,6 +31,7 @@ import pytest import yaml + from lsst.ts import salobj, utils from lsst.ts.xml.enums.Script import ScriptState from lsst.ts.xml.type_hints import BaseMsgType @@ -378,10 +379,7 @@ async def test_pause(self) -> None: await asyncio.wait_for(script.done_task, timeout=STD_TIMEOUT) assert script.state.lastCheckpoint == end_checkpoint assert script.state.numCheckpoints == 2 - duration = ( - script.timestamps[ScriptState.ENDING] - - script.timestamps[ScriptState.RUNNING] - ) + duration = script.timestamps[ScriptState.ENDING] - script.timestamps[ScriptState.RUNNING] desired_duration = wait_time print(f"test_pause duration={duration:0.2f}") assert abs(duration - desired_duration) < 0.2 @@ -405,10 +403,7 @@ async def test_stop_at_checkpoint(self) -> None: assert script.state.lastCheckpoint == end_checkpoint assert script.state.numCheckpoints == 2 assert script.state.state == ScriptState.STOPPED - duration = ( - script.timestamps[ScriptState.STOPPING] - - script.timestamps[ScriptState.RUNNING] - ) + duration = script.timestamps[ScriptState.STOPPING] - script.timestamps[ScriptState.RUNNING] # waited and then stopped at the "end" checkpoint desired_duration = wait_time print(f"test_stop_at_checkpoint duration={duration:0.2f}") @@ -438,10 +433,7 @@ async def test_stop_while_paused(self) -> None: assert script.state.lastCheckpoint == start_checkpoint assert script.state.numCheckpoints == 1 assert script.state.state == ScriptState.STOPPED - duration = ( - script.timestamps[ScriptState.STOPPING] - - script.timestamps[ScriptState.RUNNING] - ) + duration = script.timestamps[ScriptState.STOPPING] - script.timestamps[ScriptState.RUNNING] # the script ran quickly because we stopped the script # just as soon as it paused at the "start" checkpoint desired_duration = 0 @@ -468,10 +460,7 @@ async def test_stop_while_running(self) -> None: assert script.state.lastCheckpoint == start_checkpoint assert script.state.numCheckpoints == 1 assert script.state.state == ScriptState.STOPPED - duration = ( - script.timestamps[ScriptState.STOPPING] - - script.timestamps[ScriptState.RUNNING] - ) + duration = script.timestamps[ScriptState.STOPPING] - script.timestamps[ScriptState.RUNNING] # we waited `pause_time` seconds after the "start" checkpoint desired_duration = pause_time print(f"test_stop_while_running duration={duration:0.2f}") @@ -507,10 +496,7 @@ async def check_fail(self, fail_run: bool) -> None: assert script.state.lastCheckpoint == "end" assert script.state.numCheckpoints == 2 end_run_state = ScriptState.ENDING - duration = ( - script.timestamps[end_run_state] - - script.timestamps[ScriptState.RUNNING] - ) + duration = script.timestamps[end_run_state] - script.timestamps[ScriptState.RUNNING] # if fail_run then failed before waiting, # otherwise failed after desired_duration = 0 if fail_run else wait_time @@ -542,9 +528,7 @@ def __init__(self, index: int, remote_indices: Iterable[int]) -> None: # use remotes that read history here, to check that # script.start_task waits for the start_task in each remote. for rind in remote_indices: - remotes.append( - salobj.Remote(domain=self.domain, name="Test", index=rind) - ) + remotes.append(salobj.Remote(domain=self.domain, name="Test", index=rind)) self.remotes = remotes remote_indices = [5, 7] @@ -576,57 +560,39 @@ async def logcallback(data: BaseMsgType) -> None: remote.evt_logMessage.callback = logcallback - process = await asyncio.create_subprocess_exec( - str(script_path), str(index) - ) + process = await asyncio.create_subprocess_exec(str(script_path), str(index)) try: assert process.returncode is None - descr = await remote.evt_description.next( - flush=False, timeout=STD_TIMEOUT - ) + descr = await remote.evt_description.next(flush=False, timeout=STD_TIMEOUT) assert descr.classname == "TestScript" assert descr.description == "test script" assert "test script that waits" in descr.help assert descr.remotes == "" - state = await remote.evt_state.next( - flush=False, timeout=STD_TIMEOUT - ) + state = await remote.evt_state.next(flush=False, timeout=STD_TIMEOUT) assert state.state == ScriptState.UNCONFIGURED assert state.groupId == "" - logLevel_data = await remote.evt_logLevel.next( - flush=False, timeout=STD_TIMEOUT - ) + logLevel_data = await remote.evt_logLevel.next(flush=False, timeout=STD_TIMEOUT) assert logLevel_data.level == logging.INFO wait_time = 0.1 config = f"wait_time: {wait_time}" if fail: config = config + f"\n{fail}: True" - await remote.cmd_configure.set_start( - config=config, timeout=STD_TIMEOUT - ) - state = await remote.evt_state.next( - flush=False, timeout=STD_TIMEOUT - ) + await remote.cmd_configure.set_start(config=config, timeout=STD_TIMEOUT) + state = await remote.evt_state.next(flush=False, timeout=STD_TIMEOUT) assert state.state == ScriptState.CONFIGURED assert state.groupId == "" - metadata = await remote.evt_metadata.next( - flush=False, timeout=STD_TIMEOUT - ) + metadata = await remote.evt_metadata.next(flush=False, timeout=STD_TIMEOUT) assert metadata.duration == wait_time assert metadata.totalCheckpoints == 2 group_id = "a non-blank group ID" - await remote.cmd_setGroupId.set_start( - groupId=group_id, timeout=STD_TIMEOUT - ) - state = await remote.evt_state.next( - flush=False, timeout=STD_TIMEOUT - ) + await remote.cmd_setGroupId.set_start(groupId=group_id, timeout=STD_TIMEOUT) + state = await remote.evt_state.next(flush=False, timeout=STD_TIMEOUT) assert state.groupId == group_id await remote.cmd_run.start(timeout=STD_TIMEOUT) @@ -665,9 +631,7 @@ async def test_script_schema_process(self) -> None: stderr=subprocess.PIPE, ) try: - stdout, stderr = await asyncio.wait_for( - process.communicate(), timeout=STD_TIMEOUT - ) + stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=STD_TIMEOUT) schema = yaml.safe_load(stdout) assert schema == salobj.TestScript.get_schema() await asyncio.wait_for(process.wait(), timeout=STD_TIMEOUT) @@ -675,6 +639,4 @@ async def test_script_schema_process(self) -> None: finally: if process.returncode is None: process.terminate() - warnings.warn( - "Killed a process that was not properly terminated", RuntimeWarning - ) + warnings.warn("Killed a process that was not properly terminated", RuntimeWarning) diff --git a/tests/test_basics.py b/tests/test_basics.py index 7003ceaef..616b5a8b3 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -26,14 +26,13 @@ import unittest import pytest + from lsst.ts import salobj, utils index_gen = utils.index_generator() -class TestBaseCscTestCaseIsolation( - salobj.BaseCscTestCase, unittest.IsolatedAsyncioTestCase -): +class TestBaseCscTestCaseIsolation(salobj.BaseCscTestCase, unittest.IsolatedAsyncioTestCase): def basic_make_csc( self, initial_state: salobj.State | int, @@ -67,9 +66,7 @@ def setUp(self) -> None: async def test_assert_raises_ack_error(self) -> None: """Test the assertRaisesAckError function.""" index = next(index_gen) - async with salobj.Domain() as domain, salobj.SalInfo( - domain, "Test", index=index - ) as salinfo: + async with salobj.Domain() as domain, salobj.SalInfo(domain, "Test", index=index) as salinfo: private_seqNum = 5 ack = 23 error = -6 @@ -94,9 +91,7 @@ async def test_assert_raises_ack_error(self) -> None: ): with pytest.raises(ExceptionClass): with salobj.assertRaisesAckError(): - raise ExceptionClass( - "assertRaisesAckError should ignore other exception types" - ) + raise ExceptionClass("assertRaisesAckError should ignore other exception types") with pytest.raises(AssertionError): with salobj.assertRaisesAckError(ack=5): @@ -123,27 +118,19 @@ async def test_assert_raises_ack_error(self) -> None: with salobj.assertRaisesAckError(ack=1, error=2, result_contains=result): raise salobj.AckError( "match ack, error and full result", - ackcmd=salinfo.make_ackcmd( - private_seqNum=4, ack=1, error=2, result=result - ), + ackcmd=salinfo.make_ackcmd(private_seqNum=4, ack=1, error=2, result=result), ) # test result_contains with a substring of the result string - with salobj.assertRaisesAckError( - ack=1, error=2, result_contains=result[2:-2] - ): + with salobj.assertRaisesAckError(ack=1, error=2, result_contains=result[2:-2]): raise salobj.AckError( "match ack, error and a substring of result", - ackcmd=salinfo.make_ackcmd( - private_seqNum=4, ack=1, error=2, result=result - ), + ackcmd=salinfo.make_ackcmd(private_seqNum=4, ack=1, error=2, result=result), ) async def test_ack_error_repr(self) -> None: """Test AckError.__str__ and AckError.__repr__""" index = next(index_gen) - async with salobj.Domain() as domain, salobj.SalInfo( - domain, "Test", index=index - ) as salinfo: + async with salobj.Domain() as domain, salobj.SalInfo(domain, "Test", index=index) as salinfo: msg = "a message" private_seqNum = 5 ack = 23 diff --git a/tests/test_config_files.py b/tests/test_config_files.py index 2a9bcad92..b0af2eeb0 100644 --- a/tests/test_config_files.py +++ b/tests/test_config_files.py @@ -24,6 +24,7 @@ import unittest import pytest + from lsst.ts import salobj @@ -36,16 +37,12 @@ def test_get_schema(self) -> None: schema = self.get_schema(csc_package_root=csc_package_root, sal_name="Test") assert isinstance(schema, dict) - schema2 = self.get_schema( - csc_package_root=csc_package_root, schema_subpath="schema/Test.yaml" - ) + schema2 = self.get_schema(csc_package_root=csc_package_root, schema_subpath="schema/Test.yaml") assert schema == schema2 with pytest.raises(AssertionError): # Invalid sal_name - self.get_schema( - csc_package_root=csc_package_root, sal_name="NoSuchSalComponent" - ) + self.get_schema(csc_package_root=csc_package_root, sal_name="NoSuchSalComponent") with pytest.raises(AssertionError): # Invalid schema_subpath self.get_schema( @@ -144,13 +141,9 @@ def test_local_configs(self) -> None: configs_root = pathlib.Path(__file__).parent / "data" / "configs" for config_dir in configs_root.glob("good_*"): - self.check_config_files( - config_dir=config_dir, schema=self.schema, exclude_glob="bad_*" - ) + self.check_config_files(config_dir=config_dir, schema=self.schema, exclude_glob="bad_*") for config_dir in configs_root.glob("bad_*"): with self.subTest(config_dir=str(config_dir)): with pytest.raises(AssertionError): - self.check_config_files( - config_dir=config_dir, schema=self.schema, exclude_glob="bad_*" - ) + self.check_config_files(config_dir=config_dir, schema=self.schema, exclude_glob="bad_*") diff --git a/tests/test_controller.py b/tests/test_controller.py index 5f261abe4..1f42e1906 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -25,6 +25,7 @@ import numpy as np import pytest + from lsst.ts import salobj, utils np.random.seed(47) @@ -52,9 +53,7 @@ class ControllerWithDoMethods(salobj.Controller): a subset of commands. """ - def __init__( - self, command_names: Iterable[str], allow_missing_callbacks: bool = False - ) -> None: + def __init__(self, command_names: Iterable[str], allow_missing_callbacks: bool = False) -> None: index = next(index_gen) for name in command_names: setattr(self, f"do_{name}", self.mock_do_method) @@ -86,9 +85,10 @@ async def test_do_callbacks_false(self) -> None: async def test_do_callbacks_true(self) -> None: index = next(index_gen) - async with salobj.Domain() as domain, salobj.SalInfo( - domain=domain, name="Test", index=index - ) as salinfo: + async with ( + salobj.Domain() as domain, + salobj.SalInfo(domain=domain, name="Test", index=index) as salinfo, + ): command_names = salinfo.command_names # Build a controller and check that callbacks are assigned. @@ -104,9 +104,7 @@ async def test_do_callbacks_true(self) -> None: if missing_name in skip_names: continue with self.subTest(missing_name=missing_name): - incomplete_names = [ - name for name in command_names if name != missing_name - ] + incomplete_names = [name for name in command_names if name != missing_name] # With allow_missing_callbacks=False (the default) # missing do_{command} methods should raise TypeError with pytest.raises(TypeError): @@ -118,16 +116,11 @@ async def test_do_callbacks_true(self) -> None: # should have callback controller._unsupported_cmd_callback. # Note that command callbacks are not assigned until # the CSC is started. - async with ControllerWithDoMethods( - incomplete_names, allow_missing_callbacks=True - ) as controller: + async with ControllerWithDoMethods(incomplete_names, allow_missing_callbacks=True) as controller: for name in command_names: command_topic = getattr(controller, f"cmd_{name}") if name == missing_name: - assert ( - command_topic.callback - == controller._unsupported_cmd_callback - ) + assert command_topic.callback == controller._unsupported_cmd_callback else: do_method = getattr(controller, f"do_{name}") assert command_topic.callback == do_method @@ -140,9 +133,7 @@ async def test_do_callbacks_true(self) -> None: async def test_write_only_true(self) -> None: index = next(index_gen) # Build a controller and check that callbacks are assigned. - async with salobj.Controller( - name="Test", index=index, write_only=True - ) as controller: + async with salobj.Controller(name="Test", index=index, write_only=True) as controller: for name in controller.salinfo.command_names: assert not hasattr(controller, f"cmd_{name}") @@ -154,6 +145,4 @@ async def test_write_only_true(self) -> None: # Check that do_callbacks cannot be true if write_only is true. with pytest.raises(ValueError): - salobj.Controller( - name="Test", index=index, do_callbacks=True, write_only=True - ) + salobj.Controller(name="Test", index=index, do_callbacks=True, write_only=True) diff --git a/tests/test_controller_logging.py b/tests/test_controller_logging.py index c7b436092..75d067aa1 100644 --- a/tests/test_controller_logging.py +++ b/tests/test_controller_logging.py @@ -28,6 +28,7 @@ import numpy as np import pytest + from lsst.ts import salobj, utils from lsst.ts.xml.type_hints import BaseMsgType @@ -55,9 +56,7 @@ async def do_wait(self, data: BaseMsgType) -> None: raise RuntimeError(self.exc_msg) -class ControllerLoggingTestCase( - salobj.BaseCscTestCase, unittest.IsolatedAsyncioTestCase -): +class ControllerLoggingTestCase(salobj.BaseCscTestCase, unittest.IsolatedAsyncioTestCase): def basic_make_csc( self, initial_state: salobj.State | int, @@ -72,21 +71,15 @@ def basic_make_csc( ) async def test_logging(self) -> None: - async with self.make_csc( - initial_state=salobj.State.ENABLED, config_dir=TEST_CONFIG_DIR - ): - logLevel = await self.remote.evt_logLevel.next( - flush=False, timeout=STD_TIMEOUT - ) + async with self.make_csc(initial_state=salobj.State.ENABLED, config_dir=TEST_CONFIG_DIR): + logLevel = await self.remote.evt_logLevel.next(flush=False, timeout=STD_TIMEOUT) assert logLevel.level == logging.INFO info_message = "test info message" self.csc.log.info(info_message) # Skip initial messages until we find this new one. while True: - msg = await self.remote.evt_logMessage.next( - flush=False, timeout=STD_TIMEOUT - ) + msg = await self.remote.evt_logMessage.next(flush=False, timeout=STD_TIMEOUT) if msg.message == info_message: break assert msg.level == logging.INFO @@ -95,15 +88,11 @@ async def test_logging(self) -> None: info_message = "message from background thread" loop = asyncio.get_running_loop() await loop.run_in_executor(None, self.csc.log.info, info_message) - await self.assert_next_sample( - topic=self.remote.evt_logMessage, message=info_message - ) + await self.assert_next_sample(topic=self.remote.evt_logMessage, message=info_message) filepath = pathlib.Path(__file__) subpath = "/".join(filepath.parts[-2:]) - assert msg.filePath.endswith( - subpath - ), f"{msg.filePath} does not end with {subpath!r}" + assert msg.filePath.endswith(subpath), f"{msg.filePath} does not end with {subpath!r}" assert msg.functionName == "test_logging" assert msg.lineNumber > 0 assert msg.process == os.getpid() @@ -112,50 +101,36 @@ async def test_logging(self) -> None: encodable_message = "test warn message" warn_message = encodable_message + "\u2013" self.csc.log.warning(warn_message) - msg = await self.remote.evt_logMessage.next( - flush=False, timeout=STD_TIMEOUT - ) + msg = await self.remote.evt_logMessage.next(flush=False, timeout=STD_TIMEOUT) encodable_len = len(encodable_message) assert msg.message[0:encodable_len] == encodable_message assert msg.level == logging.WARNING assert msg.traceback == "" with pytest.raises(asyncio.TimeoutError): - await self.remote.evt_logMessage.next( - flush=False, timeout=NO_DATA_TIMEOUT - ) + await self.remote.evt_logMessage.next(flush=False, timeout=NO_DATA_TIMEOUT) self.remote.evt_logLevel.flush() - await self.remote.cmd_setLogLevel.set_start( - level=logging.ERROR, timeout=STD_TIMEOUT - ) + await self.remote.cmd_setLogLevel.set_start(level=logging.ERROR, timeout=STD_TIMEOUT) - logLevel = await self.remote.evt_logLevel.next( - flush=False, timeout=STD_TIMEOUT - ) + logLevel = await self.remote.evt_logLevel.next(flush=False, timeout=STD_TIMEOUT) assert logLevel.level == logging.ERROR info_message = "test info message" self.csc.log.info(info_message) with pytest.raises(asyncio.TimeoutError): - await self.remote.evt_logMessage.next( - flush=False, timeout=NO_DATA_TIMEOUT - ) + await self.remote.evt_logMessage.next(flush=False, timeout=NO_DATA_TIMEOUT) warn_message = "test warn message" self.csc.log.warning(warn_message) with pytest.raises(asyncio.TimeoutError): - await self.remote.evt_logMessage.next( - flush=False, timeout=NO_DATA_TIMEOUT - ) + await self.remote.evt_logMessage.next(flush=False, timeout=NO_DATA_TIMEOUT) with salobj.assertRaisesAckError(): await self.remote.cmd_wait.set_start(duration=5, timeout=STD_TIMEOUT) - msg = await self.remote.evt_logMessage.next( - flush=False, timeout=STD_TIMEOUT - ) + msg = await self.remote.evt_logMessage.next(flush=False, timeout=STD_TIMEOUT) assert self.csc.exc_msg in msg.traceback assert "Traceback" in msg.traceback assert "RuntimeError" in msg.traceback diff --git a/tests/test_csc_commander.py b/tests/test_csc_commander.py index 9f3ed8983..b8da9cdb5 100644 --- a/tests/test_csc_commander.py +++ b/tests/test_csc_commander.py @@ -25,6 +25,7 @@ import numpy as np import pytest + from lsst.ts import salobj, utils # Standard timeout (sec) @@ -80,9 +81,10 @@ def basic_make_csc( ) async def test_basics(self) -> None: - async with self.make_csc(initial_state=salobj.State.STANDBY), BasicCscCommander( - index=self.csc.salinfo.index - ) as commander: + async with ( + self.make_csc(initial_state=salobj.State.STANDBY), + BasicCscCommander(index=self.csc.salinfo.index) as commander, + ): commander.testing = True print("wait for summary state") await self.assert_next_summary_state(salobj.State.STANDBY) @@ -101,9 +103,7 @@ async def test_basics(self) -> None: t0 = utils.current_tai() wait_time = 2 # seconds print("run wait command") - await commander.run_command( - f"wait {salobj.SalRetCode.CMD_COMPLETE} {wait_time}" - ) + await commander.run_command(f"wait {salobj.SalRetCode.CMD_COMPLETE} {wait_time}") dt = utils.current_tai() - t0 # The margin of 0.2 compensates for the clock in Docker on macOS # not being strictly monotonic. @@ -176,9 +176,7 @@ async def test_synchronous_custom_command(self) -> None: # TODO DM-37502: modify this to expect construction to raise, # once we drop support for synchronous callback functions. with pytest.warns(DeprecationWarning): - async with SynchronousCustomCommandCscCommander( - index=self.csc.salinfo.index - ) as commander: + async with SynchronousCustomCommandCscCommander(index=self.csc.salinfo.index) as commander: commander.testing = True print("wait for summary state") # Test BasicCscCommander's "echo" command diff --git a/tests/test_csc_communication.py b/tests/test_csc_communication.py index 2296a8ccf..deb2e1c09 100644 --- a/tests/test_csc_communication.py +++ b/tests/test_csc_communication.py @@ -33,6 +33,7 @@ import numpy as np import pytest + from lsst.ts import salobj, utils # Long enough to perform any reasonable operation @@ -89,9 +90,7 @@ async def _report_summary_state(self) -> None: await super()._report_summary_state() if self.summary_state == salobj.State.FAULT: if self.doraise: - raise RuntimeError( - "Intentionally raise an exception when going to the FAULT state" - ) + raise RuntimeError("Intentionally raise an exception when going to the FAULT state") else: await self.fault( code=10934, @@ -153,16 +152,12 @@ async def test_duplicate_rejection(self) -> None: async with self.make_csc(initial_state=salobj.State.STANDBY): assert not self.csc.check_if_duplicate - duplicate_csc = salobj.TestCsc( - index=self.csc.salinfo.index, check_if_duplicate=True - ) + duplicate_csc = salobj.TestCsc(index=self.csc.salinfo.index, check_if_duplicate=True) try: # Change origin so heartbeat private_origin differs. duplicate_csc.salinfo.domain.origin += 1 assert duplicate_csc.check_if_duplicate - with pytest.raises( - salobj.ExpectedError, match="found another instance" - ): + with pytest.raises(salobj.ExpectedError, match="found another instance"): await asyncio.wait_for(duplicate_csc.done_task, timeout=STD_TIMEOUT) finally: await duplicate_csc.close() @@ -207,12 +202,15 @@ async def test_bin_script_duplicate(self) -> None: args = [exe_name, str(index), "--state", "standby"] - async with salobj.Domain() as domain, salobj.Remote( - domain=domain, - name="Test", - index=index, - include=["summaryState", "heartbeat"], - ) as self.remote: + async with ( + salobj.Domain() as domain, + salobj.Remote( + domain=domain, + name="Test", + index=index, + include=["summaryState", "heartbeat"], + ) as self.remote, + ): process1 = await asyncio.create_subprocess_exec( *args, stderr=subprocess.PIPE, @@ -235,9 +233,7 @@ async def test_bin_script_duplicate(self) -> None: assert process2.returncode > 0 assert process2.stderr is not None # make mypy happy try: - errbytes = await asyncio.wait_for( - process2.stderr.read(), timeout=STD_TIMEOUT - ) + errbytes = await asyncio.wait_for(process2.stderr.read(), timeout=STD_TIMEOUT) assert b"found another instance" in errbytes except asyncio.TimeoutError: raise AssertionError("timed out trying to read process2 stderr") @@ -252,9 +248,7 @@ async def test_bin_script_duplicate(self) -> None: print(std_out_bytes.decode()) if process2.stderr is not None: - errbytes = await asyncio.wait_for( - process2.stderr.read(), timeout=STD_TIMEOUT - ) + errbytes = await asyncio.wait_for(process2.stderr.read(), timeout=STD_TIMEOUT) print(errbytes.decode()) except asyncio.TimeoutError: print("Timeout waiting for process2 std out and/or std err") @@ -268,9 +262,7 @@ async def test_bin_script_duplicate(self) -> None: # CSC 1 quit early; try to print stderr, then fail. try: assert process1.stderr is not None # make mypy happy - errbytes = await asyncio.wait_for( - process1.stderr.read(), timeout=STD_TIMEOUT - ) + errbytes = await asyncio.wait_for(process1.stderr.read(), timeout=STD_TIMEOUT) print("Subprocess stderr: ", errbytes.decode()) except Exception as e: print(f"Could not read subprocess stderr: {e}") @@ -292,25 +284,19 @@ async def test_bin_script_version(self) -> None: stderr=subprocess.PIPE, ) try: - stdout, stderr = await asyncio.wait_for( - process.communicate(), timeout=STD_TIMEOUT - ) + stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=STD_TIMEOUT) assert stdout.decode()[:-1] == salobj.__version__ await asyncio.wait_for(process.wait(), timeout=STD_TIMEOUT) assert process.returncode == 0 finally: if process.returncode is None: process.terminate() - warnings.warn( - "Killed a process that was not properly terminated", RuntimeWarning - ) + warnings.warn("Killed a process that was not properly terminated", RuntimeWarning) async def test_log_level(self) -> None: """Test that specifying a log level to make_csc works.""" # If specified then log level is the value given. - async with self.make_csc( - initial_state=salobj.State.STANDBY, log_level=logging.DEBUG - ): + async with self.make_csc(initial_state=salobj.State.STANDBY, log_level=logging.DEBUG): assert self.csc.log.getEffectiveLevel() == logging.DEBUG # Check that the remote has the same log # (and hence the same effective log level). @@ -318,9 +304,7 @@ async def test_log_level(self) -> None: max_log_level = salobj.sal_info.MAX_LOG_LEVEL excessive_log_level = max_log_level + 5 - async with self.make_csc( - initial_state=salobj.State.STANDBY, log_level=excessive_log_level - ): + async with self.make_csc(initial_state=salobj.State.STANDBY, log_level=excessive_log_level): assert self.csc.log.getEffectiveLevel() == excessive_log_level # At this point log level is WARNING; now check that by default @@ -341,19 +325,13 @@ async def test_setArrays_command(self) -> None: # send the setArrays command with random data arrays_dict = self.csc.make_random_arrays_dict() - await self.remote.cmd_setArrays.set_start( - **arrays_dict, timeout=STD_TIMEOUT - ) + await self.remote.cmd_setArrays.set_start(**arrays_dict, timeout=STD_TIMEOUT) cmd_data_sent = self.remote.cmd_setArrays.data # see if new data was broadcast correctly - evt_data = await self.remote.evt_arrays.next( - flush=False, timeout=STD_TIMEOUT - ) + evt_data = await self.remote.evt_arrays.next(flush=False, timeout=STD_TIMEOUT) self.csc.assert_arrays_equal(cmd_data_sent, evt_data) - tel_data = await self.remote.tel_arrays.next( - flush=False, timeout=STD_TIMEOUT - ) + tel_data = await self.remote.tel_arrays.next(flush=False, timeout=STD_TIMEOUT) self.csc.assert_arrays_equal(cmd_data_sent, tel_data) assert self.csc.evt_arrays.has_data @@ -378,19 +356,13 @@ async def test_setScalars_command(self) -> None: # send the setScalars command with random data scalars_dict = self.csc.make_random_scalars_dict() - await self.remote.cmd_setScalars.set_start( - **scalars_dict, timeout=STD_TIMEOUT - ) + await self.remote.cmd_setScalars.set_start(**scalars_dict, timeout=STD_TIMEOUT) cmd_data_sent = self.remote.cmd_setScalars.data # see if new data is being broadcast correctly - evt_data = await self.remote.evt_scalars.next( - flush=False, timeout=STD_TIMEOUT - ) + evt_data = await self.remote.evt_scalars.next(flush=False, timeout=STD_TIMEOUT) self.csc.assert_scalars_equal(cmd_data_sent, evt_data) - tel_data = await self.remote.tel_scalars.next( - flush=False, timeout=STD_TIMEOUT - ) + tel_data = await self.remote.tel_scalars.next(flush=False, timeout=STD_TIMEOUT) self.csc.assert_scalars_equal(cmd_data_sent, tel_data) assert self.csc.evt_scalars.has_data @@ -426,9 +398,7 @@ async def test_fault_state_transitions(self) -> None: # and check the state and error code. await self.remote.cmd_fault.start(timeout=STD_TIMEOUT) await self.assert_next_summary_state(salobj.State.FAULT) - await self.assert_next_sample( - topic=self.remote.evt_errorCode, errorCode=1 - ) + await self.assert_next_sample(topic=self.remote.evt_errorCode, errorCode=1) # Issue the ``standby`` command to recover. await self.remote.cmd_standby.start(timeout=STD_TIMEOUT) @@ -441,9 +411,7 @@ async def test_fault_method(self) -> None: """Test BaseCsc.fault with and without optional arguments.""" async with self.make_csc(initial_state=salobj.State.STANDBY): await self.assert_next_summary_state(salobj.State.STANDBY) - await self.assert_next_sample( - topic=self.remote.evt_errorCode, errorCode=0, errorReport="" - ) + await self.assert_next_sample(topic=self.remote.evt_errorCode, errorCode=0, errorReport="") code = 52 report = "Report for error code" @@ -454,14 +422,10 @@ async def test_fault_method(self) -> None: await self.csc.fault(code="not a valid code", report=report) await self.assert_next_summary_state(salobj.State.FAULT) with pytest.raises(asyncio.TimeoutError): - await self.remote.evt_errorCode.next( - flush=False, timeout=NO_DATA_TIMEOUT - ) + await self.remote.evt_errorCode.next(flush=False, timeout=NO_DATA_TIMEOUT) await self.remote.cmd_standby.start(timeout=STD_TIMEOUT) - await self.assert_next_sample( - topic=self.remote.evt_errorCode, errorCode=0, errorReport="" - ) + await self.assert_next_sample(topic=self.remote.evt_errorCode, errorCode=0, errorReport="") await self.assert_next_summary_state(salobj.State.STANDBY) # if code is specified then errorReport is output; @@ -482,9 +446,7 @@ async def test_fault_method(self) -> None: await self.remote.cmd_wait.set_start(duration=5, timeout=STD_TIMEOUT) await self.remote.cmd_standby.start(timeout=STD_TIMEOUT) - await self.assert_next_sample( - topic=self.remote.evt_errorCode, errorCode=0, errorReport="" - ) + await self.assert_next_sample(topic=self.remote.evt_errorCode, errorCode=0, errorReport="") await self.assert_next_summary_state(salobj.State.STANDBY) await self.csc.fault(code=code, report="") @@ -497,9 +459,7 @@ async def test_fault_method(self) -> None: ) await self.remote.cmd_standby.start(timeout=STD_TIMEOUT) - await self.assert_next_sample( - topic=self.remote.evt_errorCode, errorCode=0, errorReport="" - ) + await self.assert_next_sample(topic=self.remote.evt_errorCode, errorCode=0, errorReport="") await self.remote.cmd_exitControl.start(timeout=STD_TIMEOUT) async def test_fault_problems(self) -> None: @@ -507,26 +467,19 @@ async def test_fault_problems(self) -> None: for doraise, report_first in itertools.product((False, True), (False, True)): with self.subTest(doraise=doraise, report_first=report_first): index = self.next_index() - async with FailInReportFaultCsc( - index=index, doraise=doraise, report_first=report_first - ) as csc, salobj.Remote( - domain=csc.domain, name="Test", index=index - ) as remote: - await self.assert_next_summary_state( - salobj.State.ENABLED, remote=remote - ) - await self.assert_next_sample( - topic=remote.evt_errorCode, errorCode=0, errorReport="" - ) + async with ( + FailInReportFaultCsc(index=index, doraise=doraise, report_first=report_first) as csc, + salobj.Remote(domain=csc.domain, name="Test", index=index) as remote, + ): + await self.assert_next_summary_state(salobj.State.ENABLED, remote=remote) + await self.assert_next_sample(topic=remote.evt_errorCode, errorCode=0, errorReport="") code = 51 report = "Report for error code" traceback = "Traceback for error code" await csc.fault(code=code, report=report, traceback=traceback) - await self.assert_next_summary_state( - salobj.State.FAULT, remote=remote - ) + await self.assert_next_summary_state(salobj.State.FAULT, remote=remote) await self.assert_next_sample( topic=remote.evt_errorCode, errorCode=code, @@ -536,13 +489,9 @@ async def test_fault_problems(self) -> None: # make sure FAULT state and errorCode are only sent once with pytest.raises(asyncio.TimeoutError): - await remote.evt_summaryState.next( - flush=False, timeout=NO_DATA_TIMEOUT - ) + await remote.evt_summaryState.next(flush=False, timeout=NO_DATA_TIMEOUT) with pytest.raises(asyncio.TimeoutError): - await remote.evt_errorCode.next( - flush=False, timeout=NO_DATA_TIMEOUT - ) + await remote.evt_errorCode.next(flush=False, timeout=NO_DATA_TIMEOUT) async def test_make_csc_timeout(self) -> None: """Test that setting the timeout argument to make_csc works.""" @@ -564,9 +513,7 @@ async def test_standard_state_transitions(self) -> None: * standby: DISABLED or FAULT to STANDBY * exitControl: STANDBY to OFFLINE (quit) """ - async with self.make_csc( - initial_state=salobj.State.STANDBY, config_dir=TEST_CONFIG_DIR - ): + async with self.make_csc(initial_state=salobj.State.STANDBY, config_dir=TEST_CONFIG_DIR): await self.check_standard_state_transitions( enabled_commands=("setArrays", "setScalars", "wait"), skip_commands=("fault",), diff --git a/tests/test_csc_configuration.py b/tests/test_csc_configuration.py index f4692773d..8aa769dfb 100644 --- a/tests/test_csc_configuration.py +++ b/tests/test_csc_configuration.py @@ -25,6 +25,7 @@ import numpy as np import yaml + from lsst.ts import salobj, utils, xml # Long enough to perform any reasonable operation @@ -65,9 +66,7 @@ def basic_make_csc( async def test_no_config_specified(self) -> None: config_dir = TEST_CONFIGS_ROOT / "good_with_site_file" - async with self.make_csc( - initial_state=salobj.State.STANDBY, config_dir=config_dir - ): + async with self.make_csc(initial_state=salobj.State.STANDBY, config_dir=config_dir): await self.assert_next_summary_state(salobj.State.STANDBY) await self.remote.cmd_start.start(timeout=STD_TIMEOUT) @@ -113,23 +112,17 @@ async def test_default_config_dir(self) -> None: async def test_bad_config_dirs(self) -> None: for bad_config_dir in TEST_CONFIGS_ROOT.glob("bad_*"): - async with self.make_csc( - initial_state=salobj.State.STANDBY, config_dir=bad_config_dir - ): + async with self.make_csc(initial_state=salobj.State.STANDBY, config_dir=bad_config_dir): await self.assert_next_summary_state(salobj.State.STANDBY) with salobj.assertRaisesAckError(): - await self.remote.cmd_start.set_start( - configurationOverride="", timeout=STD_TIMEOUT - ) + await self.remote.cmd_start.set_start(configurationOverride="", timeout=STD_TIMEOUT) async def test_override_some_fields(self) -> None: """Test an override with some fields set to valid values.""" config_dir = TEST_CONFIGS_ROOT / "good_no_site_file" override = "some_fields.yaml" - async with self.make_csc( - initial_state=salobj.State.STANDBY, config_dir=config_dir - ): + async with self.make_csc(initial_state=salobj.State.STANDBY, config_dir=config_dir): await self.assert_next_summary_state(salobj.State.STANDBY) expected_overrides = ",".join( @@ -151,9 +144,7 @@ async def test_override_some_fields(self) -> None: ) assert len(data.version) > 0 - await self.remote.cmd_start.set_start( - configurationOverride=override, timeout=STD_TIMEOUT - ) + await self.remote.cmd_start.set_start(configurationOverride=override, timeout=STD_TIMEOUT) await self.assert_next_summary_state(salobj.State.DISABLED) config = self.csc.config @@ -178,9 +169,7 @@ async def test_override_with_hash(self) -> None: config_dir = TEST_CONFIGS_ROOT / "good_no_site_file" override = "some_fields.yaml" - async with self.make_csc( - initial_state=salobj.State.STANDBY, config_dir=config_dir - ): + async with self.make_csc(initial_state=salobj.State.STANDBY, config_dir=config_dir): await self.assert_next_summary_state(salobj.State.STANDBY) await self.remote.cmd_start.set_start( configurationOverride=f"{override}:HEAD", timeout=STD_TIMEOUT @@ -206,9 +195,7 @@ async def test_override_with_hash(self) -> None: async def test_minimal_config_dir(self) -> None: config_dir = TEST_CONFIGS_ROOT / "good_minimal" - async with self.make_csc( - initial_state=salobj.State.STANDBY, config_dir=config_dir - ): + async with self.make_csc(initial_state=salobj.State.STANDBY, config_dir=config_dir): await self.assert_next_summary_state(salobj.State.STANDBY) expected_config_url = pathlib.Path(config_dir).resolve().as_uri() data = await self.assert_next_sample( @@ -239,13 +226,9 @@ async def test_override_all_fields(self) -> None: config_dir = TEST_CONFIGS_ROOT / "good_no_site_file" override = "all_fields.yaml" - async with self.make_csc( - initial_state=salobj.State.STANDBY, config_dir=config_dir - ): + async with self.make_csc(initial_state=salobj.State.STANDBY, config_dir=config_dir): await self.assert_next_summary_state(salobj.State.STANDBY) - await self.remote.cmd_start.set_start( - configurationOverride=override, timeout=STD_TIMEOUT - ) + await self.remote.cmd_start.set_start(configurationOverride=override, timeout=STD_TIMEOUT) await self.assert_next_summary_state(salobj.State.DISABLED) config = self.csc.config override_path = os.path.join(self.csc.config_dir, override) @@ -263,9 +246,7 @@ async def test_override_all_fields(self) -> None: async def test_invalid_configs(self) -> None: config_dir = TEST_CONFIGS_ROOT / "good_no_site_file" - async with self.make_csc( - initial_state=salobj.State.STANDBY, config_dir=config_dir - ): + async with self.make_csc(initial_state=salobj.State.STANDBY, config_dir=config_dir): await self.assert_next_summary_state(salobj.State.STANDBY) for name in ("all_bad_types", "bad_format", "one_bad_type", "extra_field"): config_file = f"invalid_{name}.yaml" @@ -279,8 +260,6 @@ async def test_invalid_configs(self) -> None: assert data.summaryState == salobj.State.STANDBY # Make sure the CSC can still be started. - await self.remote.cmd_start.set_start( - configurationOverride="all_fields.yaml", timeout=10 - ) + await self.remote.cmd_start.set_start(configurationOverride="all_fields.yaml", timeout=10) assert self.csc.summary_state == salobj.State.DISABLED await self.assert_next_summary_state(salobj.State.DISABLED) diff --git a/tests/test_csc_constructor.py b/tests/test_csc_constructor.py index 84f2a1145..92bdb63a1 100644 --- a/tests/test_csc_constructor.py +++ b/tests/test_csc_constructor.py @@ -25,6 +25,7 @@ import numpy as np import pytest + from lsst.ts import salobj, utils np.random.seed(47) @@ -77,14 +78,10 @@ class MissingDoMethodCsc(salobj.BaseCsc): version = "a version" def __init__(self, index: int, allow_missing_callbacks: bool) -> None: - super().__init__( - name="Test", index=index, allow_missing_callbacks=allow_missing_callbacks - ) + super().__init__(name="Test", index=index, allow_missing_callbacks=allow_missing_callbacks) -class TestCscConstructorTestCase( - salobj.BaseCscTestCase, unittest.IsolatedAsyncioTestCase -): +class TestCscConstructorTestCase(salobj.BaseCscTestCase, unittest.IsolatedAsyncioTestCase): """Test the TestCsc constructor. Note: all of these tests must run async because the constructor @@ -170,9 +167,7 @@ async def test_late_callback_assignment(self) -> None: index = next(index_gen) csc = salobj.TestCsc(index=index) try: - cmd_topics = [ - getattr(csc, f"cmd_{name}") for name in csc.salinfo.command_names - ] + cmd_topics = [getattr(csc, f"cmd_{name}") for name in csc.salinfo.command_names] for topic in cmd_topics: assert topic.callback is None diff --git a/tests/test_csc_make_from_cmd_line.py b/tests/test_csc_make_from_cmd_line.py index 97973874c..58cb1dc8e 100644 --- a/tests/test_csc_make_from_cmd_line.py +++ b/tests/test_csc_make_from_cmd_line.py @@ -29,6 +29,7 @@ import numpy as np import pytest + from lsst.ts import salobj, utils np.random.seed(47) @@ -79,9 +80,7 @@ async def test_no_index(self) -> None: sys.argv = [sys.argv[0]] arg1 = "astring" arg2 = 2.75 - async with NoIndexCsc.make_from_cmd_line( - index=index, arg1=arg1, arg2=arg2 - ) as csc: + async with NoIndexCsc.make_from_cmd_line(index=index, arg1=arg1, arg2=arg2) as csc: assert csc.arg1 == arg1 assert csc.arg2 == arg2 @@ -99,17 +98,13 @@ async def test_duplicate_rejection(self) -> None: assert not csc.check_if_duplicate await asyncio.wait_for(csc.start_task, timeout=STD_TIMEOUT) - duplicate_csc = salobj.TestCsc.make_from_cmd_line( - index=index, check_if_duplicate=True - ) + duplicate_csc = salobj.TestCsc.make_from_cmd_line(index=index, check_if_duplicate=True) try: # Change origin so heartbeat private_origin differs. duplicate_csc.salinfo.domain.origin += 1 assert duplicate_csc.salinfo.index == index assert duplicate_csc.check_if_duplicate - with pytest.raises( - salobj.ExpectedError, match="found another instance" - ): + with pytest.raises(salobj.ExpectedError, match="found another instance"): await asyncio.wait_for(duplicate_csc.done_task, timeout=STD_TIMEOUT) finally: await duplicate_csc.close() diff --git a/tests/test_csc_simulation_mode.py b/tests/test_csc_simulation_mode.py index 0de1b2b4a..d1b49fcec 100644 --- a/tests/test_csc_simulation_mode.py +++ b/tests/test_csc_simulation_mode.py @@ -27,6 +27,7 @@ import numpy as np import pytest + from lsst.ts import salobj, utils np.random.seed(47) @@ -88,9 +89,7 @@ async def test_valid_simulation_modes(self) -> None: csc_class = self.make_csc_class(valid_simulation_modes) for simulation_mode in csc_class.valid_simulation_modes: index = next(index_gen) - async with csc_class( - index=index, simulation_mode=simulation_mode - ) as csc: + async with csc_class(index=index, simulation_mode=simulation_mode) as csc: assert csc.simulation_mode == simulation_mode async def test_simulate_cmdline_arg(self) -> None: diff --git a/tests/test_csc_utils.py b/tests/test_csc_utils.py index 963c11031..9e38a52cd 100644 --- a/tests/test_csc_utils.py +++ b/tests/test_csc_utils.py @@ -24,6 +24,7 @@ import unittest import pytest + from lsst.ts import salobj, utils # Long enough to perform any reasonable operation @@ -59,9 +60,7 @@ async def test_set_summary_state_valid(self) -> None: # set_summary_state cannot transition to FAULT state. continue with self.subTest(initial_state=initial_state, final_state=final_state): - await self.check_set_summary_state( - initial_state=initial_state, final_state=final_state - ) + await self.check_set_summary_state(initial_state=initial_state, final_state=final_state) async def test_set_summary_state_invalid_state(self) -> None: """Test set_summary_state with invalid states.""" @@ -69,17 +68,13 @@ async def test_set_summary_state_invalid_state(self) -> None: if initial_state in (salobj.State.OFFLINE, salobj.State.FAULT): # TestCsc cannot start in OFFLINE or FAULT state. continue - async with self.make_csc( - initial_state=initial_state, config_dir=TEST_CONFIG_DIR - ): + async with self.make_csc(initial_state=initial_state, config_dir=TEST_CONFIG_DIR): for bad_final_state in ( min(salobj.State) - 1, salobj.State.FAULT, max(salobj.State) + 1, ): - with self.subTest( - initial_state=initial_state, bad_final_state=bad_final_state - ): + with self.subTest(initial_state=initial_state, bad_final_state=bad_final_state): with pytest.raises(ValueError): await salobj.set_summary_state( remote=self.remote, @@ -87,9 +82,7 @@ async def test_set_summary_state_invalid_state(self) -> None: timeout=STD_TIMEOUT, ) - async def check_set_summary_state( - self, initial_state: salobj.State, final_state: salobj.State - ) -> None: + async def check_set_summary_state(self, initial_state: salobj.State, final_state: salobj.State) -> None: """Check set_summary_state for valid state transitions. Parameters @@ -99,9 +92,7 @@ async def check_set_summary_state( final_state : `State` Final summary state. """ - async with self.make_csc( - initial_state=initial_state, config_dir=TEST_CONFIG_DIR - ): + async with self.make_csc(initial_state=initial_state, config_dir=TEST_CONFIG_DIR): assert self.csc.summary_state == initial_state await self.assert_next_summary_state(initial_state) diff --git a/tests/test_hierarchical_update.py b/tests/test_hierarchical_update.py index 50496938e..dc414695a 100644 --- a/tests/test_hierarchical_update.py +++ b/tests/test_hierarchical_update.py @@ -23,6 +23,7 @@ import unittest import pytest + from lsst.ts import salobj @@ -62,21 +63,15 @@ def test_basics(self) -> None: # Overriding a dict with itself should produce no changes dict1copy = copy.deepcopy(dict1) - salobj.hierarchical_update( - main=dict1copy, override=dict1, main_name="main", override_name="override" - ) + salobj.hierarchical_update(main=dict1copy, override=dict1, main_name="main", override_name="override") assert dict1copy == dict1 dict2copy = copy.deepcopy(dict2) - salobj.hierarchical_update( - main=dict2copy, override=dict2, main_name="main", override_name="override" - ) + salobj.hierarchical_update(main=dict2copy, override=dict2, main_name="main", override_name="override") assert dict2copy == dict2 dict1copy = copy.deepcopy(dict1) - salobj.hierarchical_update( - main=dict1copy, override=dict2, main_name="dict1", override_name="dict2" - ) + salobj.hierarchical_update(main=dict1copy, override=dict2, main_name="dict1", override_name="dict2") assert dict1copy == dict( key1="dict2 value1", key2="dict2 value2", diff --git a/tests/test_queue_capacity_checker.py b/tests/test_queue_capacity_checker.py index a436374ff..bfc3df95e 100644 --- a/tests/test_queue_capacity_checker.py +++ b/tests/test_queue_capacity_checker.py @@ -24,6 +24,7 @@ import unittest import pytest + from lsst.ts import salobj @@ -66,9 +67,7 @@ def test_check_nitems(self) -> None: for start_index, end_index, use_min in itertools.product( range(nthresh + 1), range(nthresh + 1), (False, True) ): - with self.subTest( - queue_len=queue_len, start_index=start_index, end_index=end_index - ): + with self.subTest(queue_len=queue_len, start_index=start_index, end_index=end_index): if end_index == start_index: # This case is handled by check_nochange_values continue @@ -87,9 +86,7 @@ def test_check_nitems(self) -> None: continue else: nitems = qlc.warn_thresholds[end_index] - 1 - expected_log_level = ( - logging.WARNING if end_index < nthresh else logging.ERROR - ) + expected_log_level = logging.WARNING if end_index < nthresh else logging.ERROR with self.assertLogs(logger=qlc.log, level=expected_log_level): did_log = qlc.check_nitems(nitems) assert did_log @@ -98,9 +95,7 @@ def test_check_nitems(self) -> None: else: expected_warn_threshold = qlc.warn_thresholds[end_index] assert qlc.warn_threshold == expected_warn_threshold - expected_reset_threshold = ( - qlc.warn_thresholds[end_index - 1] // 2 - ) + expected_reset_threshold = qlc.warn_thresholds[end_index - 1] // 2 assert qlc.reset_threshold == expected_reset_threshold else: # Reset to a lower warning level @@ -116,16 +111,12 @@ def test_check_nitems(self) -> None: expected_warn_threshold = qlc.warn_thresholds[end_index] assert qlc.warn_threshold == expected_warn_threshold if end_index > 0: - expected_reset_threshold = ( - qlc.warn_thresholds[end_index - 1] // 2 - ) + expected_reset_threshold = qlc.warn_thresholds[end_index - 1] // 2 else: expected_reset_threshold = None assert qlc.reset_threshold == expected_reset_threshold - def make_checker( - self, queue_len: int, warn_index: int - ) -> salobj.topics.read_topic.QueueCapacityChecker: + def make_checker(self, queue_len: int, warn_index: int) -> salobj.topics.read_topic.QueueCapacityChecker: """Make a QueueCapacityChecker with specified warn_threshold. Parameters @@ -137,17 +128,13 @@ def make_checker( or `None` if ``warn_index == len(warn_thresholds)``. then the initial warn_threshold is `None` """ - qlc = salobj.topics.read_topic.QueueCapacityChecker( - descr="test", log=self.log, queue_len=queue_len - ) + qlc = salobj.topics.read_topic.QueueCapacityChecker(descr="test", log=self.log, queue_len=queue_len) assert qlc.reset_threshold is None assert qlc.warn_threshold == qlc.warn_thresholds[0] nthresh = len(qlc.warn_thresholds) if warn_index > 0: nitems = qlc.warn_thresholds[warn_index - 1] - expected_log_level = ( - logging.WARNING if warn_index < nthresh else logging.ERROR - ) + expected_log_level = logging.WARNING if warn_index < nthresh else logging.ERROR with self.assertLogs(logger=qlc.log, level=expected_log_level): did_log = qlc.check_nitems(nitems) assert did_log @@ -162,17 +149,11 @@ def make_checker( assert qlc.reset_threshold == expected_reset_threshold return qlc - def check_nochange_values( - self, queue_len_checker: salobj.topics.read_topic.QueueCapacityChecker - ) -> None: + def check_nochange_values(self, queue_len_checker: salobj.topics.read_topic.QueueCapacityChecker) -> None: """Check that calling `check_nitems` does nothing for the full range of values that should not change anything. """ - min_len = ( - 0 - if queue_len_checker.reset_threshold is None - else queue_len_checker.reset_threshold + 1 - ) + min_len = 0 if queue_len_checker.reset_threshold is None else queue_len_checker.reset_threshold + 1 max_len = ( queue_len_checker.queue_len if queue_len_checker.warn_threshold is None diff --git a/tests/test_remote.py b/tests/test_remote.py index f4e6f1785..1073c52ca 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -23,6 +23,7 @@ import numpy as np import pytest + from lsst.ts import salobj, utils # Long enough to perform any reasonable operation @@ -42,9 +43,10 @@ async def test_constructor_include_exclude(self) -> None: """Test the include and exclude arguments for salobj.Remote.""" index = next(index_gen) - async with salobj.Domain() as domain, salobj.SalInfo( - domain=domain, name="Test", index=index - ) as salinfo: + async with ( + salobj.Domain() as domain, + salobj.SalInfo(domain=domain, name="Test", index=index) as salinfo, + ): # all possible expected topic names all_command_names = set(salinfo.command_names) all_event_names = set(salinfo.event_names) @@ -53,46 +55,26 @@ async def test_constructor_include_exclude(self) -> None: # the associated method names all_command_method_names = set(f"cmd_{name}" for name in all_command_names) all_event_method_names = set(f"evt_{name}" for name in all_event_names) - all_telemetry_method_names = set( - f"tel_{name}" for name in all_telemetry_names - ) + all_telemetry_method_names = set(f"tel_{name}" for name in all_telemetry_names) # remote0 specifies neither include nor exclude; # it should have everything - remote0 = salobj.Remote( - domain=domain, name="Test", index=index, start=False - ) - remote_command_names = set( - [name for name in dir(remote0) if name.startswith("cmd_")] - ) + remote0 = salobj.Remote(domain=domain, name="Test", index=index, start=False) + remote_command_names = set([name for name in dir(remote0) if name.startswith("cmd_")]) assert remote_command_names == all_command_method_names - remote_event_names = set( - [name for name in dir(remote0) if name.startswith("evt_")] - ) + remote_event_names = set([name for name in dir(remote0) if name.startswith("evt_")]) assert remote_event_names == all_event_method_names - remote_telemetry_names = set( - [name for name in dir(remote0) if name.startswith("tel_")] - ) + remote_telemetry_names = set([name for name in dir(remote0) if name.startswith("tel_")]) assert remote_telemetry_names == all_telemetry_method_names # remote1 uses the include argument include = ["errorCode", "scalars"] - remote1 = salobj.Remote( - domain=domain, name="Test", index=index, include=include, start=False - ) - remote1_command_names = set( - [name for name in dir(remote1) if name.startswith("cmd_")] - ) + remote1 = salobj.Remote(domain=domain, name="Test", index=index, include=include, start=False) + remote1_command_names = set([name for name in dir(remote1) if name.startswith("cmd_")]) assert remote1_command_names == all_command_method_names - remote1_event_names = set( - [name for name in dir(remote1) if name.startswith("evt_")] - ) - assert remote1_event_names == set( - f"evt_{name}" for name in include if name in all_event_names - ) - remote1_telemetry_names = set( - [name for name in dir(remote1) if name.startswith("tel_")] - ) + remote1_event_names = set([name for name in dir(remote1) if name.startswith("evt_")]) + assert remote1_event_names == set(f"evt_{name}" for name in include if name in all_event_names) + remote1_telemetry_names = set([name for name in dir(remote1) if name.startswith("tel_")]) assert remote1_telemetry_names == set( f"tel_{name}" for name in include if name in all_telemetry_names ) @@ -100,79 +82,47 @@ async def test_constructor_include_exclude(self) -> None: # remote2 uses the exclude argument exclude = ["errorCode", "arrays"] - remote2 = salobj.Remote( - domain=domain, name="Test", index=index, exclude=exclude, start=False - ) - remote2_command_names = set( - [name for name in dir(remote2) if name.startswith("cmd_")] - ) + remote2 = salobj.Remote(domain=domain, name="Test", index=index, exclude=exclude, start=False) + remote2_command_names = set([name for name in dir(remote2) if name.startswith("cmd_")]) assert remote2_command_names == all_command_method_names - remote2_event_names = set( - [name for name in dir(remote2) if name.startswith("evt_")] - ) + remote2_event_names = set([name for name in dir(remote2) if name.startswith("evt_")]) assert remote2_event_names == set( f"evt_{name}" for name in all_event_names if name not in exclude ) - remote2_telemetry_names = set( - [name for name in dir(remote2) if name.startswith("tel_")] - ) + remote2_telemetry_names = set([name for name in dir(remote2) if name.startswith("tel_")]) assert remote2_telemetry_names == set( f"tel_{name}" for name in all_telemetry_names if name not in exclude ) await remote2.close() # remote3 omits commands - remote3 = salobj.Remote( - domain=domain, name="Test", index=index, readonly=True, start=False - ) - remote_command_names = set( - [name for name in dir(remote3) if name.startswith("cmd_")] - ) + remote3 = salobj.Remote(domain=domain, name="Test", index=index, readonly=True, start=False) + remote_command_names = set([name for name in dir(remote3) if name.startswith("cmd_")]) assert remote_command_names == set() - remote_event_names = set( - [name for name in dir(remote3) if name.startswith("evt_")] - ) + remote_event_names = set([name for name in dir(remote3) if name.startswith("evt_")]) assert remote_event_names == all_event_method_names - remote_telemetry_names = set( - [name for name in dir(remote3) if name.startswith("tel_")] - ) + remote_telemetry_names = set([name for name in dir(remote3) if name.startswith("tel_")]) assert remote_telemetry_names == all_telemetry_method_names await remote3.close() # remote4 uses include=[] - remote4 = salobj.Remote( - domain=domain, name="Test", index=index, include=[], start=False - ) - remote_command_names = set( - [name for name in dir(remote4) if name.startswith("cmd_")] - ) + remote4 = salobj.Remote(domain=domain, name="Test", index=index, include=[], start=False) + remote_command_names = set([name for name in dir(remote4) if name.startswith("cmd_")]) assert remote_command_names == all_command_method_names - remote_event_names = set( - [name for name in dir(remote4) if name.startswith("evt_")] - ) + remote_event_names = set([name for name in dir(remote4) if name.startswith("evt_")]) assert remote_event_names == set() - remote_telemetry_names = set( - [name for name in dir(remote4) if name.startswith("tel_")] - ) + remote_telemetry_names = set([name for name in dir(remote4) if name.startswith("tel_")]) assert remote_telemetry_names == set() await remote4.close() # remote5 uses exclude=[] (though there is no reason to doubt # that it will work the same as exclude=None) - remote5 = salobj.Remote( - domain=domain, name="Test", index=index, exclude=[], start=False - ) - remote_command_names = set( - [name for name in dir(remote5) if name.startswith("cmd_")] - ) + remote5 = salobj.Remote(domain=domain, name="Test", index=index, exclude=[], start=False) + remote_command_names = set([name for name in dir(remote5) if name.startswith("cmd_")]) assert remote_command_names == all_command_method_names - remote_event_names = set( - [name for name in dir(remote5) if name.startswith("evt_")] - ) + remote_event_names = set([name for name in dir(remote5) if name.startswith("evt_")]) assert remote_event_names == all_event_method_names - remote_telemetry_names = set( - [name for name in dir(remote5) if name.startswith("tel_")] - ) + remote_telemetry_names = set([name for name in dir(remote5) if name.startswith("tel_")]) assert remote_telemetry_names == all_telemetry_method_names await remote5.close() @@ -186,17 +136,11 @@ async def test_constructor_include_exclude(self) -> None: exclude=exclude, ) - def assert_max_history( - self, remote: salobj.Remote, evt_max_history: int = 1 - ) -> None: - for evt in [ - getattr(remote, f"evt_{name}") for name in remote.salinfo.event_names - ]: + def assert_max_history(self, remote: salobj.Remote, evt_max_history: int = 1) -> None: + for evt in [getattr(remote, f"evt_{name}") for name in remote.salinfo.event_names]: assert evt.max_history == evt_max_history - for tel in [ - getattr(remote, f"tel_{name}") for name in remote.salinfo.telemetry_names - ]: + for tel in [getattr(remote, f"tel_{name}") for name in remote.salinfo.telemetry_names]: assert tel.max_history == 0 async def test_default_max_history(self) -> None: @@ -243,12 +187,15 @@ async def test_repr(self) -> None: async def test_num_messages_consume_timeout(self) -> None: index = next(index_gen) - async with salobj.Domain() as domain, salobj.Remote( - domain=domain, - name="Test", - index=index, - num_messages=100, - consume_messages_timeout=0.01, - ) as remote: + async with ( + salobj.Domain() as domain, + salobj.Remote( + domain=domain, + name="Test", + index=index, + num_messages=100, + consume_messages_timeout=0.01, + ) as remote, + ): assert remote.salinfo.num_messages == 100 assert remote.salinfo.consume_messages_timeout == 0.01 diff --git a/tests/test_sal_info.py b/tests/test_sal_info.py index 488b64dc5..c88782185 100644 --- a/tests/test_sal_info.py +++ b/tests/test_sal_info.py @@ -26,6 +26,7 @@ import unittest import pytest + from lsst.ts import salobj, utils # Long enough to perform any reasonable operation @@ -52,9 +53,7 @@ async def test_salinfo_constructor(self) -> None: salobj.SalInfo(domain=domain, name="Test", index=invalid_index) index = next(index_gen) - async with salobj.SalInfo( - domain=domain, name="Test", index=index - ) as salinfo: + async with salobj.SalInfo(domain=domain, name="Test", index=index) as salinfo: assert salinfo.name == "Test" assert salinfo.index == index assert not salinfo.start_task.done() @@ -92,9 +91,7 @@ class SalIndex(enum.IntEnum): ONE = 1 TWO = 2 - async with salobj.SalInfo( - domain=domain, name="Script", index=SalIndex.ONE - ) as salinfo: + async with salobj.SalInfo(domain=domain, name="Script", index=SalIndex.ONE) as salinfo: assert isinstance(salinfo.index, SalIndex) assert salinfo.index == SalIndex.ONE @@ -105,15 +102,15 @@ class SalIndex(enum.IntEnum): num_messages=100, consume_messages_timeout=0.01, ) as salinfo: - assert salinfo.num_messages == 100 assert salinfo.consume_messages_timeout == 0.01 async def test_salinfo_attributes(self) -> None: index = next(index_gen) - async with salobj.Domain() as domain, salobj.SalInfo( - domain=domain, name="Test", index=index - ) as salinfo: + async with ( + salobj.Domain() as domain, + salobj.SalInfo(domain=domain, name="Test", index=index) as salinfo, + ): assert salinfo.name_index == f"Test:{index}" # Expected commands; must be complete and sorted alphabetically. @@ -152,12 +149,8 @@ async def test_salinfo_attributes(self) -> None: assert expected_telemetry == salinfo.telemetry_names expected_sal_topic_names = ["ackcmd"] - expected_sal_topic_names += [ - f"command_{name}" for name in salinfo.command_names - ] - expected_sal_topic_names += [ - f"logevent_{name}" for name in salinfo.event_names - ] + expected_sal_topic_names += [f"command_{name}" for name in salinfo.command_names] + expected_sal_topic_names += [f"logevent_{name}" for name in salinfo.event_names] expected_sal_topic_names += [name for name in salinfo.telemetry_names] assert sorted(expected_sal_topic_names) == list(salinfo.sal_topic_names) @@ -169,9 +162,7 @@ async def test_salinfo_attributes(self) -> None: assert not salinfo2.indexed assert salinfo2.name_index == "MTRotator" - assert ( - salinfo.component_info.topic_subname == os.environ["LSST_TOPIC_SUBNAME"] - ) + assert salinfo.component_info.topic_subname == os.environ["LSST_TOPIC_SUBNAME"] async def test_salinfo_component_info(self) -> None: """Test some of the component info in SalInfo. @@ -179,9 +170,10 @@ async def test_salinfo_component_info(self) -> None: The main tests of ComponentInfo are elsewhere. """ index = next(index_gen) - async with salobj.Domain() as domain, salobj.SalInfo( - domain=domain, name="Test", index=index - ) as salinfo: + async with ( + salobj.Domain() as domain, + salobj.SalInfo(domain=domain, name="Test", index=index) as salinfo, + ): # Check some topic and field metadata for attr_name, topic_info in salinfo.component_info.topics.items(): assert attr_name == topic_info.attr_name @@ -197,9 +189,7 @@ async def test_salinfo_component_info(self) -> None: "tel_arrays", "tel_scalars", ) - assert set(some_expected_attr_names).issubset( - salinfo.component_info.topics.keys() - ) + assert set(some_expected_attr_names).issubset(salinfo.component_info.topics.keys()) async def test_lsst_topic_subname_required(self) -> None: # Delete LSST_TOPIC_SUBNAME. This should prevent constructing @@ -235,9 +225,10 @@ async def test_log_level(self) -> None: async def test_make_ack_cmd(self) -> None: index = next(index_gen) - async with salobj.Domain() as domain, salobj.SalInfo( - domain=domain, name="Test", index=index - ) as salinfo: + async with ( + salobj.Domain() as domain, + salobj.SalInfo(domain=domain, name="Test", index=index) as salinfo, + ): # Use all defaults seq_num = 55 ack = salobj.SalRetCode.CMD_COMPLETE @@ -265,14 +256,13 @@ async def test_make_ack_cmd(self) -> None: async def test_write_only(self) -> None: index = next(index_gen) - async with salobj.Domain() as domain, salobj.SalInfo( - domain=domain, name="Test", index=index, write_only=True - ) as salinfo: + async with ( + salobj.Domain() as domain, + salobj.SalInfo(domain=domain, name="Test", index=index, write_only=True) as salinfo, + ): # Cannot add a read topic to a write-only SalInfo with pytest.raises(RuntimeError): - salobj.topics.ReadTopic( - salinfo=salinfo, attr_name="evt_summaryState", max_history=0 - ) + salobj.topics.ReadTopic(salinfo=salinfo, attr_name="evt_summaryState", max_history=0) # Check that starting a write-only SalInfo # does not start the read loop diff --git a/tests/test_speed.py b/tests/test_speed.py index dc1f48554..9dd31c268 100644 --- a/tests/test_speed.py +++ b/tests/test_speed.py @@ -30,6 +30,7 @@ from unittest.mock import MagicMock import astropy.units as u + from lsst.ts import salobj, utils from lsst.ts.xml.component_info import ComponentInfo @@ -45,9 +46,7 @@ class MockVerify: raise ImportError("TODO: re-enable verify when we decide to use Kafkfa") except ImportError: - warnings.warn( - "verify could not be imported; measurements will not be uploaded", UserWarning - ) + warnings.warn("verify could not be imported; measurements will not be uploaded", UserWarning) verify = MockVerify # Long enough to perform any reasonable operation @@ -133,17 +132,14 @@ async def make_remote_and_topic_writer( Return the remote. """ script_path = self.datadir / "topic_writer.py" - process = await asyncio.create_subprocess_exec( - str(script_path), str(self.index) - ) + process = await asyncio.create_subprocess_exec(str(script_path), str(self.index)) try: - async with salobj.Domain() as domain, salobj.Remote( - domain=domain, name="Test", index=self.index - ) as remote: + async with ( + salobj.Domain() as domain, + salobj.Remote(domain=domain, name="Test", index=self.index) as remote, + ): yield remote - await salobj.set_summary_state( - remote=remote, state=salobj.State.OFFLINE, timeout=STD_TIMEOUT - ) + await salobj.set_summary_state(remote=remote, state=salobj.State.OFFLINE, timeout=STD_TIMEOUT) await asyncio.wait_for(process.wait(), timeout=STD_TIMEOUT) finally: if process.returncode is None: @@ -159,9 +155,7 @@ async def test_class_creation_speed(self) -> None: topic_subname = os.environ["LSST_TOPIC_SUBNAME"] t0 = time.monotonic() component_info = ComponentInfo(topic_subname=topic_subname, name="MTM1M3") - data_classes = [ - topic_info.make_dataclass() for topic_info in component_info.topics.values() - ] + data_classes = [topic_info.make_dataclass() for topic_info in component_info.topics.values()] dt = time.monotonic() - t0 ntopics = len(data_classes) creation_speed = ntopics / dt @@ -169,18 +163,14 @@ async def test_class_creation_speed(self) -> None: f"Created {creation_speed:0.1f} topic classes/sec ({ntopics} topic classes); " f"total duration {dt:0.2f} seconds." ) - self.insert_measurement( - verify.Measurement("salobj.CreateClasses", creation_speed * u.ct / u.second) - ) + self.insert_measurement(verify.Measurement("salobj.CreateClasses", creation_speed * u.ct / u.second)) async def test_command_speed(self) -> None: async with self.make_remote_and_topic_writer() as remote: summary_state = await remote.evt_summaryState.next(flush=False, timeout=60) while summary_state.private_sndStamp < self.start_time: print(f"Discarding old topic: {summary_state}") - summary_state = await remote.evt_summaryState.next( - flush=False, timeout=60 - ) + summary_state = await remote.evt_summaryState.next(flush=False, timeout=60) t0 = time.monotonic() num_commands = 1000 print(f"Writting {num_commands} commands.") @@ -192,14 +182,10 @@ async def test_command_speed(self) -> None: dt = time.monotonic() - t0 command_speed = num_commands / dt assert command_speed > 20 - print( - f"Issued {command_speed:0.0f} fault commands/second ({num_commands} commands)" - ) + print(f"Issued {command_speed:0.0f} fault commands/second ({num_commands} commands)") self.insert_measurement( - verify.Measurement( - "salobj.IssueCommands", command_speed * u.ct / u.second - ) + verify.Measurement("salobj.IssueCommands", command_speed * u.ct / u.second) ) async def test_read_speed(self) -> None: @@ -207,9 +193,7 @@ async def test_read_speed(self) -> None: summary_state = await remote.evt_summaryState.next(flush=False, timeout=60) while summary_state.private_sndStamp < self.start_time: print(f"Discarding old topic: {summary_state}") - summary_state = await remote.evt_summaryState.next( - flush=False, timeout=60 - ) + summary_state = await remote.evt_summaryState.next(flush=False, timeout=60) await salobj.set_summary_state( remote=remote, @@ -243,9 +227,7 @@ async def test_read_speed(self) -> None: ) ) - await salobj.set_summary_state( - remote=remote, state=salobj.State.STANDBY, timeout=STD_TIMEOUT - ) + await salobj.set_summary_state(remote=remote, state=salobj.State.STANDBY, timeout=STD_TIMEOUT) await salobj.set_summary_state( remote=remote, state=salobj.State.ENABLED, @@ -281,9 +263,7 @@ async def test_read_speed(self) -> None: ) async def test_write_speed(self) -> None: - async with salobj.Controller( - name="Test", index=self.index, do_callbacks=False - ) as controller: + async with salobj.Controller(name="Test", index=self.index, do_callbacks=False) as controller: num_samples = 1000 t0 = time.monotonic() @@ -291,9 +271,7 @@ async def test_write_speed(self) -> None: await controller.tel_arrays.write() dt = time.monotonic() - t0 arrays_write_speed = num_samples / dt - print( - f"Wrote {arrays_write_speed:0.0f} arrays samples/second ({num_samples} samples)" - ) + print(f"Wrote {arrays_write_speed:0.0f} arrays samples/second ({num_samples} samples)") self.insert_measurement( verify.Measurement( @@ -308,9 +286,7 @@ async def test_write_speed(self) -> None: await asyncio.sleep(0) dt = time.monotonic() - t0 log_level_write_speed = num_samples / dt - print( - f"Wrote {log_level_write_speed:0.0f} logLevel samples/second ({num_samples} samples)" - ) + print(f"Wrote {log_level_write_speed:0.0f} logLevel samples/second ({num_samples} samples)") self.insert_measurement( verify.Measurement( diff --git a/tests/test_topics.py b/tests/test_topics.py index 8b9b8e1d6..13c28224b 100644 --- a/tests/test_topics.py +++ b/tests/test_topics.py @@ -32,6 +32,7 @@ import numpy as np import pytest + from lsst.ts import salobj, utils # Long enough to perform any reasonable operation @@ -113,21 +114,15 @@ async def test_base_topic_constructor_good(self) -> None: salinfo = salobj.SalInfo(domain=domain, name="Test", index=1) for cmd_name in salinfo.command_names: - cmd = salobj.topics.BaseTopic( - salinfo=salinfo, attr_name="cmd_" + cmd_name - ) + cmd = salobj.topics.BaseTopic(salinfo=salinfo, attr_name="cmd_" + cmd_name) assert cmd.attr_name == f"cmd_{cmd_name}" for evt_name in salinfo.event_names: - evt = salobj.topics.BaseTopic( - salinfo=salinfo, attr_name="evt_" + evt_name - ) + evt = salobj.topics.BaseTopic(salinfo=salinfo, attr_name="evt_" + evt_name) assert evt.attr_name == f"evt_{evt_name}" for tel_name in salinfo.telemetry_names: - tel = salobj.topics.BaseTopic( - salinfo=salinfo, attr_name="tel_" + tel_name - ) + tel = salobj.topics.BaseTopic(salinfo=salinfo, attr_name="tel_" + tel_name) assert tel.attr_name == f"tel_{tel_name}" async def test_base_topic_constructor_errors(self) -> None: @@ -144,52 +139,42 @@ async def test_base_topic_constructor_errors(self) -> None: "tel", # no trailing underscore ): with pytest.raises(RuntimeError): - salobj.topics.BaseTopic( - salinfo=salinfo, attr_name=bad_prefix + good_name - ) + salobj.topics.BaseTopic(salinfo=salinfo, attr_name=bad_prefix + good_name) for good_prefix in ("ack_", "cmd_", "evt_", "tel_"): for bad_name in ("", "no_such_topic"): with pytest.raises(RuntimeError): - salobj.topics.BaseTopic( - salinfo=salinfo, attr_name=good_prefix + bad_name - ) + salobj.topics.BaseTopic(salinfo=salinfo, attr_name=good_prefix + bad_name) for cmd_name in salinfo.command_names: for non_cmd_prefix in ("ack_", "evt_", "tel_"): with pytest.raises(RuntimeError): - salobj.topics.BaseTopic( - salinfo=salinfo, attr_name=non_cmd_prefix + cmd_name - ) + salobj.topics.BaseTopic(salinfo=salinfo, attr_name=non_cmd_prefix + cmd_name) # there is overlap between event and telemetry names # so just use the command_ prefix as the invalid prefix non_evt_prefix = "cmd_" for evt_name in salinfo.event_names: with pytest.raises(RuntimeError): - salobj.topics.BaseTopic( - salinfo=salinfo, attr_name=non_evt_prefix + evt_name - ) + salobj.topics.BaseTopic(salinfo=salinfo, attr_name=non_evt_prefix + evt_name) # there is overlap between event and telemetry names # so just use the command_ prefix as the invalid prefix non_tel_prefix = "cmd__" for tel_name in salinfo.telemetry_names: with pytest.raises(RuntimeError): - salobj.topics.BaseTopic( - salinfo=salinfo, attr_name=non_tel_prefix + tel_name - ) + salobj.topics.BaseTopic(salinfo=salinfo, attr_name=non_tel_prefix + tel_name) async def test_command_isolation(self) -> None: """Test that multiple RemoteCommands for one command only see ackcmd replies to their own samples. """ - async with salobj.Domain() as domain, salobj.SalInfo( - domain=domain, name="Test", index=1 - ) as salinfo, salobj.SalInfo(domain=domain, name="Test", index=0) as salinfo0: - cmdreader = salobj.topics.ReadTopic( - salinfo=salinfo, attr_name="cmd_wait", max_history=0 - ) + async with ( + salobj.Domain() as domain, + salobj.SalInfo(domain=domain, name="Test", index=1) as salinfo, + salobj.SalInfo(domain=domain, name="Test", index=0) as salinfo0, + ): + cmdreader = salobj.topics.ReadTopic(salinfo=salinfo, attr_name="cmd_wait", max_history=0) cmdwriter = salobj.topics.RemoteCommand(salinfo=salinfo, name="wait") cmdtype = salinfo.sal_topic_names.index(cmdwriter.sal_name) ackcmdwriter = salobj.topics.AckCmdWriter(salinfo=salinfo) @@ -199,9 +184,7 @@ async def test_command_isolation(self) -> None: unfiltered_ackcmd_reader = salobj.topics.ReadTopic( salinfo=salinfo0, attr_name="ack_ackcmd", max_history=0 ) - await asyncio.wait_for( - asyncio.gather(salinfo.start(), salinfo0.start()), timeout=STD_TIMEOUT - ) + await asyncio.wait_for(asyncio.gather(salinfo.start(), salinfo0.start()), timeout=STD_TIMEOUT) # Send and acknowledge 4 commands: # * The first is acknowledged with a different origin. @@ -254,9 +237,7 @@ async def unfiltered_ackcmd_reader_callback( for i in range(4): cmd_callback_event.clear() tasks.append(asyncio.create_task(cmdwriter.start(timeout=2))) - await asyncio.wait_for( - cmd_callback_event.wait(), timeout=STD_TIMEOUT - ) + await asyncio.wait_for(cmd_callback_event.wait(), timeout=STD_TIMEOUT) assert nread == i + 1 await tasks[3] assert not tasks[0].done() # Origin did not match. @@ -318,9 +299,7 @@ async def test_controller_event_write(self) -> None: written_data = await self.csc.evt_scalars.write() self.csc.assert_scalars_equal(written_data, scalars_dict1) self.csc.assert_scalars_equal(self.csc.evt_scalars.data, scalars_dict1) - read_data = await self.remote.evt_scalars.next( - flush=False, timeout=STD_TIMEOUT - ) + read_data = await self.remote.evt_scalars.next(flush=False, timeout=STD_TIMEOUT) self.csc.assert_scalars_equal(read_data, scalars_dict1) rcv_tai0 = utils.current_tai() with pytest.raises(asyncio.TimeoutError): @@ -337,9 +316,7 @@ async def test_controller_event_write(self) -> None: write_result = await self.csc.evt_scalars.set_write(**scalars_dict2) self.csc.assert_scalars_equal(self.csc.evt_scalars.data, scalars_dict2) self.csc.assert_scalars_equal(write_result.data, scalars_dict2) - read_data = await self.remote.evt_scalars.next( - flush=False, timeout=STD_TIMEOUT - ) + read_data = await self.remote.evt_scalars.next(flush=False, timeout=STD_TIMEOUT) self.csc.assert_scalars_equal(read_data, scalars_dict2) with pytest.raises(asyncio.TimeoutError): await self.remote.evt_scalars.next(flush=False, timeout=NO_DATA_TIMEOUT) @@ -349,17 +326,11 @@ async def test_controller_set_and_write(self) -> None: and ControllerEvent. """ async with self.make_csc(initial_state=salobj.State.ENABLED): - for do_telemetry, do_arrays in itertools.product( - (False, True), (False, True) - ): + for do_telemetry, do_arrays in itertools.product((False, True), (False, True)): with self.subTest(do_telmetry=do_telemetry, do_arrrays=do_arrays): - await self.check_controller_set_and_write( - do_telemetry=do_telemetry, do_arrays=do_arrays - ) + await self.check_controller_set_and_write(do_telemetry=do_telemetry, do_arrays=do_arrays) - async def set_scalars( - self, num_commands: int, assert_none: bool = True - ) -> list[salobj.BaseMsgType]: + async def set_scalars(self, num_commands: int, assert_none: bool = True) -> list[salobj.BaseMsgType]: """Send the setScalars command repeatedly and return the data sent. Each command is sent with new random data. Each command triggers @@ -383,9 +354,7 @@ async def set_scalars( sent_data_list = [] for _ in range(num_commands): scalars_dict = self.csc.make_random_scalars_dict() - await self.remote.cmd_setScalars.set_start( - **scalars_dict, timeout=STD_TIMEOUT - ) + await self.remote.cmd_setScalars.set_start(**scalars_dict, timeout=STD_TIMEOUT) sent_data_list.append(self.remote.cmd_setScalars.data) return sent_data_list @@ -397,12 +366,8 @@ async def test_aget(self) -> None: await self.remote.tel_scalars.aget(timeout=NO_DATA_TIMEOUT) # start waiting for both events, then trigger multiple events - evt_task = asyncio.create_task( - self.remote.evt_scalars.aget(timeout=STD_TIMEOUT) - ) - tel_task = asyncio.create_task( - self.remote.tel_scalars.aget(timeout=STD_TIMEOUT) - ) + evt_task = asyncio.create_task(self.remote.evt_scalars.aget(timeout=STD_TIMEOUT)) + tel_task = asyncio.create_task(self.remote.tel_scalars.aget(timeout=STD_TIMEOUT)) num_commands = 3 cmd_data_list = await self.set_scalars(num_commands=num_commands) @@ -415,12 +380,8 @@ async def test_aget(self) -> None: # the aget should not interfere with next for i in range(num_commands): print(f"test {i}") - evt_data = await self.remote.evt_scalars.next( - flush=False, timeout=STD_TIMEOUT - ) - tel_data = await self.remote.tel_scalars.next( - flush=False, timeout=STD_TIMEOUT - ) + evt_data = await self.remote.evt_scalars.next(flush=False, timeout=STD_TIMEOUT) + tel_data = await self.remote.tel_scalars.next(flush=False, timeout=STD_TIMEOUT) self.csc.assert_scalars_equal(cmd_data_list[i], evt_data) self.csc.assert_scalars_equal(cmd_data_list[i], tel_data) @@ -448,9 +409,7 @@ async def test_plain_get(self) -> None: num_commands = 3 for _ in range(num_commands): - cmd_data_list = await self.set_scalars( - num_commands=1, assert_none=False - ) + cmd_data_list = await self.set_scalars(num_commands=1, assert_none=False) next_data = await read_topic.next(flush=False, timeout=STD_TIMEOUT) get_data = read_topic.get() @@ -493,9 +452,7 @@ async def test_next(self) -> None: evt_data_list = [] while True: try: - evt_data = await self.remote.evt_scalars.next( - flush=False, timeout=NO_DATA_TIMEOUT - ) + evt_data = await self.remote.evt_scalars.next(flush=False, timeout=NO_DATA_TIMEOUT) assert evt_data is not None evt_data_list.append(evt_data) except asyncio.TimeoutError: @@ -507,9 +464,7 @@ async def test_next(self) -> None: tel_data_list = [] while True: try: - tel_data = await self.remote.tel_scalars.next( - flush=False, timeout=NO_DATA_TIMEOUT - ) + tel_data = await self.remote.tel_scalars.next(flush=False, timeout=NO_DATA_TIMEOUT) assert tel_data is not None tel_data_list.append(tel_data) except asyncio.TimeoutError: @@ -537,9 +492,7 @@ class Reader: Reader name """ - def __init__( - self, read_topic: salobj.topics.ReadTopic, nitems: int, name: str - ) -> None: + def __init__(self, read_topic: salobj.topics.ReadTopic, nitems: int, name: str) -> None: self.read_topic = read_topic self.nitems = nitems self.name = name @@ -566,9 +519,7 @@ async def read_loop(self) -> None: ] for item in data: await asyncio.wait_for( - asyncio.gather( - *[reader.ready_to_read.wait() for reader in readers] - ), + asyncio.gather(*[reader.ready_to_read.wait() for reader in readers]), timeout=STD_TIMEOUT, ) for reader in readers: @@ -621,9 +572,7 @@ async def tel_callback(data: salobj.BaseMsgType) -> None: await self.remote.tel_scalars.next(flush=False) cmd_data_list = await self.set_scalars(num_commands=num_commands) - await asyncio.wait_for( - asyncio.gather(evt_future, tel_future), timeout=STD_TIMEOUT - ) + await asyncio.wait_for(asyncio.gather(evt_future, tel_future), timeout=STD_TIMEOUT) assert len(evt_data_list) == num_commands for cmd_data, evt_data in zip(cmd_data_list, evt_data_list): @@ -662,9 +611,7 @@ def tel_callback(data: salobj.BaseMsgType) -> None: self.remote.tel_scalars.callback = tel_callback cmd_data_list = await self.set_scalars(num_commands=num_commands) - await asyncio.wait_for( - asyncio.gather(evt_future, tel_future), timeout=STD_TIMEOUT - ) + await asyncio.wait_for(asyncio.gather(evt_future, tel_future), timeout=STD_TIMEOUT) assert len(evt_data_list) == num_commands for cmd_data, evt_data in zip(cmd_data_list, evt_data_list): @@ -721,9 +668,7 @@ async def test_controller_command_get_next(self) -> None: self.csc.cmd_wait.callback = None duration = 1 - task1 = asyncio.create_task( - self.remote.cmd_wait.set_start(duration=duration) - ) + task1 = asyncio.create_task(self.remote.cmd_wait.set_start(duration=duration)) next_data = await self.csc.cmd_wait.next(timeout=STD_TIMEOUT) get_data = self.csc.cmd_wait.get() assert get_data is not None @@ -735,9 +680,7 @@ async def test_controller_command_get_next(self) -> None: await self.csc.cmd_wait.next(timeout=NO_DATA_TIMEOUT) duration = 2 - task2 = asyncio.create_task( - self.remote.cmd_wait.set_start(duration=duration) - ) + task2 = asyncio.create_task(self.remote.cmd_wait.set_start(duration=duration)) await asyncio.sleep(0.5) get_data = self.csc.cmd_wait.get() next_data = await self.csc.cmd_wait.next(timeout=STD_TIMEOUT) @@ -777,9 +720,7 @@ async def foo() -> None: async def test_controller_command_success(self) -> None: """Test ack when a controller command succeeds.""" async with self.make_csc(initial_state=salobj.State.ENABLED): - ackcmd = await self.remote.cmd_wait.set_start( - duration=0, timeout=STD_TIMEOUT - ) + ackcmd = await self.remote.cmd_wait.set_start(duration=0, timeout=STD_TIMEOUT) assert ackcmd.ack == salobj.SalRetCode.CMD_COMPLETE async def test_controller_command_callback_return_failed_ackcmd(self) -> None: @@ -928,9 +869,7 @@ async def check_controller_set_and_write( # and all values of force_output. for force_output in (False, True, None): with self.subTest(force_output=force_output): - write_result = await write_topic.set_write( - **input_dict, force_output=force_output - ) + write_result = await write_topic.set_write(**input_dict, force_output=force_output) assert not write_result.did_change if force_output is True: assert write_result.was_written @@ -1026,9 +965,7 @@ async def test_multiple_commands(self) -> None: running at the same time. """ async with self.make_csc(initial_state=salobj.State.ENABLED): - await self.assert_next_sample( - self.remote.evt_heartbeat, flush=True, timeout=STD_TIMEOUT - ) + await self.assert_next_sample(self.remote.evt_heartbeat, flush=True, timeout=STD_TIMEOUT) assert self.csc.cmd_wait.has_callback assert self.csc.cmd_wait.allow_multiple_callbacks @@ -1086,9 +1023,10 @@ async def test_multiple_sequential_commands(self) -> None: async def test_remote_command_not_ready(self) -> None: """Test RemoteCommand methods that should raise an exception when the read loop isn't running.""" - async with salobj.Domain() as domain, salobj.SalInfo( - domain=domain, name="Test", index=self.next_index() - ) as salinfo: + async with ( + salobj.Domain() as domain, + salobj.SalInfo(domain=domain, name="Test", index=self.next_index()) as salinfo, + ): cmdwriter = salobj.topics.RemoteCommand(salinfo=salinfo, name="setScalars") with pytest.raises(RuntimeError): await cmdwriter.start(timeout=NO_DATA_TIMEOUT) @@ -1103,12 +1041,8 @@ async def test_remote_command_set(self) -> None: for each call, rather than remembering anything from the previous command. This is different than WriteTopic.set. """ - async with salobj.Domain() as domain, salobj.SalInfo( - domain=domain, name="Test", index=1 - ) as salinfo: - cmdreader = salobj.topics.ControllerCommand( - salinfo=salinfo, name="setScalars" - ) + async with salobj.Domain() as domain, salobj.SalInfo(domain=domain, name="Test", index=1) as salinfo: + cmdreader = salobj.topics.ControllerCommand(salinfo=salinfo, name="setScalars") cmdwriter = salobj.topics.RemoteCommand(salinfo=salinfo, name="setScalars") await salinfo.start() @@ -1136,18 +1070,14 @@ async def cmd_reader_callback(data: salobj.BaseMsgType) -> None: for kwargs in kwargs_list: cmdwriter.set(**kwargs) for field in fields: - assert getattr(cmdwriter.data, field) == pytest.approx( - kwargs.get(field, 0) - ) + assert getattr(cmdwriter.data, field) == pytest.approx(kwargs.get(field, 0)) # RemoteCommand.start with no data does not reset data, # so that it can be used with set. last_kwargs = kwargs_list[-1] await cmdwriter.start(timeout=STD_TIMEOUT) for field in fields: - assert getattr(cmdwriter.data, field) == pytest.approx( - last_kwargs.get(field, 0) - ) + assert getattr(cmdwriter.data, field) == pytest.approx(last_kwargs.get(field, 0)) assert len(read_data_list) == 1 # RemoteCommand.set with no kwargs resets all data @@ -1163,9 +1093,7 @@ async def cmd_reader_callback(data: salobj.BaseMsgType) -> None: assert len(read_data_list) == i + start_ind read_data = read_data_list[-1] for field in fields: - assert getattr(read_data, field) == pytest.approx( - kwargs.get(field, 0) - ) + assert getattr(read_data, field) == pytest.approx(kwargs.get(field, 0)) # Make sure set_write and write are prohibited. with pytest.raises(NotImplementedError): @@ -1175,14 +1103,13 @@ async def cmd_reader_callback(data: salobj.BaseMsgType) -> None: async def test_read_topic_not_ready(self) -> None: """Test ReadTopic for exceptions when the read loop isn't running.""" - async with salobj.Domain() as domain, salobj.SalInfo( - domain=domain, name="Test", index=self.next_index() - ) as salinfo: + async with ( + salobj.Domain() as domain, + salobj.SalInfo(domain=domain, name="Test", index=self.next_index()) as salinfo, + ): # Use a logevent topic because it is not volatile # (which might cause the read loop to start too quickly). - topic = salobj.topics.ReadTopic( - salinfo=salinfo, attr_name="evt_scalars", max_history=100 - ) + topic = salobj.topics.ReadTopic(salinfo=salinfo, attr_name="evt_scalars", max_history=100) with pytest.raises(RuntimeError): topic.has_data with pytest.raises(RuntimeError): @@ -1199,9 +1126,10 @@ async def test_read_topic_not_ready(self) -> None: async def test_read_topic_constructor_errors_and_warnings(self) -> None: MIN_QUEUE_LEN = salobj.topics.MIN_QUEUE_LEN - async with salobj.Domain() as domain, salobj.SalInfo( - domain=domain, name="Test", index=self.next_index() - ) as salinfo: + async with ( + salobj.Domain() as domain, + salobj.SalInfo(domain=domain, name="Test", index=self.next_index()) as salinfo, + ): # max_history must not be negative for bad_max_history in (-1, -10): for attr_name in ( @@ -1266,17 +1194,13 @@ async def test_command_next_ack(self) -> None: ) assert ackcmd1.ack == salobj.SalRetCode.CMD_INPROGRESS assert ackcmd1.timeout == pytest.approx(duration) - ackcmd2 = await self.remote.cmd_wait.next_ackcmd( - ackcmd1, wait_done=True, timeout=STD_TIMEOUT - ) + ackcmd2 = await self.remote.cmd_wait.next_ackcmd(ackcmd1, wait_done=True, timeout=STD_TIMEOUT) assert ackcmd2.ack == salobj.SalRetCode.CMD_COMPLETE # Now try a timeout. Specify a negative duration to avoid the # CMD_INPROGRESS command ack that extends the timeout. with salobj.assertRaisesAckTimeoutError(): - await self.remote.cmd_wait.set_start( - duration=-5, wait_done=False, timeout=NO_DATA_TIMEOUT - ) + await self.remote.cmd_wait.set_start(duration=-5, wait_done=False, timeout=NO_DATA_TIMEOUT) async def test_command_seq_num(self) -> None: async with self.make_csc(initial_state=salobj.State.ENABLED): @@ -1318,11 +1242,7 @@ async def test_mock_write_topic(self) -> None: ] for attr_name, data_list in test_data: topic = getattr(topics, attr_name) - assert ( - topic.default_force_output is False - if attr_name.startswith("evt") - else True - ) + assert topic.default_force_output is False if attr_name.startswith("evt") else True assert len(topic.data_list) == 0 for i, data in enumerate(data_list): await topic.set_write(**data) @@ -1344,36 +1264,22 @@ async def test_topic_subname(self) -> None: """Test specifying topic subname with $LSST_TOPIC_SUBNAME.""" salobj.set_test_topic_subname(randomize=True) - async with salobj.Domain() as domain, salobj.SalInfo( - domain=domain, name="Test", index=0 - ) as salinfo_r1, salobj.SalInfo( - domain=domain, name="Test", index=0 - ) as salinfo_w1: + async with ( + salobj.Domain() as domain, + salobj.SalInfo(domain=domain, name="Test", index=0) as salinfo_r1, + salobj.SalInfo(domain=domain, name="Test", index=0) as salinfo_w1, + ): salobj.set_test_topic_subname(randomize=True) - async with salobj.SalInfo( - domain=domain, name="Test", index=0 - ) as salinfo_r2, salobj.SalInfo( - domain=domain, name="Test", index=0 - ) as salinfo_w2: - assert ( - salinfo_r1.component_info.topic_subname - == salinfo_w1.component_info.topic_subname - ) - assert ( - salinfo_r2.component_info.topic_subname - == salinfo_w2.component_info.topic_subname - ) - assert ( - salinfo_r1.component_info.topic_subname - != salinfo_r2.component_info.topic_subname - ) + async with ( + salobj.SalInfo(domain=domain, name="Test", index=0) as salinfo_r2, + salobj.SalInfo(domain=domain, name="Test", index=0) as salinfo_w2, + ): + assert salinfo_r1.component_info.topic_subname == salinfo_w1.component_info.topic_subname + assert salinfo_r2.component_info.topic_subname == salinfo_w2.component_info.topic_subname + assert salinfo_r1.component_info.topic_subname != salinfo_r2.component_info.topic_subname - writer1 = salobj.topics.ControllerEvent( - salinfo=salinfo_w1, name="errorCode" - ) - writer2 = salobj.topics.ControllerEvent( - salinfo=salinfo_w2, name="errorCode" - ) + writer1 = salobj.topics.ControllerEvent(salinfo=salinfo_w1, name="errorCode") + writer2 = salobj.topics.ControllerEvent(salinfo=salinfo_w2, name="errorCode") await asyncio.wait_for( asyncio.gather(salinfo_w1.start(), salinfo_w2.start()), timeout=STD_TIMEOUT, @@ -1386,12 +1292,8 @@ async def test_topic_subname(self) -> None: await writer2.set_write(errorCode=20 + i) # create readers and set callbacks for them - reader1 = salobj.topics.RemoteEvent( - salinfo=salinfo_r1, name="errorCode" - ) - reader2 = salobj.topics.RemoteEvent( - salinfo=salinfo_r2, name="errorCode" - ) + reader1 = salobj.topics.RemoteEvent(salinfo=salinfo_r1, name="errorCode") + reader2 = salobj.topics.RemoteEvent(salinfo=salinfo_r2, name="errorCode") await asyncio.wait_for( asyncio.gather(salinfo_r1.start(), salinfo_r2.start()), timeout=STD_TIMEOUT, @@ -1534,12 +1436,8 @@ async def test_topic_repr(self) -> None: async def test_write_topic_set(self) -> None: """Test that WriteTopic.set uses existing data for defaults.""" - async with salobj.Domain() as domain, salobj.SalInfo( - domain=domain, name="Test", index=1 - ) as salinfo: - write_topic = salobj.topics.WriteTopic( - salinfo=salinfo, attr_name="evt_scalars" - ) + async with salobj.Domain() as domain, salobj.SalInfo(domain=domain, name="Test", index=1) as salinfo: + write_topic = salobj.topics.WriteTopic(salinfo=salinfo, attr_name="evt_scalars") await asyncio.wait_for(salinfo.start(), timeout=STD_TIMEOUT) predicted_data_dict = vars(write_topic.DataType()) @@ -1556,28 +1454,25 @@ async def test_write_topic_set(self) -> None: write_topic.set(**kwargs) predicted_data_dict.update(kwargs) for field in fields: - assert getattr(write_topic.data, field) == pytest.approx( - predicted_data_dict[field] - ) + assert getattr(write_topic.data, field) == pytest.approx(predicted_data_dict[field]) async def test_read_num_messages_read_timeout(self) -> None: """Test modifying the value of num_messages and message_read_timeout in SalInfo read loop.""" num_messages = 100 consume_messages_timeout = 2.5 - async with salobj.Domain() as domain, salobj.SalInfo( - domain=domain, - name="Test", - index=3, - num_messages=num_messages, - consume_messages_timeout=consume_messages_timeout, - ) as salinfo: - tel_reader = salobj.topics.ReadTopic( - salinfo=salinfo, attr_name="tel_scalars", max_history=0 - ) - tel_writter = salobj.topics.WriteTopic( - salinfo=salinfo, attr_name="tel_scalars" - ) + async with ( + salobj.Domain() as domain, + salobj.SalInfo( + domain=domain, + name="Test", + index=3, + num_messages=num_messages, + consume_messages_timeout=consume_messages_timeout, + ) as salinfo, + ): + tel_reader = salobj.topics.ReadTopic(salinfo=salinfo, attr_name="tel_scalars", max_history=0) + tel_writter = salobj.topics.WriteTopic(salinfo=salinfo, attr_name="tel_scalars") await asyncio.wait_for(salinfo.start(), timeout=STD_TIMEOUT) # Write 99 messages and try to read them with a short timeout. diff --git a/tests/test_validator.py b/tests/test_validator.py index 5aa18e2d8..f45db2c21 100644 --- a/tests/test_validator.py +++ b/tests/test_validator.py @@ -26,6 +26,7 @@ import jsonschema import pytest import yaml + from lsst.ts import salobj @@ -137,9 +138,7 @@ def test_invalid_data(self) -> None: intarr0=[0, 2, -3, -5, 4], multi_type=5, ) - bad_data = dict( - string0=45, bool0=35, int0=1.234, float0="hello", intarr0=45, multi_type=3.5 - ) + bad_data = dict(string0=45, bool0=35, int0=1.234, float0="hello", intarr0=45, multi_type=3.5) # set one field at a time to bad data for field in good_data: data = good_data.copy() From 9c9aefe044546f7f346da0af880537bf0548d956 Mon Sep 17 00:00:00 2001 From: Wouter van Reeven Date: Tue, 3 Mar 2026 18:23:08 +0100 Subject: [PATCH 2/2] Add support for numpy types in SAL messages. --- doc/news/OSW-1886.misc.1.rst | 1 + python/lsst/ts/salobj/controller.py | 5 +++-- python/lsst/ts/salobj/csc_commander.py | 6 ++++++ python/lsst/ts/salobj/sal_info.py | 19 ++++++++++++++++-- python/lsst/ts/salobj/topics/base_topic.py | 11 ++++++++++- python/lsst/ts/salobj/topics/write_topic.py | 22 +++++++++++---------- tests/test_controller_logging.py | 1 + tests/test_topics.py | 13 ++++++------ 8 files changed, 57 insertions(+), 21 deletions(-) create mode 100644 doc/news/OSW-1886.misc.1.rst diff --git a/doc/news/OSW-1886.misc.1.rst b/doc/news/OSW-1886.misc.1.rst new file mode 100644 index 000000000..ce94c1774 --- /dev/null +++ b/doc/news/OSW-1886.misc.1.rst @@ -0,0 +1 @@ +Added support for numpy types in SAL messages. diff --git a/python/lsst/ts/salobj/controller.py b/python/lsst/ts/salobj/controller.py index 1fe5deaee..53a7ceac0 100644 --- a/python/lsst/ts/salobj/controller.py +++ b/python/lsst/ts/salobj/controller.py @@ -382,7 +382,7 @@ async def close(self, exception: Exception | None = None, cancel_start: bool = T if not self.isopen: # Closed or closing (we know this instance is fully constructed - # because we checked that start_task is exists). + # because we checked that start_task exists). # Wait for done_task to be finished, # ignoring any exception. If you want to know about the exception # you can examine done_task yourself. @@ -433,7 +433,8 @@ async def do_setLogLevel(self, data: type_hints.BaseMsgType) -> None: data : ``cmd_setLogLevel.DataType`` Logging level. """ - self.log.setLevel(data.level) # type: ignore + # Explicitly cast to int so numpy int types are accepted. + self.log.setLevel(int(data.level)) # type: ignore await self.put_log_level() async def put_log_level(self) -> None: diff --git a/python/lsst/ts/salobj/csc_commander.py b/python/lsst/ts/salobj/csc_commander.py index 332c41db5..7e32312bf 100644 --- a/python/lsst/ts/salobj/csc_commander.py +++ b/python/lsst/ts/salobj/csc_commander.py @@ -36,6 +36,8 @@ import warnings from collections.abc import AsyncGenerator, Callable, Sequence +import numpy as np + from lsst.ts.xml import sal_enums, type_hints from . import csc_utils, domain, remote @@ -584,8 +586,12 @@ async def run_command_topic(self, command_name: str, args: Sequence[str]) -> Non raise ValueError(f"Command {command_name} requires {len(kwargs)} arguments; got {len(args)}") for (name, default_value), str_value in zip(kwargs.items(), args): try: + # TODO OSW-1915 Remove backward compatibility with python + # data types. if isinstance(default_value, bool): kwargs[name] = BOOL_DICT[str_value.lower()] + elif isinstance(default_value, np.bool): + kwargs[name] = np.bool(BOOL_DICT[str_value.lower()]) else: kwargs[name] = type(default_value)(str_value) except Exception: diff --git a/python/lsst/ts/salobj/sal_info.py b/python/lsst/ts/salobj/sal_info.py index ab873a5ff..f8b68ad63 100644 --- a/python/lsst/ts/salobj/sal_info.py +++ b/python/lsst/ts/salobj/sal_info.py @@ -28,6 +28,7 @@ import base64 import collections import enum +import inspect import itertools import json import logging @@ -361,7 +362,15 @@ def __init__( if self.index != 0 and not self.indexed: raise ValueError(f"Index={index!r} must be 0 or None; {name} is not an indexed SAL component") if len(self.command_names) > 0: - self._ackcmd_type = self.component_info.topics["ack_ackcmd"].make_dataclass() + # TODO OSW-1915 Remove backward compatibility with python data + # types. + # hasattr doesn't work so use inspect instead. + ack_topic_info = self.component_info.topics["ack_ackcmd"] + args = inspect.getfullargspec(ack_topic_info.make_dataclass).args + if "with_numpy_types" in args: + self._ackcmd_type = ack_topic_info.make_dataclass(with_numpy_types=True) + else: + self._ackcmd_type = ack_topic_info.make_dataclass() domain.add_salinfo(self) @@ -1294,7 +1303,13 @@ def _process_message( return sequential_read_errors last_sample_timestamps[kafka_name][index] = data_dict["private_sndStamp"] data_dict["private_rcvStamp"] = utils.current_tai() - data = read_topic.DataType(**data_dict) + + # TODO OSW-1915 Remove backward compatibility with python data types. + if hasattr(read_topic.topic_info, "convert_to_numpy_dict"): + numpy_data_dic = read_topic.topic_info.convert_to_numpy_dict(data_dict) + data = read_topic.DataType(**numpy_data_dic) + else: + data = read_topic.DataType(**data_dict) history_offset = self._history_offsets.get(kafka_name) if history_offset is None: diff --git a/python/lsst/ts/salobj/topics/base_topic.py b/python/lsst/ts/salobj/topics/base_topic.py index 1b095b392..07f1ba04c 100644 --- a/python/lsst/ts/salobj/topics/base_topic.py +++ b/python/lsst/ts/salobj/topics/base_topic.py @@ -24,6 +24,7 @@ __all__ = ["BaseTopic"] import abc +import inspect import typing from lsst.ts.xml import type_hints @@ -64,7 +65,15 @@ def __init__(self, *, salinfo: SalInfo, attr_name: str) -> None: self.topic_info = self.salinfo.component_info.topics[attr_name] self.rev_code = self.topic_info.get_revcode() self.log = salinfo.log.getChild(self.sal_name) - self._type = self.topic_info.make_dataclass() + + # TODO OSW-1915 Remove backward compatibility with python data + # types. + # hasattr doesn't work so use inspect instead. + args = inspect.getfullargspec(self.topic_info.make_dataclass).args + if "with_numpy_types" in args: + self._type = self.topic_info.make_dataclass(with_numpy_types=True) + else: + self._type = self.topic_info.make_dataclass() except Exception as e: raise RuntimeError(f"Failed to create topic {salinfo.name}.{attr_name}") from e diff --git a/python/lsst/ts/salobj/topics/write_topic.py b/python/lsst/ts/salobj/topics/write_topic.py index f75fe5d64..f7e704004 100644 --- a/python/lsst/ts/salobj/topics/write_topic.py +++ b/python/lsst/ts/salobj/topics/write_topic.py @@ -131,15 +131,10 @@ def __init__( # Record which field names are float, double or array of either, # to make it easy to compare float fields with nan equal. self._float_field_names = set() - for name, value in vars(self._data).items(): - if isinstance(value, list): - # In our SAL schemas arrays are fixed length - # and must contain at least one element. - elt = value[0] - else: - elt = value - if isinstance(elt, float): - self._float_field_names.add(name) + for field in self.topic_info.fields: + field_info = self.topic_info.fields[field] + if field_info.sal_type in ["float", "double"]: + self._float_field_names.add(field) salinfo.add_writer(self) @@ -261,9 +256,16 @@ def set(self, **kwargs: typing.Any) -> bool: except Exception as e: raise TypeError(f"Cannot set {self.attr_name}.{field_name}={value!r}; wrong type.") from e data_dict[field_name] = value + + # TODO OSW-1915 Remove backward compatibility with python data types. # Check the data by creating a DataType, because no checking is done # when directly setting attributes of a dataclass. - self.data = self.DataType(**data_dict) + if hasattr(self.topic_info, "convert_to_numpy_dict"): + numpy_data_dic = self.topic_info.convert_to_numpy_dict(data_dict) + self.data = self.DataType(**numpy_data_dic) + else: + self.data = self.DataType(**data_dict) + return did_change async def set_write(self, *, force_output: bool | None = None, **kwargs: typing.Any) -> SetWriteResult: diff --git a/tests/test_controller_logging.py b/tests/test_controller_logging.py index 75d067aa1..513958f8b 100644 --- a/tests/test_controller_logging.py +++ b/tests/test_controller_logging.py @@ -62,6 +62,7 @@ def basic_make_csc( initial_state: salobj.State | int, config_dir: str | pathlib.Path | None, simulation_mode: int, + **kwargs: typing.Any, ) -> salobj.BaseCsc: return FailedCallbackCsc( initial_state=initial_state, diff --git a/tests/test_topics.py b/tests/test_topics.py index 13c28224b..7c57649f1 100644 --- a/tests/test_topics.py +++ b/tests/test_topics.py @@ -52,6 +52,7 @@ def basic_make_csc( initial_state: salobj.State | int, config_dir: str | pathlib.Path | None, simulation_mode: int, + **kwargs: typing.Any, ) -> salobj.BaseCsc: return salobj.TestCsc( self.next_index(), @@ -789,7 +790,7 @@ async def fail_timeout(data: salobj.BaseMsgType) -> None: async def test_controller_command_callback_canceled(self) -> None: """Test exception raised by remote command when controller command - callback is cancelled (raises asyncio.CancelledError). + callback is canceled (raises asyncio.CancelledError). """ async with self.make_csc(initial_state=salobj.State.ENABLED): @@ -807,7 +808,7 @@ async def check_controller_command_callback_failure( result_contains: str | None = None, ) -> None: """Check the exception raised by a remote command when the controller - controller command raises an exception or returns a failed ackcmd. + command raises an exception or returns a failed ackcmd. Parameters ---------- @@ -1039,7 +1040,7 @@ async def test_remote_command_set(self) -> None: Test that RemoteCommand.set and set_start both begin with a new sample for each call, rather than remembering anything from the previous - command. This is different than WriteTopic.set. + command. This is different from WriteTopic.set. """ async with salobj.Domain() as domain, salobj.SalInfo(domain=domain, name="Test", index=1) as salinfo: cmdreader = salobj.topics.ControllerCommand(salinfo=salinfo, name="setScalars") @@ -1118,7 +1119,7 @@ async def test_read_topic_not_ready(self) -> None: topic.get_oldest() with pytest.raises(RuntimeError): # Use a timeout of 0 because the exception - # should occur before the timeout is used + # should occur before the timeout is used, # and we cannot afford to wait -- the read loop might start. await topic.aget(timeout=0) with pytest.raises(RuntimeError): @@ -1476,7 +1477,7 @@ async def test_read_num_messages_read_timeout(self) -> None: await asyncio.wait_for(salinfo.start(), timeout=STD_TIMEOUT) # Write 99 messages and try to read them with a short timeout. - # This should timeout because the read loop has a timeout of 5s. + # This should time out because the read loop has a timeout of 5s. for i in range(num_messages - 1): await tel_writter.write() @@ -1487,7 +1488,7 @@ async def test_read_num_messages_read_timeout(self) -> None: await tel_reader.next(flush=False, timeout=consume_messages_timeout) # Write num_messages messages and try to read them with a - # short timeout. This should not timeout because the read + # short timeout. This should not time out because the read # loop only waits for num_messages. for i in range(num_messages): await tel_writter.write()