From 994f4ded41b7cfa563a3481c0c7215e500623f52 Mon Sep 17 00:00:00 2001 From: Ed Nutting Date: Wed, 14 Jan 2026 15:21:11 +0000 Subject: [PATCH 1/8] Fix: Launching Tiers was missing the interval argument --- gator/launch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gator/launch.py b/gator/launch.py index 76fdbc4..cf5b099 100644 --- a/gator/launch.py +++ b/gator/launch.py @@ -105,6 +105,7 @@ async def launch( client=client, logger=logger, tracking=tracking, + interval=interval, quiet=quiet and not all_msg, all_msg=all_msg, heartbeat_cb=heartbeat_cb, From 50bc906194d70e941dc9bd86cf6d43c08fc475d7 Mon Sep 17 00:00:00 2001 From: Ed Nutting Date: Wed, 14 Jan 2026 16:16:39 +0000 Subject: [PATCH 2/8] Fix #23: Divide the launch modes into default vs internal Default is to assume the user is calling Gator and therefore to treat a single Job as though it is to be launched as per a normal JobGroup/JobArray, i.e. via the scheduler. Internal mode tells Gator it's executing as a child on the actual runner instance, and therefore to expect a single job to run locally regardless of scheduler options. --- gator/__main__.py | 9 ++++++ gator/launch.py | 59 +++++++++++++++++++++++++++------------ gator/scheduler/common.py | 2 +- 3 files changed, 51 insertions(+), 19 deletions(-) diff --git a/gator/__main__.py b/gator/__main__.py index 94f27f3..54838e8 100644 --- a/gator/__main__.py +++ b/gator/__main__.py @@ -78,6 +78,13 @@ default=0, help="Maximum number of critical messages before failure", ) +@click.option( + "--internal", + type=bool, + default=False, + is_flag=True, + help="Indicates this instance of Gator is running as an internal child job.", +) @click.argument("spec", type=click.Path(exists=True), required=False) def main( ident: str, @@ -94,6 +101,7 @@ def main( limit_warning: Optional[int], limit_error: int, limit_critical: int, + internal: bool, spec: str, ) -> None: # Determine a tracking directory @@ -139,6 +147,7 @@ def main( error=limit_error, critical=limit_critical, ), + internal=internal ) ) if not summary.passed: diff --git a/gator/launch.py b/gator/launch.py index cf5b099..e4e8fa9 100644 --- a/gator/launch.py +++ b/gator/launch.py @@ -52,6 +52,7 @@ async def launch( sched_opts: Optional[Dict[str, str]] = None, glyph: Optional[str] = None, limits: Optional[MessageLimits] = None, + internal: bool = False, ) -> Summary: # Glyph only used when progress bar visible del glyph @@ -98,9 +99,14 @@ async def launch( spec.ident = ident # Check the spec object spec.check() - # If a JobArray or JobGroup is provided, launch a tier - if isinstance(spec, (JobArray, JobGroup)): - top = Tier( + # If this is an internal executor instance, we expect to have been given + # only a single job to execute and we launch a wrapper to run it. + if internal: + if not isinstance(spec, Job): + raise Exception("Internal instances may only be given one job to run.") + + # Launch a wrapper to actually run the job on the current machine + top = Wrapper( spec=spec, client=client, logger=logger, @@ -109,24 +115,41 @@ async def launch( quiet=quiet and not all_msg, all_msg=all_msg, heartbeat_cb=heartbeat_cb, - scheduler=scheduler, - sched_opts=sched_opts, - limits=limits, - ) - # If a Job is provided, launch a wrapper - elif isinstance(spec, Job): - top = Wrapper( - spec=spec, - client=client, - logger=logger, - tracking=tracking, - interval=interval, - quiet=quiet and not all_msg, limits=limits, ) - # Unsupported forms else: - raise Exception(f"Unsupported specification object of type {type(spec).__name__}") + # If a JobArray, JobGroup or Job is provided, launch a tier + if isinstance(spec, JobArray | JobGroup): + top = Tier( + spec=spec, + client=client, + logger=logger, + tracking=tracking, + interval=interval, + quiet=quiet and not all_msg, + all_msg=all_msg, + heartbeat_cb=heartbeat_cb, + scheduler=scheduler, + sched_opts=sched_opts, + limits=limits, + ) + elif isinstance(spec, Job): + top = Tier( + spec=JobArray(jobs=[spec]), + client=client, + logger=logger, + tracking=tracking, + interval=interval, + quiet=quiet and not all_msg, + all_msg=all_msg, + heartbeat_cb=heartbeat_cb, + scheduler=scheduler, + sched_opts=sched_opts, + limits=limits, + ) + # Unsupported forms + else: + raise Exception(f"Unsupported specification object of type {type(spec).__name__}") # Setup signal handler to capture CTRL+C events def _handler(sig: signal, evt_loop: asyncio.BaseEventLoop, top: Union[Tier, Wrapper]): diff --git a/gator/scheduler/common.py b/gator/scheduler/common.py index c3d378c..fd4dd9e 100644 --- a/gator/scheduler/common.py +++ b/gator/scheduler/common.py @@ -106,7 +106,7 @@ def create_command(self, child: Child, options: Optional[Dict[str, str]] = None) return list( itertools.chain( self.base_command, - ["--id", child.ident, "--tracking", child.tracking.as_posix()], + ["--internal", "--id", child.ident, "--tracking", child.tracking.as_posix()], *(["--sched-arg", f"{k}={v}"] for k, v in full_opts.items()), ) ) From f20fcc45a8cefab9e6bd4df655b956d1224085db Mon Sep 17 00:00:00 2001 From: Ed Nutting Date: Wed, 14 Jan 2026 19:18:55 +0000 Subject: [PATCH 3/8] Fix regression for nested JobArray and JobGroup specs. I hadn't appreciated that Gator invokes itself for nested JobArray and JobGroup specs. This seems like an odd choice as it might result in the JobArray/JobGroup being dispatched onto individual Slurm nodes and then further distribution is restricted to just that slurm node. May need further investigation/consideration. --- gator/launch.py | 36 ++++++++++++++++++----------------- tests/test_local_scheduler.py | 1 + 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/gator/launch.py b/gator/launch.py index e4e8fa9..9858cf2 100644 --- a/gator/launch.py +++ b/gator/launch.py @@ -99,14 +99,11 @@ async def launch( spec.ident = ident # Check the spec object spec.check() - # If this is an internal executor instance, we expect to have been given - # only a single job to execute and we launch a wrapper to run it. - if internal: - if not isinstance(spec, Job): - raise Exception("Internal instances may only be given one job to run.") - # Launch a wrapper to actually run the job on the current machine - top = Wrapper( + # Whether or not this is an `internal` instance, if we've been given an + # array or group to execute, we must use the scheduler to run the tasks. + if isinstance(spec, JobArray | JobGroup): + top = Tier( spec=spec, client=client, logger=logger, @@ -115,12 +112,19 @@ async def launch( quiet=quiet and not all_msg, all_msg=all_msg, heartbeat_cb=heartbeat_cb, + scheduler=scheduler, + sched_opts=sched_opts, limits=limits, ) - else: - # If a JobArray, JobGroup or Job is provided, launch a tier - if isinstance(spec, JobArray | JobGroup): - top = Tier( + elif isinstance(spec, Job): + # If we have been given only a single job to execute, and we're an + # internal instance, then we launch a wrapper to run it. + # + # Otherwise, the user must've called Gator directly with just a single + # job in the top-level of the spec, but we still want to use the + # scheduler so we wrap it in a dummy Tier with a dummy JobArray. + if internal: + top = Wrapper( spec=spec, client=client, logger=logger, @@ -129,11 +133,9 @@ async def launch( quiet=quiet and not all_msg, all_msg=all_msg, heartbeat_cb=heartbeat_cb, - scheduler=scheduler, - sched_opts=sched_opts, limits=limits, ) - elif isinstance(spec, Job): + else: top = Tier( spec=JobArray(jobs=[spec]), client=client, @@ -147,9 +149,9 @@ async def launch( sched_opts=sched_opts, limits=limits, ) - # Unsupported forms - else: - raise Exception(f"Unsupported specification object of type {type(spec).__name__}") + # Unsupported forms + else: + raise Exception(f"Unsupported specification object of type {type(spec).__name__}") # Setup signal handler to capture CTRL+C events def _handler(sig: signal, evt_loop: asyncio.BaseEventLoop, top: Union[Tier, Wrapper]): diff --git a/tests/test_local_scheduler.py b/tests/test_local_scheduler.py index 797f946..93f6bb2 100644 --- a/tests/test_local_scheduler.py +++ b/tests/test_local_scheduler.py @@ -89,6 +89,7 @@ def _create_proc(*_args, **_kwargs): call( f"python3 -m gator --limit-error=0 --limit-critical=0" " --parent test:1234 --interval 7 --scheduler local --all-msg " + "--internal " f"--id T{x} --tracking {(tmp_path / f'T{x}').as_posix()}" " --sched-arg concurrency=1", stdin=subprocess.DEVNULL, From 38c6761760ee85f1ad62a684a487497e0597ce1d Mon Sep 17 00:00:00 2001 From: Ed Nutting Date: Wed, 14 Jan 2026 22:42:22 +0000 Subject: [PATCH 4/8] Tidy up the launch logic --- gator/launch.py | 85 +++++++++++++++++++++++-------------------------- 1 file changed, 39 insertions(+), 46 deletions(-) diff --git a/gator/launch.py b/gator/launch.py index 9858cf2..68f16a4 100644 --- a/gator/launch.py +++ b/gator/launch.py @@ -20,7 +20,7 @@ import socket from functools import partial from pathlib import Path -from typing import Dict, Optional, Type, Union +from typing import Dict, Optional, Type, Union, cast from rich.console import Console @@ -32,6 +32,7 @@ from .hub.api import HubAPI from .scheduler import LocalScheduler from .specs import Job, JobArray, JobGroup, Spec +from .specs.common import SpecBase from .tier import Tier from .wrapper import Wrapper @@ -82,29 +83,57 @@ async def launch( ) # Work out where the spec is coming from # - From server (nested call) + parsed_spec: SpecBase if spec is None and client.linked and ident: raw_spec = await client.spec(ident=ident) - spec = Spec.parse_str(raw_spec.get("spec", "")) + parsed_spec = Spec.parse_str(raw_spec.get("spec", "")) # - Passed in directly (when used as a library elif spec is not None and isinstance(spec, (Job, JobArray, JobGroup)): - pass + parsed_spec = cast(SpecBase, spec) # - Passed as a file path elif spec is not None and isinstance(spec, (Path, str)): - spec = Spec.parse(Path(spec)) + parsed_spec = Spec.parse(Path(spec)) # - Unknown else: raise Exception("No specification file provided and no parent server to query") + + # Hint for the type checker and a safety during debugging + assert isinstance(parsed_spec, Job | JobArray | JobGroup), \ + ("Expected specification to be a Job, JobArray or JobGroup, received " + f"{type(parsed_spec).__name__}." + ) + # If an ident has been provided, override whatever the spec gives if ident is not None: - spec.ident = ident + parsed_spec.ident = ident + # Check the spec object - spec.check() + parsed_spec.check() - # Whether or not this is an `internal` instance, if we've been given an - # array or group to execute, we must use the scheduler to run the tasks. - if isinstance(spec, JobArray | JobGroup): + # When user launches a single job, wrap it up in a JobArray so we can + # launch it via a common mechanism (which will ensure this job launches via + # the specified scheduler) + if isinstance(parsed_spec, Job) and not internal: + parsed_spec = JobArray(jobs=[parsed_spec]) + + if isinstance(parsed_spec, Job) and internal: + # Internal single job - launch via the wrapper on current machine + # as this is the executor instance. I.e. don't use the scheduler + top = Wrapper( + spec=parsed_spec, + client=client, + logger=logger, + tracking=tracking, + interval=interval, + quiet=quiet and not all_msg, + all_msg=all_msg, + heartbeat_cb=heartbeat_cb, + limits=limits, + ) + else: + # Non-internal single job or a multi-task job - launch via the scheduler top = Tier( - spec=spec, + spec=parsed_spec, client=client, logger=logger, tracking=tracking, @@ -116,42 +145,6 @@ async def launch( sched_opts=sched_opts, limits=limits, ) - elif isinstance(spec, Job): - # If we have been given only a single job to execute, and we're an - # internal instance, then we launch a wrapper to run it. - # - # Otherwise, the user must've called Gator directly with just a single - # job in the top-level of the spec, but we still want to use the - # scheduler so we wrap it in a dummy Tier with a dummy JobArray. - if internal: - top = Wrapper( - spec=spec, - client=client, - logger=logger, - tracking=tracking, - interval=interval, - quiet=quiet and not all_msg, - all_msg=all_msg, - heartbeat_cb=heartbeat_cb, - limits=limits, - ) - else: - top = Tier( - spec=JobArray(jobs=[spec]), - client=client, - logger=logger, - tracking=tracking, - interval=interval, - quiet=quiet and not all_msg, - all_msg=all_msg, - heartbeat_cb=heartbeat_cb, - scheduler=scheduler, - sched_opts=sched_opts, - limits=limits, - ) - # Unsupported forms - else: - raise Exception(f"Unsupported specification object of type {type(spec).__name__}") # Setup signal handler to capture CTRL+C events def _handler(sig: signal, evt_loop: asyncio.BaseEventLoop, top: Union[Tier, Wrapper]): From ab54dd59a5e42a31187e6b4905ed0d24351793fe Mon Sep 17 00:00:00 2001 From: Ed Nutting Date: Wed, 14 Jan 2026 22:47:41 +0000 Subject: [PATCH 5/8] Add tests for handling of a single Job with/without `internal` mode --- gator/launch.py | 2 +- tests/test_local_scheduler.py | 32 ++++++++++++++++++++++++++++++++ tests/test_wrapper.py | 27 +++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/gator/launch.py b/gator/launch.py index 68f16a4..a201ce5 100644 --- a/gator/launch.py +++ b/gator/launch.py @@ -41,7 +41,7 @@ async def launch( ident: Optional[str] = None, hub: Optional[str] = None, parent: Optional[str] = None, - spec: Optional[Union[Spec, Path]] = None, + spec: Optional[Union[SpecBase, Spec, Path]] = None, tracking: Optional[Path] = None, interval: int = 5, quiet: bool = False, diff --git a/tests/test_local_scheduler.py b/tests/test_local_scheduler.py index 93f6bb2..1733294 100644 --- a/tests/test_local_scheduler.py +++ b/tests/test_local_scheduler.py @@ -105,3 +105,35 @@ def _create_proc(*_args, **_kwargs): await sched.wait_for_all() # Check all monitors were fired up as_mon.assert_has_calls([call(f"T{x}", y) for x, y in zip(range(10), procs)]) + + async def test_local_scheduler_default_launch(self, mocker, tmp_path): + """Check that launch() without `internal` flag uses Tier/scheduler for a single Job""" + from gator.launch import launch + from gator.specs import JobArray + + # Patch Console to avoid output during test + mocker.patch("gator.launch.Console") + # Mock the Tier class to verify it's instantiated + mk_tier_cls = mocker.patch("gator.launch.Tier") + mk_tier = MagicMock() + mk_tier.launch = AsyncMock() + mk_tier.summarise = AsyncMock() + mk_tier.is_root = True + mk_tier_cls.return_value = mk_tier + # Define a job specification + job = Job("test_scheduler", cwd=tmp_path.as_posix(), command="echo", args=["scheduler"]) + # Call launch without internal flag (defaults to False) + trk_dir = tmp_path / "tracking" + await launch(spec=job, tracking=trk_dir, scheduler=LocalScheduler) + # Verify Tier was instantiated + mk_tier_cls.assert_called_once() + call_kwargs = mk_tier_cls.call_args.kwargs + # The single Job should be wrapped in a JobArray + assert isinstance(call_kwargs["spec"], JobArray) + assert len(call_kwargs["spec"].jobs) == 1 + assert call_kwargs["spec"].jobs[0] is job + assert call_kwargs["tracking"] == trk_dir + assert call_kwargs["scheduler"] is LocalScheduler + # Verify Tier.launch() was called + mk_tier.launch.assert_called_once() + mk_tier.summarise.assert_called_once() diff --git a/tests/test_wrapper.py b/tests/test_wrapper.py index f19d95d..ab1b111 100644 --- a/tests/test_wrapper.py +++ b/tests/test_wrapper.py @@ -371,3 +371,30 @@ async def test_wrapper_metric(self, tmp_path, mocker) -> None: await wrp.stop() # Wait for task to complete await t_wrp + + async def test_wrapper_internal_launch(self, tmp_path, mocker) -> None: + """Check that launch() with `internal=True` uses Wrapper for a single Job""" + from gator.launch import launch + + # Patch Console to avoid output during test + mocker.patch("gator.launch.Console") + # Mock the Wrapper class to verify it's instantiated + mk_wrapper_cls = mocker.patch("gator.launch.Wrapper") + mk_wrapper = MagicMock() + mk_wrapper.launch = AsyncMock() + mk_wrapper.summarise = AsyncMock() + mk_wrapper.is_root = True + mk_wrapper_cls.return_value = mk_wrapper + # Define a job specification + job = Job("test_internal", cwd=tmp_path.as_posix(), command="echo", args=["internal"]) + # Call launch with internal=True + trk_dir = tmp_path / "tracking" + await launch(spec=job, tracking=trk_dir, internal=True) + # Verify Wrapper was instantiated with the Job (not wrapped in JobArray) + mk_wrapper_cls.assert_called_once() + call_kwargs = mk_wrapper_cls.call_args.kwargs + assert call_kwargs["spec"] is job + assert call_kwargs["tracking"] == trk_dir + # Verify Wrapper.launch() was called + mk_wrapper.launch.assert_called_once() + mk_wrapper.summarise.assert_called_once() From fcd941b6e290f2df063eb8b172cf28ee23c624e4 Mon Sep 17 00:00:00 2001 From: Ed Nutting Date: Wed, 14 Jan 2026 22:51:34 +0000 Subject: [PATCH 6/8] Test that JobArray and JobGroup are still launched via scheduler even under internal mode. --- tests/test_local_scheduler.py | 70 ++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/tests/test_local_scheduler.py b/tests/test_local_scheduler.py index 1733294..54af64c 100644 --- a/tests/test_local_scheduler.py +++ b/tests/test_local_scheduler.py @@ -23,7 +23,7 @@ from gator.common.logger import Logger from gator.common.ws_client import WebsocketClient from gator.scheduler import LocalScheduler -from gator.specs.jobs import Job +from gator.specs.jobs import Job, JobArray @pytest.mark.asyncio @@ -137,3 +137,71 @@ async def test_local_scheduler_default_launch(self, mocker, tmp_path): # Verify Tier.launch() was called mk_tier.launch.assert_called_once() mk_tier.summarise.assert_called_once() + + async def test_local_scheduler_jobarray_internal_launch(self, mocker, tmp_path): + """Check that launch() with JobArray and `internal=True` still uses Tier/scheduler""" + from gator.launch import launch + from gator.specs import JobArray + + # Patch Console to avoid output during test + mocker.patch("gator.launch.Console") + # Mock the Tier class to verify it's instantiated + mk_tier_cls = mocker.patch("gator.launch.Tier") + mk_tier = MagicMock() + mk_tier.launch = AsyncMock() + mk_tier.summarise = AsyncMock() + mk_tier.is_root = True + mk_tier_cls.return_value = mk_tier + # Define a JobArray specification + jobs: list[Job | JobArray | JobGroup] = [ + Job(f"test_job_{i}", cwd=tmp_path.as_posix(), command="echo", args=[f"job{i}"]) + for i in range(3) + ] + job_array = JobArray(jobs=jobs) + # Call launch with internal=True (should still use scheduler for multi-job specs) + trk_dir = tmp_path / "tracking" + await launch(spec=job_array, tracking=trk_dir, scheduler=LocalScheduler, internal=True) + # Verify Tier was instantiated (not Wrapper) + mk_tier_cls.assert_called_once() + call_kwargs = mk_tier_cls.call_args.kwargs + # The JobArray should be passed through unchanged + assert call_kwargs["spec"] is job_array + assert call_kwargs["tracking"] == trk_dir + assert call_kwargs["scheduler"] is LocalScheduler + # Verify Tier.launch() was called + mk_tier.launch.assert_called_once() + mk_tier.summarise.assert_called_once() + + async def test_local_scheduler_jobgroup_internal_launch(self, mocker, tmp_path): + """Check that launch() with JobGroup and `internal=True` still uses Tier/scheduler""" + from gator.launch import launch + from gator.specs import JobGroup + + # Patch Console to avoid output during test + mocker.patch("gator.launch.Console") + # Mock the Tier class to verify it's instantiated + mk_tier_cls = mocker.patch("gator.launch.Tier") + mk_tier = MagicMock() + mk_tier.launch = AsyncMock() + mk_tier.summarise = AsyncMock() + mk_tier.is_root = True + mk_tier_cls.return_value = mk_tier + # Define a JobGroup specification + jobs: list[Job | JobArray | JobGroup] = [ + Job(f"test_job_{i}", cwd=tmp_path.as_posix(), command="echo", args=[f"job{i}"]) + for i in range(3) + ] + job_group = JobGroup(jobs=jobs) + # Call launch with internal=True (should still use scheduler for multi-job specs) + trk_dir = tmp_path / "tracking" + await launch(spec=job_group, tracking=trk_dir, scheduler=LocalScheduler, internal=True) + # Verify Tier was instantiated (not Wrapper) + mk_tier_cls.assert_called_once() + call_kwargs = mk_tier_cls.call_args.kwargs + # The JobGroup should be passed through unchanged + assert call_kwargs["spec"] is job_group + assert call_kwargs["tracking"] == trk_dir + assert call_kwargs["scheduler"] is LocalScheduler + # Verify Tier.launch() was called + mk_tier.launch.assert_called_once() + mk_tier.summarise.assert_called_once() From 24db988f8bb17d6f059d535c668bbf8835b3ca38 Mon Sep 17 00:00:00 2001 From: Ed Nutting Date: Wed, 14 Jan 2026 22:56:24 +0000 Subject: [PATCH 7/8] Remove redundant logic; Fix typecheck --- gator/launch.py | 2 +- tests/test_local_scheduler.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gator/launch.py b/gator/launch.py index a201ce5..79861b2 100644 --- a/gator/launch.py +++ b/gator/launch.py @@ -116,7 +116,7 @@ async def launch( if isinstance(parsed_spec, Job) and not internal: parsed_spec = JobArray(jobs=[parsed_spec]) - if isinstance(parsed_spec, Job) and internal: + if isinstance(parsed_spec, Job): # Internal single job - launch via the wrapper on current machine # as this is the executor instance. I.e. don't use the scheduler top = Wrapper( diff --git a/tests/test_local_scheduler.py b/tests/test_local_scheduler.py index 54af64c..494c2c4 100644 --- a/tests/test_local_scheduler.py +++ b/tests/test_local_scheduler.py @@ -23,7 +23,7 @@ from gator.common.logger import Logger from gator.common.ws_client import WebsocketClient from gator.scheduler import LocalScheduler -from gator.specs.jobs import Job, JobArray +from gator.specs.jobs import Job @pytest.mark.asyncio @@ -141,7 +141,7 @@ async def test_local_scheduler_default_launch(self, mocker, tmp_path): async def test_local_scheduler_jobarray_internal_launch(self, mocker, tmp_path): """Check that launch() with JobArray and `internal=True` still uses Tier/scheduler""" from gator.launch import launch - from gator.specs import JobArray + from gator.specs import JobArray, JobGroup # Patch Console to avoid output during test mocker.patch("gator.launch.Console") @@ -175,7 +175,7 @@ async def test_local_scheduler_jobarray_internal_launch(self, mocker, tmp_path): async def test_local_scheduler_jobgroup_internal_launch(self, mocker, tmp_path): """Check that launch() with JobGroup and `internal=True` still uses Tier/scheduler""" from gator.launch import launch - from gator.specs import JobGroup + from gator.specs import JobArray, JobGroup # Patch Console to avoid output during test mocker.patch("gator.launch.Console") From 47816eecdaff2eb0bfa4bfd72323dbe0fb26df79 Mon Sep 17 00:00:00 2001 From: Ed Nutting Date: Thu, 15 Jan 2026 19:22:49 +0000 Subject: [PATCH 8/8] Apply suggestion from @Intuity Co-authored-by: Peter Birch --- gator/launch.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gator/launch.py b/gator/launch.py index 79861b2..3b766f4 100644 --- a/gator/launch.py +++ b/gator/launch.py @@ -98,10 +98,10 @@ async def launch( raise Exception("No specification file provided and no parent server to query") # Hint for the type checker and a safety during debugging - assert isinstance(parsed_spec, Job | JobArray | JobGroup), \ - ("Expected specification to be a Job, JobArray or JobGroup, received " - f"{type(parsed_spec).__name__}." - ) + assert isinstance(parsed_spec, Job | JobArray | JobGroup), ( + f"Expected specification to be a Job, JobArray or JobGroup, received " + f"{type(parsed_spec).__name__}." + ) # If an ident has been provided, override whatever the spec gives if ident is not None: