Skip to content

Commit a6b25bb

Browse files
authored
[CLI] Handle unrecognized arguments (#3076)
* All commands now reject unrecognized arguments * Undocumented `${{ run.args }}` for tasks and services is still supported but requires `--` pseudo-argument: > If you have positional arguments that must begin with `-` > and don’t look like negative numbers, you can insert > the pseudo-argument `'--'` which tells `parse_args()` > that everything after that is a positional argument ``` dstack apply --reuse -- --some=arg --some-option ^^ ``` Fixes: #3073
1 parent b9cc1ef commit a6b25bb

File tree

16 files changed

+96
-69
lines changed

16 files changed

+96
-69
lines changed

src/dstack/_internal/cli/commands/__init__.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
import argparse
22
import os
3+
import shlex
34
from abc import ABC, abstractmethod
4-
from typing import List, Optional
5+
from typing import ClassVar, Optional
56

67
from rich_argparse import RichHelpFormatter
78

89
from dstack._internal.cli.services.completion import ProjectNameCompleter
9-
from dstack._internal.cli.utils.common import configure_logging
10+
from dstack._internal.core.errors import CLIError
1011
from dstack.api import Client
1112

1213

1314
class BaseCommand(ABC):
14-
NAME: str = "name the command"
15-
DESCRIPTION: str = "describe the command"
16-
DEFAULT_HELP: bool = True
17-
ALIASES: Optional[List[str]] = None
15+
NAME: ClassVar[str] = "name the command"
16+
DESCRIPTION: ClassVar[str] = "describe the command"
17+
DEFAULT_HELP: ClassVar[bool] = True
18+
ALIASES: ClassVar[Optional[list[str]]] = None
19+
ACCEPT_EXTRA_ARGS: ClassVar[bool] = False
1820

1921
def __init__(self, parser: argparse.ArgumentParser):
2022
self._parser = parser
@@ -50,7 +52,8 @@ def _register(self):
5052

5153
@abstractmethod
5254
def _command(self, args: argparse.Namespace):
53-
pass
55+
if not self.ACCEPT_EXTRA_ARGS and args.extra_args:
56+
raise CLIError(f"Unrecognized arguments: {shlex.join(args.extra_args)}")
5457

5558

5659
class APIBaseCommand(BaseCommand):
@@ -65,5 +68,5 @@ def _register(self):
6568
).completer = ProjectNameCompleter() # type: ignore[attr-defined]
6669

6770
def _command(self, args: argparse.Namespace):
68-
configure_logging()
71+
super()._command(args)
6972
self.api = Client.from_config(project_name=args.project)

src/dstack/_internal/cli/commands/apply.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import argparse
2+
import shlex
23

34
from argcomplete import FilesCompleter # type: ignore[attr-defined]
45

@@ -19,6 +20,7 @@ class ApplyCommand(APIBaseCommand):
1920
NAME = "apply"
2021
DESCRIPTION = "Apply a configuration"
2122
DEFAULT_HELP = False
23+
ACCEPT_EXTRA_ARGS = True
2224

2325
def _register(self):
2426
super()._register()
@@ -84,13 +86,14 @@ def _command(self, args: argparse.Namespace):
8486
configurator_class = get_apply_configurator_class(configuration.type)
8587
configurator = configurator_class(api_client=self.api)
8688
configurator_parser = configurator.get_parser()
87-
known, unknown = configurator_parser.parse_known_args(args.unknown)
89+
configurator_args, unknown_args = configurator_parser.parse_known_args(args.extra_args)
90+
if unknown_args:
91+
raise CLIError(f"Unrecognized arguments: {shlex.join(unknown_args)}")
8892
configurator.apply_configuration(
8993
conf=configuration,
9094
configuration_path=configuration_path,
9195
command_args=args,
92-
configurator_args=known,
93-
unknown_args=unknown,
96+
configurator_args=configurator_args,
9497
)
9598
except KeyboardInterrupt:
9699
console.print("\nOperation interrupted by user. Exiting...")

src/dstack/_internal/cli/commands/completion.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import argparse
2+
13
import argcomplete
24

35
from dstack._internal.cli.commands import BaseCommand
@@ -15,6 +17,6 @@ def _register(self):
1517
choices=["bash", "zsh"],
1618
)
1719

