diff --git a/src/briefcase/cmdline.py b/src/briefcase/cmdline.py index 7daf43d97..9ad0a4e72 100644 --- a/src/briefcase/cmdline.py +++ b/src/briefcase/cmdline.py @@ -17,6 +17,7 @@ UpdateCommand, UpgradeCommand, ) +from briefcase.commands.base import split_passthrough from briefcase.platforms import get_output_formats, get_platforms from .exceptions import InvalidFormatError, NoCommandError, UnsupportedCommandError @@ -102,19 +103,19 @@ def parse_cmdline(args): def normalize(name): return {n.lower(): n for n in platforms.keys()}.get(name.lower(), name) + # argparse handles `--` specially, so make the passthrough args bypass the parser. + def parse_known_args(args): + args, passthough = split_passthrough(args) + options, extra = parser.parse_known_args(args) + if passthough: + extra += ["--"] + passthough + return options, extra + # Use parse_known_args to ensure any extra arguments can be ignored, # and parsed as part of subcommand handling. This will capture the # command, platform (filling a default if unspecified) and format # (with no value if unspecified). - options, extra = parser.parse_known_args(args) - - # parse_known_args() strips `--` if it is the first unknown argument. - # Put it back in so that passthrough handling is consistent. - try: - if args[1] == "--": - extra.insert(0, "--") - except IndexError: - pass + options, extra = parse_known_args(args) # If no command has been provided, display top-level help. if options.command is None: @@ -156,7 +157,7 @@ def normalize(name): # Re-parse the arguments, now that we know it is a command that makes use # of platform/output_format. - options, extra = parser.parse_known_args(args) + options, extra = parse_known_args(args) # Import the platform module platform_module = platforms[options.platform] diff --git a/src/briefcase/commands/base.py b/src/briefcase/commands/base.py index 5654474cb..59a1483bf 100644 --- a/src/briefcase/commands/base.py +++ b/src/briefcase/commands/base.py @@ -87,6 +87,15 @@ def full_options(state, options): return full +def split_passthrough(args): + try: + pos = args.index("--") + except ValueError: + return args, [] + else: + return args[:pos], args[pos + 1 :] + + class BaseCommand(ABC): cmd_line = "briefcase {command} {platform} {output_format}" supported_host_os = {"Darwin", "Linux", "Windows"} @@ -504,25 +513,17 @@ def parse_options(self, extra): self.add_default_options(parser) self.add_options(parser) - # If the command allows passthrough arguments, add the option, + # If the command allows passthrough arguments, add an option for the help, # then process the argument list to strip out the passthrough args. if self.allows_passthrough: parser.add_argument( "--", dest="passthrough", - metavar="passthrough arguments", + metavar="ARGS ...", required=False, help="Arguments to pass to the app", ) - - # If "--" is present in the argument list, strip off any - # arguments after that one, and store them for separate handling. - if "--" in extra: - pos = extra.index("--") - args, passthrough = extra[:pos], extra[pos + 1 :] - else: - passthrough = [] - args = extra + args, passthrough = split_passthrough(extra) else: args = extra diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index b9dc997b5..e9344ac7d 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -1,3 +1,4 @@ +import shlex import sys from unittest import mock @@ -12,7 +13,11 @@ UnsupportedCommandError, ) from briefcase.platforms.linux.appimage import LinuxAppImageCreateCommand -from briefcase.platforms.macOS.app import macOSAppCreateCommand, macOSAppPublishCommand +from briefcase.platforms.macOS.app import ( + macOSAppCreateCommand, + macOSAppPublishCommand, + macOSAppRunCommand, +) from briefcase.platforms.windows.app import WindowsAppCreateCommand @@ -102,12 +107,44 @@ def test_new_command(logger, console): assert options == {"template": None} -def test_dev_command(monkeypatch, logger, console): +# Common tests for dev and run commands. +def dev_run_parameters(command): + return [ + (f"{command} {args}", expected) + for args, expected in [ + ("", {}), + ("-r", {"update_requirements": True}), + ("--update-requirements", {"update_requirements": True}), + ("--test", {"test_mode": True}), + ("--test -r", {"test_mode": True, "update_requirements": True}), + ("--", {}), + ("-- ''", {"passthrough": [""]}), + ("-- --test", {"passthrough": ["--test"]}), + ("--test -- --test", {"test_mode": True, "passthrough": ["--test"]}), + ("--test -- -r", {"test_mode": True, "passthrough": ["-r"]}), + ("-r -- --test", {"update_requirements": True, "passthrough": ["--test"]}), + ("-- -y --no maybe", {"passthrough": ["-y", "--no", "maybe"]}), + ( + "--test -- -y --no maybe", + {"test_mode": True, "passthrough": ["-y", "--no", "maybe"]}, + ), + ] + ] + + +@pytest.mark.parametrize( + "cmdline, expected_options", + dev_run_parameters("dev") + + [ + ("dev --no-run", {"run_app": False}), + ], +) +def test_dev_command(monkeypatch, logger, console, cmdline, expected_options): """``briefcase dev`` returns the Dev command.""" # Pretend we're on macOS, regardless of where the tests run. monkeypatch.setattr(sys, "platform", "darwin") - cmd, options = do_cmdline_parse("dev".split(), logger, console) + cmd, options = do_cmdline_parse(shlex.split(cmdline), logger, console) assert isinstance(cmd, DevCommand) assert cmd.platform == "macOS" @@ -122,54 +159,43 @@ def test_dev_command(monkeypatch, logger, console): "run_app": True, "test_mode": False, "passthrough": [], + **expected_options, } -def test_dev_command_only_passthrough(monkeypatch, logger, console): - """``briefcase dev`` with only passthrough args returns the Dev command.""" +@pytest.mark.parametrize( + "cmdline, expected_options", + dev_run_parameters("run") + + [ + ("run -u", {"update": True}), + ("run --update", {"update": True}), + ("run --update-resources", {"update_resources": True}), + ("run --no-update", {"no_update": True}), + ], +) +def test_run_command(monkeypatch, logger, console, cmdline, expected_options): + """``briefcase run`` returns the Run command for the correct platform.""" # Pretend we're on macOS, regardless of where the tests run. monkeypatch.setattr(sys, "platform", "darwin") - cmd, options = do_cmdline_parse("dev -- -y --no maybe".split(), logger, console) + cmd, options = do_cmdline_parse(shlex.split(cmdline), logger, console) - assert isinstance(cmd, DevCommand) + assert isinstance(cmd, macOSAppRunCommand) assert cmd.platform == "macOS" - assert cmd.output_format is None + assert cmd.output_format == "app" assert cmd.input.enabled assert cmd.logger.verbosity == 1 assert cmd.logger is logger assert cmd.input is console assert options == { "appname": None, + "update": False, "update_requirements": False, - "run_app": True, + "update_resources": False, + "no_update": False, "test_mode": False, - "passthrough": ["-y", "--no", "maybe"], - } - - -def test_dev_command_passthrough(monkeypatch, logger, console): - """``briefcase dev`` with passthrough args returns the Dev command.""" - # Pretend we're on macOS, regardless of where the tests run. - monkeypatch.setattr(sys, "platform", "darwin") - - cmd, options = do_cmdline_parse( - "dev --test -- -y --no maybe".split(), logger, console - ) - - assert isinstance(cmd, DevCommand) - assert cmd.platform == "macOS" - assert cmd.output_format is None - assert cmd.input.enabled - assert cmd.logger.verbosity == 1 - assert cmd.logger is logger - assert cmd.input is console - assert options == { - "appname": None, - "update_requirements": False, - "run_app": True, - "test_mode": True, - "passthrough": ["-y", "--no", "maybe"], + "passthrough": [], + **expected_options, }