diff --git a/pyproject.toml b/pyproject.toml index bc83be17ec..7f0845c3c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,7 +94,7 @@ target-version = "py310" line-length = 99 [tool.ruff.lint] -select = ["E", "F", "I", "Q", "W", "PGH", "FLY", "S113"] +select = ["E", "F", "I", "Q", "W", "PGH", "FLY", "S113", "PLR1722"] ignore = [ "E501", "E712", diff --git a/scripts/docs/gen_cli_reference.py b/scripts/docs/gen_cli_reference.py index 86ddae329b..d78263b87b 100644 --- a/scripts/docs/gen_cli_reference.py +++ b/scripts/docs/gen_cli_reference.py @@ -10,6 +10,7 @@ import re import shlex import subprocess +import sys from fnmatch import fnmatch from functools import cache @@ -58,7 +59,7 @@ def process_file(file: File): def main(): if os.environ.get(DISABLE_ENV): logger.warning("CLI reference generation is disabled") - exit() + sys.exit() logger.info("Generating CLI reference...") # Sequential processing take > 10s diff --git a/scripts/docs/gen_openapi_reference.py b/scripts/docs/gen_openapi_reference.py index 9998e1b7be..e054de0f6b 100644 --- a/scripts/docs/gen_openapi_reference.py +++ b/scripts/docs/gen_openapi_reference.py @@ -6,6 +6,7 @@ import logging import os import re +import sys from pathlib import Path from typing import Any @@ -31,7 +32,7 @@ if os.environ.get(disable_env): logger.warning("OpenAPI reference generation is disabled") - exit(0) + sys.exit(0) def _write_tag_references(tags: list[str]) -> None: diff --git a/scripts/docs/gen_rest_plugin_spec_reference.py b/scripts/docs/gen_rest_plugin_spec_reference.py index 8c89a84596..68633b1a97 100644 --- a/scripts/docs/gen_rest_plugin_spec_reference.py +++ b/scripts/docs/gen_rest_plugin_spec_reference.py @@ -5,6 +5,7 @@ import json import logging import os +import sys from pathlib import Path from dstack._internal.settings import DSTACK_VERSION @@ -13,7 +14,7 @@ disable_env = "DSTACK_DOCS_DISABLE_REST_PLUGIN_SPEC_REFERENCE" if os.environ.get(disable_env): logger.warning("REST plugin spec reference generation is disabled") - exit(0) + sys.exit(0) try: from example_plugin_server.main import app @@ -23,7 +24,7 @@ " The REST Plugin API won't be generated." " Run 'uv pip install examples/plugins/example_plugin_server' to install 'example_plugin_server'." ) - exit(0) + sys.exit(0) app.title = "REST Plugin OpenAPI Spec" app.servers = [ diff --git a/src/dstack/_internal/cli/commands/apply.py b/src/dstack/_internal/cli/commands/apply.py index c10f54f33e..7e37522a8e 100644 --- a/src/dstack/_internal/cli/commands/apply.py +++ b/src/dstack/_internal/cli/commands/apply.py @@ -1,5 +1,6 @@ import argparse import shlex +import sys from argcomplete import FilesCompleter # type: ignore[attr-defined] @@ -103,4 +104,4 @@ def _command(self, args: argparse.Namespace): ) except KeyboardInterrupt: console.print("\nOperation interrupted by user. Exiting...") - exit(0) + sys.exit(0) diff --git a/src/dstack/_internal/cli/commands/attach.py b/src/dstack/_internal/cli/commands/attach.py index a22d63d37c..5543ea32e6 100644 --- a/src/dstack/_internal/cli/commands/attach.py +++ b/src/dstack/_internal/cli/commands/attach.py @@ -112,7 +112,7 @@ def _command(self, args: argparse.Namespace): f" Use [code]-p[/code] in [code]dstack attach[/code] to override the local" f" port mapping, e.g. [code]-p {e.port + 1}:{e.port}[/code].[/]" ) - exit(1) + sys.exit(1) if not attached: raise CLIError(f"Failed to attach to run {args.run_name}") _print_attached_message( @@ -141,7 +141,7 @@ def _command(self, args: argparse.Namespace): finally: run.detach() # TODO: Handle run resubmissions similar to dstack apply - exit(exit_code) + sys.exit(exit_code) def _print_finished_message_when_available(run: Run) -> None: diff --git a/src/dstack/_internal/cli/commands/fleet.py b/src/dstack/_internal/cli/commands/fleet.py index 001779fe1b..02cc88cc1d 100644 --- a/src/dstack/_internal/cli/commands/fleet.py +++ b/src/dstack/_internal/cli/commands/fleet.py @@ -1,4 +1,5 @@ import argparse +import sys import time from uuid import UUID @@ -120,14 +121,14 @@ def _delete(self, args: argparse.Namespace): "The [code]/[/] format is not supported for fleet names." " Can only delete fleets or instances owned by the current project" ) - exit(1) + sys.exit(1) name = args.name.name try: self.api.client.fleets.get(project_name=self.api.project, name=name) except ResourceNotExistsError: console.print(f"Fleet [code]{name}[/] does not exist") - exit(1) + sys.exit(1) if not args.instances: if not args.yes and not confirm_ask(f"Delete the fleet [code]{name}[/]?"): @@ -174,6 +175,6 @@ def _get(self, args: argparse.Namespace): ) except ResourceNotExistsError: error_console.print(f"Fleet [code]{args.name or args.id}[/] not found") - exit(1) + sys.exit(1) print(fleet.model_dump_json(indent=2)) diff --git a/src/dstack/_internal/cli/commands/gateway.py b/src/dstack/_internal/cli/commands/gateway.py index bd29431f20..be1d33de16 100644 --- a/src/dstack/_internal/cli/commands/gateway.py +++ b/src/dstack/_internal/cli/commands/gateway.py @@ -1,4 +1,5 @@ import argparse +import sys import time from rich.live import Live @@ -139,7 +140,7 @@ def _delete(self, args: argparse.Namespace): "The [code]/[/] format is not supported for gateway names." " Can only delete gateways owned by the current project" ) - exit(1) + sys.exit(1) name = args.name.name gateway = self.api.client.gateways.get(self.api.project, name) print_gateways_table([gateway], current_project=self.api.project) @@ -164,7 +165,7 @@ def _update(self, args: argparse.Namespace): " when [code]--domain[/] is passed." " Can only update gateways owned by the current project" ) - exit(1) + sys.exit(1) self.api.client.gateways.set_wildcard_domain( self.api.project, args.name.name, args.domain ) diff --git a/src/dstack/_internal/cli/commands/login.py b/src/dstack/_internal/cli/commands/login.py index 8431a76c61..c7b7463e12 100644 --- a/src/dstack/_internal/cli/commands/login.py +++ b/src/dstack/_internal/cli/commands/login.py @@ -121,7 +121,7 @@ def _prompt_url(self) -> str: ) except KeyboardInterrupt: console.print("\nCancelled by user") - raise SystemExit(1) + sys.exit(1) if url is None: raise CLIError("URL is required") return url @@ -137,7 +137,7 @@ def _prompt_provider(self, available_providers: list[str]) -> str: instruction="(↑↓ Enter)", ).ask() if selected_provider is None: - raise SystemExit(1) + sys.exit(1) return selected_provider def _configure_projects( diff --git a/src/dstack/_internal/cli/commands/preset.py b/src/dstack/_internal/cli/commands/preset.py index a28669493e..ea557aaba9 100644 --- a/src/dstack/_internal/cli/commands/preset.py +++ b/src/dstack/_internal/cli/commands/preset.py @@ -238,7 +238,7 @@ def _command(self, args: argparse.Namespace) -> None: args.subfunc(args) except KeyboardInterrupt: console.print("\nOperation interrupted by user. Exiting...") - exit(0) + sys.exit(0) def _reconcile(self) -> None: """Finalize any detached/orphaned session whose agent already completed, diff --git a/src/dstack/_internal/cli/commands/run.py b/src/dstack/_internal/cli/commands/run.py index 1310e1274f..730ce4aa26 100644 --- a/src/dstack/_internal/cli/commands/run.py +++ b/src/dstack/_internal/cli/commands/run.py @@ -1,4 +1,5 @@ import argparse +import sys from uuid import UUID from dstack._internal.cli.commands import APIBaseCommand @@ -63,6 +64,6 @@ def _get(self, args: argparse.Namespace): run = self.api.client.runs.get(project_name=self.api.project, run_name=args.name) except ResourceNotExistsError: error_console.print(f"Run [code]{args.name or args.id}[/] not found") - exit(1) + sys.exit(1) print(run.model_dump_json(indent=2)) diff --git a/src/dstack/_internal/cli/commands/volume.py b/src/dstack/_internal/cli/commands/volume.py index dea74d0b26..4ab8d5129d 100644 --- a/src/dstack/_internal/cli/commands/volume.py +++ b/src/dstack/_internal/cli/commands/volume.py @@ -1,4 +1,5 @@ import argparse +import sys import time from rich.live import Live @@ -95,7 +96,7 @@ def _delete(self, args: argparse.Namespace): self.api.client.volumes.get(project_name=self.api.project, name=args.name) except ResourceNotExistsError: console.print(f"Volume [code]{args.name}[/] does not exist") - exit(1) + sys.exit(1) if not args.yes and not confirm_ask(f"Delete the volume [code]{args.name}[/]?"): console.print("\nExiting...") @@ -112,6 +113,6 @@ def _get(self, args: argparse.Namespace): volume = self.api.client.volumes.get(project_name=self.api.project, name=args.name) except ResourceNotExistsError: error_console.print("Volume not found") - exit(1) + sys.exit(1) print(volume.model_dump_json(indent=2)) diff --git a/src/dstack/_internal/cli/main.py b/src/dstack/_internal/cli/main.py index 27abf68ad7..84ae1e2863 100644 --- a/src/dstack/_internal/cli/main.py +++ b/src/dstack/_internal/cli/main.py @@ -1,4 +1,5 @@ import argparse +import sys import argcomplete from rich.markup import escape @@ -100,7 +101,7 @@ def main(): except (ClientError, CLIError, ConfigurationError, SSHError) as e: error_console.print(f"[error]{escape(str(e))}[/]") logger.debug(e, exc_info=True) - exit(1) + sys.exit(1) if __name__ == "__main__": diff --git a/src/dstack/_internal/cli/services/configurators/fleet.py b/src/dstack/_internal/cli/services/configurators/fleet.py index 97ff872779..12ef066a48 100644 --- a/src/dstack/_internal/cli/services/configurators/fleet.py +++ b/src/dstack/_internal/cli/services/configurators/fleet.py @@ -1,4 +1,5 @@ import argparse +import sys import time from pathlib import Path from typing import Optional @@ -186,7 +187,7 @@ def _apply_plan(self, plan: FleetPlan, command_args: argparse.Namespace): console.print( "\n[error]Some instances failed. Check the table above for errors.[/]" ) - exit(1) + sys.exit(1) def _apply_plan_on_old_server(self, plan: FleetPlan, command_args: argparse.Namespace): action_message = "" @@ -281,7 +282,7 @@ def _apply_plan_on_old_server(self, plan: FleetPlan, command_args: argparse.Name ) if _fleet_has_failed_instances(fleet): console.print("\n[error]Some instances failed. Check the table above for errors.[/]") - exit(1) + sys.exit(1) def delete_configuration( self, @@ -291,7 +292,7 @@ def delete_configuration( ): if conf.name is None: console.print("[error]Configuration specifies no fleet to delete[/]") - exit(1) + sys.exit(1) try: self.api.client.fleets.get( @@ -300,7 +301,7 @@ def delete_configuration( ) except ResourceNotExistsError: console.print(f"Fleet [code]{conf.name}[/] does not exist") - exit(1) + sys.exit(1) if not command_args.yes and not confirm_ask(f"Delete the fleet [code]{conf.name}[/]?"): console.print("\nExiting...") diff --git a/src/dstack/_internal/cli/services/configurators/gateway.py b/src/dstack/_internal/cli/services/configurators/gateway.py index f7e216f0dd..48af0e70a6 100644 --- a/src/dstack/_internal/cli/services/configurators/gateway.py +++ b/src/dstack/_internal/cli/services/configurators/gateway.py @@ -1,4 +1,5 @@ import argparse +import sys import time from rich.table import Table @@ -160,7 +161,7 @@ def apply_configuration( console.print( f"\n[error]Provisioning failed. Error: {gateway.status_message or 'unknown'}[/]" ) - exit(1) + sys.exit(1) def delete_configuration( self, @@ -170,13 +171,13 @@ def delete_configuration( ): if conf.name is None: console.print("[error]Configuration specifies no gateway to delete[/]") - exit(1) + sys.exit(1) try: self.api.client.gateways.get(project_name=self.api.project, gateway_name=conf.name) except ResourceNotExistsError: console.print(f"Gateway [code]{conf.name}[/] does not exist") - exit(1) + sys.exit(1) if not command_args.yes and not confirm_ask(f"Delete the gateway [code]{conf.name}[/]?"): console.print("\nExiting...") diff --git a/src/dstack/_internal/cli/services/configurators/run.py b/src/dstack/_internal/cli/services/configurators/run.py index d3273ba9c6..756c8396fa 100644 --- a/src/dstack/_internal/cli/services/configurators/run.py +++ b/src/dstack/_internal/cli/services/configurators/run.py @@ -229,7 +229,7 @@ def apply_plan( f" Use [code]-p[/code] in [code]dstack apply[/code] to override the local" f" port mapping, e.g. [code]-p {e.port + 1}:{e.port}[/code].[/]" ) - exit(1) + sys.exit(1) if command_args.detach: detach_message = f"Run [code]{run.name}[/] submitted, detaching..." @@ -286,14 +286,14 @@ def apply_plan( f" Use [code]-p[/code] in [code]dstack attach[/code] to override the local" f" port mapping, e.g. [code]-p {e.port + 1}:{e.port}[/code].[/]" ) - exit(1) + sys.exit(1) if attached: for entry in run.logs(): sys.stdout.buffer.write(entry) sys.stdout.buffer.flush() else: console.print("[error]Failed to attach, exiting...[/]") - exit(1) + sys.exit(1) finally: run.detach() @@ -308,14 +308,14 @@ def apply_plan( break if run.status.is_finished(): print_finished_message(run) - exit(get_run_exit_code(run)) + sys.exit(get_run_exit_code(run)) time.sleep(1) if not reattach: console.print( "[error]Lost run connection. Timed out waiting for run final status." " Check `dstack ps` to see if it's done or failed." ) - exit(1) + sys.exit(1) except KeyboardInterrupt: try: if command_args.yes or not confirm_ask( @@ -348,7 +348,7 @@ def delete_configuration( ): if conf.name is None: console.print("[error]Configuration specifies no run to delete[/]") - exit(1) + sys.exit(1) try: self.api.client.runs.get( project_name=self.api.project, @@ -356,7 +356,7 @@ def delete_configuration( ) except ResourceNotExistsError: console.print(f"Run [code]{conf.name}[/] does not exist") - exit(1) + sys.exit(1) if not command_args.yes and not confirm_ask(f"Delete the run [code]{conf.name}[/]?"): console.print("\nExiting...") return diff --git a/src/dstack/_internal/cli/services/configurators/volume.py b/src/dstack/_internal/cli/services/configurators/volume.py index f8e2dea334..55b23bafa6 100644 --- a/src/dstack/_internal/cli/services/configurators/volume.py +++ b/src/dstack/_internal/cli/services/configurators/volume.py @@ -1,4 +1,5 @@ import argparse +import sys import time from rich.table import Table @@ -127,7 +128,7 @@ def apply_configuration( console.print( f"\n[error]Provisioning failed. Error: {volume.status_message or 'unknown'}[/]" ) - exit(1) + sys.exit(1) def delete_configuration( self, @@ -137,7 +138,7 @@ def delete_configuration( ): if conf.name is None: console.print("[error]Configuration specifies no volume to delete[/]") - exit(1) + sys.exit(1) try: self.api.client.volumes.get( @@ -146,7 +147,7 @@ def delete_configuration( ) except ResourceNotExistsError: console.print(f"Volume [code]{conf.name}[/] does not exist") - exit(1) + sys.exit(1) if not command_args.yes and not confirm_ask(f"Delete the volume [code]{conf.name}[/]?"): console.print("\nExiting...") diff --git a/src/dstack/_internal/cli/utils/common.py b/src/dstack/_internal/cli/utils/common.py index 0fa3dd9532..71a76c846d 100644 --- a/src/dstack/_internal/cli/utils/common.py +++ b/src/dstack/_internal/cli/utils/common.py @@ -143,7 +143,7 @@ def confirm_ask(prompt, **kwargs) -> bool: return Confirm.ask(prompt=prompt, **kwargs) except KeyboardInterrupt: console.print("\nCancelled by user") - raise SystemExit(1) + sys.exit(1) def add_row_from_dict(table: Table, data: dict[str, Any], **kwargs): diff --git a/src/dstack/_internal/core/models/repos/remote.py b/src/dstack/_internal/core/models/repos/remote.py index 917969e7c5..f51eb85e07 100644 --- a/src/dstack/_internal/core/models/repos/remote.py +++ b/src/dstack/_internal/core/models/repos/remote.py @@ -1,6 +1,7 @@ import io import re import subprocess +import sys import time from dataclasses import dataclass from typing import Annotated, BinaryIO, Callable, Dict, Optional, Union, cast @@ -384,4 +385,4 @@ def _repo_diff_verbose(repo: git.Repo, repo_hash: str, warning_time: float = 5) return collector.get() except KeyboardInterrupt: print("\nAborted") - exit(1) + sys.exit(1)