18-
def _command(self, args):
20+
def _command(self, args: argparse.Namespace):
1921
super()._command(args)
2022
print(argcomplete.shellcode(["dstack"], shell=args.shell)) # type: ignore[attr-defined]

src/dstack/_internal/cli/commands/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def _register(self):
4040
)
4141

4242
def _command(self, args: argparse.Namespace):
43+
super()._command(args)
4344
config_manager = ConfigManager()
4445
if args.remove:
4546
config_manager.delete_project(args.project)

src/dstack/_internal/cli/commands/init.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
is_git_repo_url,
1010
register_init_repo_args,
1111
)
12-
from dstack._internal.cli.utils.common import configure_logging, confirm_ask, console, warn
12+
from dstack._internal.cli.utils.common import confirm_ask, console, warn
1313
from dstack._internal.core.errors import ConfigurationError
1414
from dstack._internal.core.models.repos.remote import RemoteRepo
1515
from dstack._internal.core.services.configs import ConfigManager
@@ -52,7 +52,7 @@ def _register(self):
5252
)
5353

5454
def _command(self, args: argparse.Namespace):
55-
configure_logging()
55+
super()._command(args)
5656

5757
repo_path: Optional[Path] = None
5858
repo_url: Optional[str] = None

src/dstack/_internal/cli/commands/offer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def _command(self, args: argparse.Namespace):
9999
conf = TaskConfiguration(commands=[":"])
100100

101101
configurator = OfferConfigurator(api_client=self.api)
102-
configurator.apply_args(conf, args, [])
102+
configurator.apply_args(conf, args)
103103
profile = load_profile(Path.cwd(), profile_name=args.profile)
104104

105105
run_spec = RunSpec(

src/dstack/_internal/cli/commands/project.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ def _register(self):
6767
set_default_parser.set_defaults(subfunc=self._set_default)
6868

6969
def _command(self, args: argparse.Namespace):
70+
super()._command(args)
7071
if not hasattr(args, "subfunc"):
7172
args.subfunc = self._list
7273
args.subfunc(args)

src/dstack/_internal/cli/commands/server.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import argparse
12
import os
2-
from argparse import Namespace
33

44
from dstack._internal import settings
55
from dstack._internal.cli.commands import BaseCommand
@@ -53,7 +53,7 @@ def _register(self):
5353
)
5454
self._parser.add_argument("--token", type=str, help="The admin user token")
5555

56-
def _command(self, args: Namespace):
56+
def _command(self, args: argparse.Namespace):
5757
super()._command(args)
5858

5959
if not UVICORN_INSTALLED:

src/dstack/_internal/cli/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def main():
8383
argcomplete.autocomplete(parser, always_complete_options=False)
8484

8585
args, unknown_args = parser.parse_known_args()
86-
args.unknown = unknown_args
86+
args.extra_args = unknown_args
8787

8888
try:
8989
check_for_updates()

src/dstack/_internal/cli/services/configurators/base.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import argparse
22
import os
33
from abc import ABC, abstractmethod
4-
from typing import Generic, List, TypeVar, Union, cast
4+
from typing import ClassVar, Generic, List, TypeVar, Union, cast
55

66
from dstack._internal.cli.services.args import env_var
77
from dstack._internal.core.errors import ConfigurationError
@@ -18,7 +18,7 @@
1818

1919

2020
class BaseApplyConfigurator(ABC, Generic[ApplyConfigurationT]):
21-
TYPE: ApplyConfigurationType
21+
TYPE: ClassVar[ApplyConfigurationType]
2222

2323
def __init__(self, api_client: Client):
2424
self.api = api_client
@@ -30,7 +30,6 @@ def apply_configuration(
3030
configuration_path: str,
3131
command_args: argparse.Namespace,
3232
configurator_args: argparse.Namespace,
33-
unknown_args: List[str],
3433
):
3534
"""
3635
Implements `dstack apply` for a given configuration type.
@@ -40,7 +39,6 @@ def apply_configuration(
4039
configuration_path: The path to the configuration file.
4140
command_args: The args parsed by `dstack apply`.
4241
configurator_args: The known args parsed by `cls.get_parser()`.
43-
unknown_args: The unknown args after parsing by `cls.get_parser()`.
4442
"""
4543
pass
4644

0 commit comments

Comments
 (0)