diff --git a/changes/1115.feature.rst b/changes/1115.feature.rst new file mode 100644 index 000000000..76c59fe7f --- /dev/null +++ b/changes/1115.feature.rst @@ -0,0 +1 @@ +The ``-C``/``--config`` option can now be used to override app settings from the command line. diff --git a/docs/reference/commands/index.rst b/docs/reference/commands/index.rst index 9579131bf..c3d8573a5 100644 --- a/docs/reference/commands/index.rst +++ b/docs/reference/commands/index.rst @@ -20,6 +20,31 @@ Common options The following options are available to all commands: +``-C `` / ``--config `` +--------------------------------------------- + +Override the value of an app's configuration in ``pyproject.toml`` with the provided +value. + +The key will be applied *after* (and will therefore take precedence over) any platform +or backend-specific configuration has been merged into the app's configuration. The key +must be a "top level" TOML key. The use of `dotted keys +`__ to define nested configuration structures is not +permitted. + +The value passed to the setting should be valid TOML. If the value being overridden is a +string, this means you must quote the value. This may require the use of escape +sequences at the command line to ensure the value provided to Briefcase by the shell +includes the quotes. + +For example, to override the template used by the create command, you can use ``-C +template=...``, but the value must be quoted:: + + briefcase create -C template=\"https://example.com/template\" + +The only app key that *cannot* be overridden with ``-C`` is ``app_name``, as it is used +to identify apps. + ``-h`` / ``--help`` ------------------- diff --git a/src/briefcase/__main__.py b/src/briefcase/__main__.py index a868c422d..703052fde 100644 --- a/src/briefcase/__main__.py +++ b/src/briefcase/__main__.py @@ -20,8 +20,11 @@ def main(): try: Command, extra_cmdline = parse_cmdline(sys.argv[1:]) command = Command(logger=logger, console=console) - options = command.parse_options(extra=extra_cmdline) - command.parse_config(Path.cwd() / "pyproject.toml") + options, overrides = command.parse_options(extra=extra_cmdline) + command.parse_config( + Path.cwd() / "pyproject.toml", + overrides=overrides, + ) command(**options) except HelpText as e: logger.info() diff --git a/src/briefcase/commands/base.py b/src/briefcase/commands/base.py index 9c511c18a..c776f957b 100644 --- a/src/briefcase/commands/base.py +++ b/src/briefcase/commands/base.py @@ -11,6 +11,7 @@ from abc import ABC, abstractmethod from argparse import RawDescriptionHelpFormatter from pathlib import Path +from typing import Any from cookiecutter import exceptions as cookiecutter_exceptions from cookiecutter.repository import is_repo_url @@ -103,6 +104,38 @@ def split_passthrough(args): return args[:pos], args[pos + 1 :] +def parse_config_overrides(config_overrides: list[str] | None) -> dict[str, Any]: + """Parse command line -C/--config option overrides. + + :param config_overrides: The values passed in as configuration overrides. Each value + *should* be a "key=" string. + :returns: A dictionary of app configuration keys to override and their new values. + :raises BriefcaseCommandError: if any of the values can't be parsed as valid TOML. + """ + overrides = {} + if config_overrides: + for override in config_overrides: + try: + # Do initial checks of the key being overridden. + # These catch cases that would be valid TOML, but would result + # in invalid app configurations. + key, _ = override.split("=", 1) + if "." in key: + raise BriefcaseConfigError( + "Can't override multi-level configuration keys." + ) + elif key == "app_name": + raise BriefcaseConfigError("The app name cannot be overridden.") + + # Now actually parse the value + overrides.update(tomllib.loads(override)) + except ValueError as e: + raise BriefcaseConfigError( + f"Unable to parse configuration override {override}" + ) from e + return overrides + + class BaseCommand(ABC): cmd_line = "briefcase {command} {platform} {output_format}" supported_host_os = {"Darwin", "Linux", "Windows"} @@ -614,7 +647,8 @@ def parse_options(self, extra): :param extra: the remaining command line arguments after the initial ArgumentParser runs over the command line. - :return: dictionary of parsed arguments for Command + :return: dictionary of parsed arguments for Command, and a dictionary of parsed + configuration overrides. """ default_format = getattr( get_platforms().get(self.platform), "DEFAULT_OUTPUT_FORMAT", None @@ -674,7 +708,10 @@ def parse_options(self, extra): self.logger.verbosity = options.pop("verbosity") self.logger.save_log = options.pop("save_log") - return options + # Parse the configuration overrides + overrides = parse_config_overrides(options.pop("config_overrides")) + + return options, overrides def clone_options(self, command): """Clone options from one command to this one. @@ -687,12 +724,20 @@ def add_default_options(self, parser): :param parser: a stub argparse parser for the command. """ + parser.add_argument( + "-C", + "--config", + dest="config_overrides", + action="append", + metavar="KEY=VALUE", + help="Override the value of the app configuration item KEY with VALUE", + ) parser.add_argument( "-v", "--verbosity", action="count", default=0, - help="Enable verbose logging. Use -vv and -vvv to increase logging verbosity.", + help="Enable verbose logging. Use -vv and -vvv to increase logging verbosity", ) parser.add_argument("-V", "--version", action="version", version=__version__) parser.add_argument( @@ -780,7 +825,7 @@ def add_options(self, parser): :param parser: a stub argparse parser for the command. """ - def parse_config(self, filename): + def parse_config(self, filename, overrides): try: with open(filename, "rb") as config_file: # Parse the content of the pyproject.toml file, extracting @@ -792,6 +837,8 @@ def parse_config(self, filename): output_format=self.output_format, ) + # Create the global config + global_config.update(overrides) self.global_config = create_config( klass=GlobalConfig, config=global_config, @@ -801,6 +848,7 @@ def parse_config(self, filename): for app_name, app_config in app_configs.items(): # Construct an AppConfig object with the final set of # configuration options for the app. + app_config.update(overrides) self.apps[app_name] = create_config( klass=AppConfig, config=app_config, diff --git a/src/briefcase/commands/create.py b/src/briefcase/commands/create.py index 44b938afc..4baa19caf 100644 --- a/src/briefcase/commands/create.py +++ b/src/briefcase/commands/create.py @@ -831,7 +831,11 @@ def verify_app_tools(self, app: AppConfig): super().verify_app_tools(app) NativeAppContext.verify(tools=self.tools, app=app) - def __call__(self, app: AppConfig | None = None, **options) -> dict | None: + def __call__( + self, + app: AppConfig | None = None, + **options, + ) -> dict | None: # Confirm host compatibility, that all required tools are available, # and that the app configuration is finalized. self.finalize(app) diff --git a/src/briefcase/commands/dev.py b/src/briefcase/commands/dev.py index 5380c2b16..ee1a30ffb 100644 --- a/src/briefcase/commands/dev.py +++ b/src/briefcase/commands/dev.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import os import subprocess import sys from pathlib import Path -from typing import List, Optional from briefcase.commands.run import RunAppMixin from briefcase.config import AppConfig @@ -112,7 +113,7 @@ def run_dev_app( app: AppConfig, env: dict, test_mode: bool, - passthrough: List[str], + passthrough: list[str], **options, ): """Run the app in the dev environment. @@ -174,11 +175,11 @@ def get_environment(self, app, test_mode: bool): def __call__( self, - appname: Optional[str] = None, - update_requirements: Optional[bool] = False, - run_app: Optional[bool] = True, - test_mode: Optional[bool] = False, - passthrough: Optional[List[str]] = None, + appname: str | None = None, + update_requirements: bool | None = False, + run_app: bool | None = True, + test_mode: bool | None = False, + passthrough: list[str] | None = None, **options, ): # Which app should we run? If there's only one defined diff --git a/src/briefcase/commands/new.py b/src/briefcase/commands/new.py index aa24ce22b..7979aa5bc 100644 --- a/src/briefcase/commands/new.py +++ b/src/briefcase/commands/new.py @@ -1,7 +1,8 @@ +from __future__ import annotations + import re import unicodedata from email.utils import parseaddr -from typing import Optional from urllib.parse import urlparse from packaging.version import Version @@ -75,7 +76,7 @@ def binary_path(self, app): """A placeholder; New command doesn't have a binary path.""" raise NotImplementedError() - def parse_config(self, filename): + def parse_config(self, filename, overrides): """There is no configuration when starting a new project; this implementation overrides the base so that no config is parsed.""" pass @@ -432,8 +433,8 @@ def build_app_context(self): def new_app( self, - template: Optional[str] = None, - template_branch: Optional[str] = None, + template: str | None = None, + template_branch: str | None = None, **options, ): """Ask questions to generate a new application, and generate a stub project from @@ -520,8 +521,8 @@ def verify_tools(self): def __call__( self, - template: Optional[str] = None, - template_branch: Optional[str] = None, + template: str | None = None, + template_branch: str | None = None, **options, ): # Confirm host compatibility, and that all required tools are available. diff --git a/src/briefcase/commands/open.py b/src/briefcase/commands/open.py index ec8f38906..5d7b34629 100644 --- a/src/briefcase/commands/open.py +++ b/src/briefcase/commands/open.py @@ -44,7 +44,11 @@ def open_app(self, app: AppConfig, **options): return state - def __call__(self, app: AppConfig | None = None, **options): + def __call__( + self, + app: AppConfig | None = None, + **options, + ): # Confirm host compatibility, that all required tools are available, # and that the app configuration is finalized. self.finalize(app) diff --git a/src/briefcase/commands/publish.py b/src/briefcase/commands/publish.py index 689089f8f..c710fdee0 100644 --- a/src/briefcase/commands/publish.py +++ b/src/briefcase/commands/publish.py @@ -55,7 +55,11 @@ def _publish_app(self, app: AppConfig, channel: str, **options) -> dict | None: return state - def __call__(self, channel=None, **options): + def __call__( + self, + channel: str | None = None, + **options, + ): # Confirm host compatibility, that all required tools are available, # and that all app configurations are finalized. self.finalize() diff --git a/src/briefcase/commands/upgrade.py b/src/briefcase/commands/upgrade.py index c130d3847..c432a0b22 100644 --- a/src/briefcase/commands/upgrade.py +++ b/src/briefcase/commands/upgrade.py @@ -1,6 +1,7 @@ +from __future__ import annotations + import sys from operator import attrgetter -from typing import List, Set, Type from briefcase.exceptions import ( BriefcaseCommandError, @@ -50,12 +51,12 @@ def add_options(self, parser): help="The Briefcase-managed tool to upgrade. If no tool is named, all tools will be upgraded.", ) - def get_tools_to_upgrade(self, tool_list: Set[str]) -> List[ManagedTool]: + def get_tools_to_upgrade(self, tool_list: set[str]) -> list[ManagedTool]: """Returns set of managed Tools that can be upgraded. Raises ``BriefcaseCommandError`` if user list contains any invalid tool names. """ - upgrade_list: set[Type[Tool]] + upgrade_list: set[type[Tool]] tools_to_upgrade: set[ManagedTool] = set() # Validate user tool list against tool registry @@ -94,7 +95,7 @@ def get_tools_to_upgrade(self, tool_list: Set[str]) -> List[ManagedTool]: return sorted(list(tools_to_upgrade), key=attrgetter("name")) - def __call__(self, tool_list: List[str], list_tools: bool = False, **options): + def __call__(self, tool_list: list[str], list_tools: bool = False, **options): """Perform tool upgrades or list tools qualifying for upgrade. :param tool_list: List of tool names from user to upgrade. diff --git a/src/briefcase/platforms/linux/appimage.py b/src/briefcase/platforms/linux/appimage.py index ceaa4c85f..09447cebc 100644 --- a/src/briefcase/platforms/linux/appimage.py +++ b/src/briefcase/platforms/linux/appimage.py @@ -70,9 +70,9 @@ def add_options(self, parser): def parse_options(self, extra): """Extract the use_docker option.""" - options = super().parse_options(extra) + options, overrides = super().parse_options(extra) self.use_docker = options.pop("use_docker") - return options + return options, overrides def clone_options(self, command): """Clone the use_docker option.""" diff --git a/src/briefcase/platforms/linux/system.py b/src/briefcase/platforms/linux/system.py index 12792e869..5b0615524 100644 --- a/src/briefcase/platforms/linux/system.py +++ b/src/briefcase/platforms/linux/system.py @@ -365,10 +365,10 @@ def add_options(self, parser): def parse_options(self, extra): """Extract the target_image option.""" - options = super().parse_options(extra) + options, overrides = super().parse_options(extra) self.target_image = options.pop("target") - return options + return options, overrides def clone_options(self, command): """Clone the target_image option.""" diff --git a/src/briefcase/platforms/windows/__init__.py b/src/briefcase/platforms/windows/__init__.py index dc2821702..deed7bb9e 100644 --- a/src/briefcase/platforms/windows/__init__.py +++ b/src/briefcase/platforms/windows/__init__.py @@ -229,9 +229,9 @@ def add_options(self, parser): def parse_options(self, extra): """Require the Windows SDK tool if an `identity` is specified for signing.""" - options = super().parse_options(extra=extra) + options, overrides = super().parse_options(extra=extra) self._windows_sdk_needed = options["identity"] is not None - return options + return options, overrides def sign_file( self, diff --git a/tests/commands/base/test_parse_config.py b/tests/commands/base/test_parse_config.py index 7f89c5a72..03fa549f6 100644 --- a/tests/commands/base/test_parse_config.py +++ b/tests/commands/base/test_parse_config.py @@ -9,7 +9,7 @@ def test_missing_config(base_command): """If the configuration file doesn't exit, raise an error.""" filename = base_command.base_path / "does_not_exist.toml" with pytest.raises(BriefcaseConfigError, match="Configuration file not found"): - base_command.parse_config(filename) + base_command.parse_config(filename, {}) def test_incomplete_global_config(base_command): @@ -32,7 +32,7 @@ def test_incomplete_global_config(base_command): BriefcaseConfigError, match=r"Global configuration is incomplete \(missing 'bundle', 'project_name'\)", ): - base_command.parse_config(filename) + base_command.parse_config(filename, {}) def test_incomplete_config(base_command): @@ -56,11 +56,11 @@ def test_incomplete_config(base_command): BriefcaseConfigError, match=r"Configuration for 'my-app' is incomplete \(missing 'sources'\)", ): - base_command.parse_config(filename) + base_command.parse_config(filename, {}) def test_parse_config(base_command): - """A well-formed configuration file can be augmented by the command line.""" + """A well-formed configuration file can turned into a set of configurations.""" filename = base_command.base_path / "pyproject.toml" create_file( filename, @@ -82,8 +82,8 @@ def test_parse_config(base_command): """, ) - # Parse the configuration - base_command.parse_config(filename) + # Parse the configuration, no overrides + base_command.parse_config(filename, {}) # There is a global configuration object assert repr(base_command.global_config) == "" @@ -113,3 +113,104 @@ def test_parse_config(base_command): assert base_command.apps["secondapp"].bundle == "org.beeware" assert base_command.apps["secondapp"].mystery == "sekrits" assert base_command.apps["secondapp"].extra == "something" + + +def test_parse_config_with_overrides(base_command): + """A well-formed configuration file can be augmented by the command line.""" + filename = base_command.base_path / "pyproject.toml" + create_file( + filename, + """ + [tool.briefcase] + project_name = "Sample project" + version = "1.2.3" + description = "A sample app" + bundle = "org.beeware" + mystery = 'default' + + [tool.briefcase.app.firstapp] + sources = ['src/firstapp'] + + [tool.briefcase.app.secondapp] + sources = ['src/secondapp'] + extra = 'something' + mystery = 'sekrits' + """, + ) + + # Parse the configuration, with overrides + base_command.parse_config( + filename, + { + "version": "2.3.4", + "custom": "something special", + "mystery": "overridden", + }, + ) + + # There is a global configuration object. It picks up the override values. + assert repr(base_command.global_config) == "" + assert base_command.global_config.project_name == "Sample project" + assert base_command.global_config.bundle == "org.beeware" + assert base_command.global_config.version == "2.3.4" + assert base_command.global_config.custom == "something special" + assert base_command.global_config.mystery == "overridden" + + # The first app will have all the base attributes required by an app, + # defined in the config file, with the values from the overrides taking precedence. + assert ( + repr(base_command.apps["firstapp"]) == "" + ) + assert base_command.apps["firstapp"].project_name == "Sample project" + assert base_command.apps["firstapp"].app_name == "firstapp" + assert base_command.apps["firstapp"].bundle == "org.beeware" + assert base_command.apps["firstapp"].version == "2.3.4" + assert base_command.apps["firstapp"].custom == "something special" + assert base_command.apps["firstapp"].mystery == "overridden" + assert not hasattr(base_command.apps["firstapp"], "extra") + + # The second app is much the same. The value for `mystery`` is overridden by the + # command line (superseding the app override); but the `extra` app-specific value + # is preserved. + assert ( + repr(base_command.apps["secondapp"]) + == "" + ) + assert base_command.apps["secondapp"].project_name == "Sample project" + assert base_command.apps["secondapp"].app_name == "secondapp" + assert base_command.apps["secondapp"].bundle == "org.beeware" + assert base_command.apps["secondapp"].version == "2.3.4" + assert base_command.apps["secondapp"].custom == "something special" + assert base_command.apps["secondapp"].mystery == "overridden" + assert base_command.apps["secondapp"].extra == "something" + + +def test_parse_config_with_invalid_override(base_command): + """If an override value doesn't pass validation, an exception is raised.""" + filename = base_command.base_path / "pyproject.toml" + create_file( + filename, + """ + [tool.briefcase] + project_name = "Sample project" + version = "1.2.3" + description = "A sample app" + bundle = "org.beeware" + mystery = 'default' + + [tool.briefcase.app.firstapp] + sources = ['src/firstapp'] + """, + ) + + # Parse the configuration, with overrides + with pytest.raises( + BriefcaseConfigError, + match=r"Version numbers must be PEP440 compliant", + ): + base_command.parse_config( + filename, + { + "version": "not-a-version-number", + }, + ) diff --git a/tests/commands/base/test_parse_config_overrides.py b/tests/commands/base/test_parse_config_overrides.py new file mode 100644 index 000000000..6c7cb1f3b --- /dev/null +++ b/tests/commands/base/test_parse_config_overrides.py @@ -0,0 +1,79 @@ +import pytest + +from briefcase.commands.base import parse_config_overrides +from briefcase.exceptions import BriefcaseConfigError + + +@pytest.mark.parametrize( + "overrides, values", + [ + # No content + (None, {}), + ([], {}), + # Boolean + (["key=true"], {"key": True}), + # Integers + (["key=42"], {"key": 42}), + (["key=-42"], {"key": -42}), + # Integers + (["key=42.37"], {"key": 42.37}), + (["key=-42.37"], {"key": -42.37}), + # Strings + (["key='hello'"], {"key": "hello"}), + (["key=''"], {"key": ""}), + (["key='42'"], {"key": "42"}), + (['key="hello"'], {"key": "hello"}), + (['key=""'], {"key": ""}), + (['key="42"'], {"key": "42"}), + # List + (['key=[1, "two", true]'], {"key": [1, "two", True]}), + # Dictionary + (['key={a=1, b="two", c=true}'], {"key": {"a": 1, "b": "two", "c": True}}), + # Multiple values + ( + [ + "key1=42", + 'key2="hello"', + 'key3=[1, "two", true]', + 'key4={a=1, b="two", c=true}', + ], + { + "key1": 42, + "key2": "hello", + "key3": [1, "two", True], + "key4": {"a": 1, "b": "two", "c": True}, + }, + ), + ], +) +def test_valid_overrides(overrides, values): + """Valid values can be parsed as config overrides.""" + assert parse_config_overrides(overrides) == values + + +@pytest.mark.parametrize( + "overrides, message", + [ + # Bare string + (["foobar"], r"Unable to parse configuration override "), + # Unquoted string + (["key=foobar"], r"Unable to parse configuration override "), + # Unbalanced quote + (["key='foobar"], r"Unable to parse configuration override "), + # Unmatched brackets + (['key=[1, "two",'], r"Unable to parse configuration override "), + # Unmatches parentheses + (['key={a=1, b="two"'], r"Unable to parse configuration override "), + # Valid value followed by invalid + (["good=42", "key=foobar"], r"Unable to parse configuration override "), + # Space in the key. + (["spacy key=42"], r"Unable to parse configuration override "), + # Multi-level key. This is legal TOML, but difficult to merge. + (["multi.level.key=42"], r"Can't override multi-level configuration keys\."), + # Key that can't be overridden + (["app_name='foobar'"], r"The app name cannot be overridden\."), + ], +) +def test_invalid_overrides(overrides, message): + with pytest.raises(BriefcaseConfigError, match=message): + parse_config_overrides(overrides) diff --git a/tests/commands/base/test_parse_options.py b/tests/commands/base/test_parse_options.py index 684e59dd1..326d41049 100644 --- a/tests/commands/base/test_parse_options.py +++ b/tests/commands/base/test_parse_options.py @@ -3,15 +3,51 @@ from briefcase.console import LogLevel -def test_parse_options(base_command): - """Command line options are parsed if provided.""" - options = base_command.parse_options(extra=("-x", "wibble", "-r", "important")) +def test_parse_options_no_overrides(base_command): + """Command line options are parsed if provided without overrides.""" + options, overrides = base_command.parse_options( + extra=( + "-x", + "wibble", + "-r", + "important", + ) + ) assert options == { "extra": "wibble", "mystery": None, "required": "important", } + assert overrides == {} + assert base_command.input.enabled + assert base_command.logger.verbosity == LogLevel.INFO + + +def test_parse_options_with_overrides(base_command): + """Command line options and overrides are parsed if provided.""" + options, overrides = base_command.parse_options( + extra=( + "-x", + "wibble", + "-r", + "important", + "-C", + "width=10", + "-C", + "height=20", + ) + ) + + assert options == { + "extra": "wibble", + "mystery": None, + "required": "important", + } + assert overrides == { + "width": 10, + "height": 20, + } assert base_command.input.enabled assert base_command.logger.verbosity == LogLevel.INFO diff --git a/tests/commands/build/test_call.py b/tests/commands/build/test_call.py index 826afb250..3a5caa040 100644 --- a/tests/commands/build/test_call.py +++ b/tests/commands/build/test_call.py @@ -12,7 +12,7 @@ def test_specific_app(build_command, first_app, second_app): } # Configure no command line options - options = build_command.parse_options([]) + options, _ = build_command.parse_options([]) # Run the build command build_command(first_app, **options) @@ -43,7 +43,7 @@ def test_multiple_apps(build_command, first_app, second_app): } # Configure no command line options - options = build_command.parse_options([]) + options, _ = build_command.parse_options([]) # Run the build command build_command(**options) @@ -81,7 +81,7 @@ def test_non_existent(build_command, first_app_config, second_app): } # Configure no command line options - options = build_command.parse_options([]) + options, _ = build_command.parse_options([]) # Run the build command build_command(**options) @@ -125,7 +125,7 @@ def test_unbuilt(build_command, first_app_unbuilt, second_app): } # Configure no command line options - options = build_command.parse_options([]) + options, _ = build_command.parse_options([]) # Run the build command build_command(**options) @@ -163,7 +163,7 @@ def test_update_app(build_command, first_app, second_app): } # Configure a -a command line option - options = build_command.parse_options(["-u"]) + options, _ = build_command.parse_options(["-u"]) # Run the build command build_command(**options) @@ -227,7 +227,7 @@ def test_update_app_requirements(build_command, first_app, second_app): } # Configure update command line options - options = build_command.parse_options(["-r"]) + options, _ = build_command.parse_options(["-r"]) # Run the build command build_command(**options) @@ -291,7 +291,7 @@ def test_update_app_resources(build_command, first_app, second_app): } # Configure update command line options - options = build_command.parse_options(["--update-resources"]) + options, _ = build_command.parse_options(["--update-resources"]) # Run the build command build_command(**options) @@ -355,7 +355,7 @@ def test_update_non_existent(build_command, first_app_config, second_app): } # Configure no command line options - options = build_command.parse_options(["-u"]) + options, _ = build_command.parse_options(["-u"]) # Run the build command build_command(**options) @@ -415,7 +415,7 @@ def test_update_unbuilt(build_command, first_app_unbuilt, second_app): } # Configure no command line options - options = build_command.parse_options(["-u"]) + options, _ = build_command.parse_options(["-u"]) # Run the build command build_command(**options) @@ -479,7 +479,7 @@ def test_build_test(build_command, first_app, second_app): } # Configure command line options - options = build_command.parse_options(["--test"]) + options, _ = build_command.parse_options(["--test"]) # Run the build command build_command(**options) @@ -544,7 +544,7 @@ def test_build_test_no_update(build_command, first_app, second_app): } # Configure command line options - options = build_command.parse_options(["--test", "--no-update"]) + options, _ = build_command.parse_options(["--test", "--no-update"]) # Run the build command build_command(**options) @@ -587,7 +587,7 @@ def test_build_test_update_dependencies(build_command, first_app, second_app): } # Configure command line options - options = build_command.parse_options(["--test", "-r"]) + options, _ = build_command.parse_options(["--test", "-r"]) # Run the build command build_command(**options) @@ -652,7 +652,7 @@ def test_build_test_update_resources(build_command, first_app, second_app): } # Configure command line options - options = build_command.parse_options(["--test", "--update-resources"]) + options, _ = build_command.parse_options(["--test", "--update-resources"]) # Run the build command build_command(**options) @@ -716,7 +716,7 @@ def test_build_invalid_update(build_command, first_app, second_app): } # Configure command line options - options = build_command.parse_options(["-u", "--no-update"]) + options, _ = build_command.parse_options(["-u", "--no-update"]) # Run the build command with pytest.raises( @@ -736,7 +736,7 @@ def test_build_invalid_update_requirements(build_command, first_app, second_app) } # Configure command line options - options = build_command.parse_options(["-r", "--no-update"]) + options, _ = build_command.parse_options(["-r", "--no-update"]) # Run the build command with pytest.raises( @@ -756,7 +756,7 @@ def test_build_invalid_update_resources(build_command, first_app, second_app): } # Configure command line options - options = build_command.parse_options(["--update-resources", "--no-update"]) + options, _ = build_command.parse_options(["--update-resources", "--no-update"]) # Run the build command with pytest.raises( @@ -776,7 +776,7 @@ def test_build_invalid_update_support(build_command, first_app, second_app): } # Configure command line options - options = build_command.parse_options(["--update-support", "--no-update"]) + options, _ = build_command.parse_options(["--update-support", "--no-update"]) # Run the build command with pytest.raises( @@ -795,7 +795,7 @@ def test_test_app_non_existent(build_command, first_app_config, second_app): } # Configure command line options - options = build_command.parse_options(["-u", "--test"]) + options, _ = build_command.parse_options(["-u", "--test"]) # Run the build command build_command(**options) @@ -856,7 +856,7 @@ def test_test_app_unbuilt(build_command, first_app_unbuilt, second_app): } # Configure command line options - options = build_command.parse_options(["-u", "--test"]) + options, _ = build_command.parse_options(["-u", "--test"]) # Run the build command build_command(**options) diff --git a/tests/commands/dev/test_call.py b/tests/commands/dev/test_call.py index 075122a0a..3b1293c09 100644 --- a/tests/commands/dev/test_call.py +++ b/tests/commands/dev/test_call.py @@ -65,7 +65,7 @@ def test_no_args_one_app(dev_command, first_app): } # Configure no command line options - options = dev_command.parse_options([]) + options, _ = dev_command.parse_options([]) # Run the run command dev_command(**options) @@ -94,7 +94,7 @@ def test_no_args_two_apps(dev_command, first_app, second_app): } # Configure no command line options - options = dev_command.parse_options([]) + options, _ = dev_command.parse_options([]) # Invoking the run command raises an error with pytest.raises(BriefcaseCommandError): @@ -112,7 +112,7 @@ def test_with_arg_one_app(dev_command, first_app): } # Configure a -a command line option - options = dev_command.parse_options(["-a", "first"]) + options, _ = dev_command.parse_options(["-a", "first"]) # Run the run command dev_command(**options) @@ -141,7 +141,7 @@ def test_with_arg_two_apps(dev_command, first_app, second_app): } # Configure a --app command line option - options = dev_command.parse_options(["--app", "second"]) + options, _ = dev_command.parse_options(["--app", "second"]) # Run the run command dev_command(**options) @@ -171,7 +171,7 @@ def test_bad_app_reference(dev_command, first_app, second_app): } # Configure a --app command line option - options = dev_command.parse_options(["--app", "does-not-exist"]) + options, _ = dev_command.parse_options(["--app", "does-not-exist"]) # Invoking the run command raises an error with pytest.raises(BriefcaseCommandError): @@ -189,7 +189,7 @@ def test_update_requirements(dev_command, first_app): } # Configure a requirements update - options = dev_command.parse_options(["-r"]) + options, _ = dev_command.parse_options(["-r"]) # Run the run command dev_command(**options) @@ -219,7 +219,7 @@ def test_run_uninstalled(dev_command, first_app_uninstalled): } # Configure no command line options - options = dev_command.parse_options([]) + options, _ = dev_command.parse_options([]) # Run the run command dev_command(**options) @@ -250,7 +250,7 @@ def test_update_uninstalled(dev_command, first_app_uninstalled): } # Configure a requirements update - options = dev_command.parse_options(["-r"]) + options, _ = dev_command.parse_options(["-r"]) # Run the run command dev_command(**options) @@ -280,7 +280,7 @@ def test_no_run(dev_command, first_app_uninstalled): } # Configure an update without run - options = dev_command.parse_options(["--no-run"]) + options, _ = dev_command.parse_options(["--no-run"]) # Run the run command dev_command(**options) @@ -308,7 +308,7 @@ def test_run_test(dev_command, first_app): } # Configure the test option - options = dev_command.parse_options(["--test"]) + options, _ = dev_command.parse_options(["--test"]) # Run the run command dev_command(**options) @@ -336,7 +336,7 @@ def test_run_test_uninstalled(dev_command, first_app_uninstalled): } # Configure the test option - options = dev_command.parse_options(["--test"]) + options, _ = dev_command.parse_options(["--test"]) # Run the run command dev_command(**options) diff --git a/tests/commands/new/test_call.py b/tests/commands/new/test_call.py index 8e3e1890c..04648626e 100644 --- a/tests/commands/new/test_call.py +++ b/tests/commands/new/test_call.py @@ -21,14 +21,14 @@ def monkeypatch_verify_git(*a, **kw): def test_parse_config(new_command): """Attempting to parse the config is a no-op when invoking new.""" - assert new_command.parse_config("some_file.toml") is None + assert new_command.parse_config("some_file.toml", {}) is None def test_new_app(new_command): """A new application can be created.""" # Configure no command line options - options = new_command.parse_options([]) + options, _ = new_command.parse_options([]) # Run the run command new_command(**options) diff --git a/tests/commands/open/test_call.py b/tests/commands/open/test_call.py index ce6f3c404..ed500e601 100644 --- a/tests/commands/open/test_call.py +++ b/tests/commands/open/test_call.py @@ -1,7 +1,7 @@ def test_open(open_command, first_app, second_app): """The open command can be called.""" # Configure no command line options - options = open_command.parse_options([]) + options, _ = open_command.parse_options([]) open_command(**options) @@ -32,7 +32,7 @@ def test_open(open_command, first_app, second_app): def test_open_single(open_command, first_app): """The open command can be called to open a single app from the config.""" # Configure no command line options - options = open_command.parse_options([]) + options, _ = open_command.parse_options([]) open_command(app=open_command.apps["first"], **options) @@ -56,7 +56,7 @@ def test_open_single(open_command, first_app): def test_create_before_open(open_command, tmp_path): """If the app doesn't exist, it will be created before opening.""" # Configure no command line options - options = open_command.parse_options([]) + options, _ = open_command.parse_options([]) open_command(app=open_command.apps["first"], **options) diff --git a/tests/commands/package/test_call.py b/tests/commands/package/test_call.py index 096236dce..a4a1d10a3 100644 --- a/tests/commands/package/test_call.py +++ b/tests/commands/package/test_call.py @@ -9,7 +9,7 @@ def test_no_args_package_one_app(package_command, first_app, tmp_path): } # Configure no command line options - options = package_command.parse_options([]) + options, _ = package_command.parse_options([]) # Run the run command package_command(**options) @@ -53,7 +53,7 @@ def test_package_one_explicit_app(package_command, first_app, second_app, tmp_pa } # Configure no command line arguments - options = package_command.parse_options([]) + options, _ = package_command.parse_options([]) # Run the build command on a specific app package_command(first_app, **options) @@ -98,7 +98,7 @@ def test_no_args_package_two_app(package_command, first_app, second_app, tmp_pat } # Configure no command line options - options = package_command.parse_options([]) + options, _ = package_command.parse_options([]) # Run the package command package_command(**options) @@ -159,7 +159,7 @@ def test_identity_arg_package_one_app(package_command, first_app, tmp_path): } # Configure an identity option - options = package_command.parse_options(["--identity", "test"]) + options, _ = package_command.parse_options(["--identity", "test"]) # Run the run command package_command(**options) @@ -203,7 +203,7 @@ def test_adhoc_sign_package_one_app(package_command, first_app, tmp_path): } # Configure an ad-hoc signing option - options = package_command.parse_options(["--adhoc-sign"]) + options, _ = package_command.parse_options(["--adhoc-sign"]) # Run the run command package_command(**options) @@ -252,7 +252,7 @@ def test_adhoc_sign_args_package_two_app( } # Configure adhoc command line options - options = package_command.parse_options(["--adhoc-sign"]) + options, _ = package_command.parse_options(["--adhoc-sign"]) # Run the package command package_command(**options) @@ -316,7 +316,7 @@ def test_identity_sign_args_package_two_app( } # Configure an identity option - options = package_command.parse_options(["--identity", "test"]) + options, _ = package_command.parse_options(["--identity", "test"]) # Run the run command package_command(**options) @@ -376,7 +376,7 @@ def test_package_alternate_format(package_command, first_app, tmp_path): } # Configure command line options with an alternate format - options = package_command.parse_options(["--packaging-format", "box"]) + options, _ = package_command.parse_options(["--packaging-format", "box"]) # Run the run command package_command(**options) @@ -419,7 +419,7 @@ def test_create_before_package(package_command, first_app_config, tmp_path): } # Configure no command line options - options = package_command.parse_options([]) + options, _ = package_command.parse_options([]) # Run the run command package_command(**options) @@ -482,7 +482,7 @@ def test_update_package_one_app(package_command, first_app, tmp_path): } # Configure an update option - options = package_command.parse_options(["-u"]) + options, _ = package_command.parse_options(["-u"]) # Run the run command package_command(**options) @@ -549,7 +549,7 @@ def test_update_package_two_app(package_command, first_app, second_app, tmp_path } # Configure an update option - options = package_command.parse_options(["--update"]) + options, _ = package_command.parse_options(["--update"]) # Run the package command package_command(**options) @@ -662,7 +662,7 @@ def test_build_before_package(package_command, first_app_unbuilt, tmp_path): } # Configure no command line options - options = package_command.parse_options([]) + options, _ = package_command.parse_options([]) # Run the run command package_command(**options) @@ -719,7 +719,7 @@ def test_already_packaged(package_command, first_app, tmp_path): create_file(artefact_path, "Packaged app") # Configure no command line options - options = package_command.parse_options([]) + options, _ = package_command.parse_options([]) # Run the run command package_command(**options) diff --git a/tests/commands/publish/test_call.py b/tests/commands/publish/test_call.py index 59b271833..4b5544388 100644 --- a/tests/commands/publish/test_call.py +++ b/tests/commands/publish/test_call.py @@ -12,7 +12,7 @@ def test_publish(publish_command, first_app, second_app): } # Configure no command line options - options = publish_command.parse_options([]) + options, _ = publish_command.parse_options([]) # Run the publish command publish_command(**options) @@ -50,7 +50,7 @@ def test_publish_alternative_channel(publish_command, first_app, second_app): } # Configure no command line options - options = publish_command.parse_options(["-c", "alternative"]) + options, _ = publish_command.parse_options(["-c", "alternative"]) # Run the publish command publish_command(**options) @@ -88,7 +88,7 @@ def test_non_existent(publish_command, first_app_config, second_app): } # Configure no command line options - options = publish_command.parse_options([]) + options, _ = publish_command.parse_options([]) # Invoking the publish command raises an error with pytest.raises(BriefcaseCommandError): @@ -116,7 +116,7 @@ def test_unbuilt(publish_command, first_app_unbuilt, second_app): } # Configure no command line options - options = publish_command.parse_options([]) + options, _ = publish_command.parse_options([]) # Invoking the publish command raises an error with pytest.raises(BriefcaseCommandError): diff --git a/tests/commands/run/test_call.py b/tests/commands/run/test_call.py index 6145f9f78..899bdb611 100644 --- a/tests/commands/run/test_call.py +++ b/tests/commands/run/test_call.py @@ -11,7 +11,7 @@ def test_no_args_one_app(run_command, first_app): } # Configure no command line options - options = run_command.parse_options([]) + options, _ = run_command.parse_options([]) # Run the run command run_command(**options) @@ -42,7 +42,7 @@ def test_no_args_one_app_with_passthrough(run_command, first_app): } # Configure no command line options - options = run_command.parse_options(["--", "foo", "--bar"]) + options, _ = run_command.parse_options(["--", "foo", "--bar"]) # Run the run command run_command(**options) @@ -73,7 +73,7 @@ def test_no_args_two_apps(run_command, first_app, second_app): } # Configure no command line options - options = run_command.parse_options([]) + options, _ = run_command.parse_options([]) # Invoking the run command raises an error with pytest.raises(BriefcaseCommandError): @@ -91,7 +91,7 @@ def test_with_arg_one_app(run_command, first_app): } # Configure a -a command line option - options = run_command.parse_options(["-a", "first"]) + options, _ = run_command.parse_options(["-a", "first"]) # Run the run command run_command(**options) @@ -122,7 +122,7 @@ def test_with_arg_two_apps(run_command, first_app, second_app): } # Configure a --app command line option - options = run_command.parse_options(["--app", "second"]) + options, _ = run_command.parse_options(["--app", "second"]) # Run the run command run_command(**options) @@ -154,7 +154,7 @@ def test_bad_app_reference(run_command, first_app, second_app): } # Configure a --app command line option - options = run_command.parse_options(["--app", "does-not-exist"]) + options, _ = run_command.parse_options(["--app", "does-not-exist"]) # Invoking the run command raises an error with pytest.raises(BriefcaseCommandError): @@ -172,7 +172,7 @@ def test_create_app_before_start(run_command, first_app_config): } # Configure no command line options - options = run_command.parse_options([]) + options, _ = run_command.parse_options([]) # Run the run command run_command(**options) @@ -220,7 +220,7 @@ def test_build_app_before_start(run_command, first_app_unbuilt): } # Configure no command line options - options = run_command.parse_options([]) + options, _ = run_command.parse_options([]) # Run the run command run_command(**options) @@ -267,7 +267,7 @@ def test_update_app(run_command, first_app): } # Configure an update option - options = run_command.parse_options(["-u"]) + options, _ = run_command.parse_options(["-u"]) # Run the run command run_command(**options) @@ -314,7 +314,7 @@ def test_update_app_requirements(run_command, first_app): } # Configure an update option - options = run_command.parse_options(["-r"]) + options, _ = run_command.parse_options(["-r"]) # Run the run command run_command(**options) @@ -361,7 +361,7 @@ def test_update_app_resources(run_command, first_app): } # Configure an update option - options = run_command.parse_options(["--update-resources"]) + options, _ = run_command.parse_options(["--update-resources"]) # Run the run command run_command(**options) @@ -408,7 +408,7 @@ def test_update_unbuilt_app(run_command, first_app_unbuilt): } # Configure an update option - options = run_command.parse_options(["-u"]) + options, _ = run_command.parse_options(["-u"]) # Run the run command run_command(**options) @@ -456,7 +456,7 @@ def test_update_non_existent(run_command, first_app_config): } # Configure an update option - options = run_command.parse_options(["-u"]) + options, _ = run_command.parse_options(["-u"]) # Run the run command run_command(**options) @@ -504,7 +504,7 @@ def test_test_mode_existing_app(run_command, first_app): } # Configure the test option - options = run_command.parse_options(["--test"]) + options, _ = run_command.parse_options(["--test"]) # Run the run command run_command(**options) @@ -551,7 +551,7 @@ def test_test_mode_existing_app_with_passthrough(run_command, first_app): } # Configure the test option - options = run_command.parse_options(["--test", "--", "foo", "--bar"]) + options, _ = run_command.parse_options(["--test", "--", "foo", "--bar"]) # Run the run command run_command(**options) @@ -602,7 +602,7 @@ def test_test_mode_existing_app_no_update(run_command, first_app): } # Configure the test option - options = run_command.parse_options(["--test", "--no-update"]) + options, _ = run_command.parse_options(["--test", "--no-update"]) # Run the run command run_command(**options) @@ -637,7 +637,7 @@ def test_test_mode_existing_app_update_requirements(run_command, first_app): } # Configure the test option - options = run_command.parse_options(["--test", "--update-requirements"]) + options, _ = run_command.parse_options(["--test", "--update-requirements"]) # Run the run command run_command(**options) @@ -684,7 +684,7 @@ def test_test_mode_existing_app_update_resources(run_command, first_app): } # Configure the test option - options = run_command.parse_options(["--test", "--update-resources"]) + options, _ = run_command.parse_options(["--test", "--update-resources"]) # Run the run command run_command(**options) @@ -731,7 +731,7 @@ def test_test_mode_update_existing_app(run_command, first_app): } # Configure the test option - options = run_command.parse_options(["-u", "--test"]) + options, _ = run_command.parse_options(["-u", "--test"]) # Run the run command run_command(**options) @@ -778,7 +778,7 @@ def test_test_mode_non_existent(run_command, first_app_config): } # Configure a test option - options = run_command.parse_options(["--test"]) + options, _ = run_command.parse_options(["--test"]) # Run the run command run_command(**options) diff --git a/tests/commands/update/test_call.py b/tests/commands/update/test_call.py index cd48396e5..2ca085fc5 100644 --- a/tests/commands/update/test_call.py +++ b/tests/commands/update/test_call.py @@ -22,7 +22,7 @@ def monkeypatch_verify_git(*a, **kw): def test_update(update_command, first_app, second_app): """The update command can be called.""" # Configure no command line options - options = update_command.parse_options([]) + options, _ = update_command.parse_options([]) update_command(**options) @@ -51,7 +51,7 @@ def test_update(update_command, first_app, second_app): def test_update_single(update_command, first_app, second_app): """The update command can be called to update a single app from the config.""" # Configure no command line options - options = update_command.parse_options([]) + options, _ = update_command.parse_options([]) update_command(app=update_command.apps["first"], **options) @@ -74,7 +74,7 @@ def test_update_single(update_command, first_app, second_app): def test_update_with_requirements(update_command, first_app, second_app): """The update command can be called, requesting a requirements update.""" # Configure a requirements update - options = update_command.parse_options(["-r"]) + options, _ = update_command.parse_options(["-r"]) update_command(**options) @@ -105,7 +105,7 @@ def test_update_with_requirements(update_command, first_app, second_app): def test_update_with_resources(update_command, first_app, second_app): """The update command can be called, requesting a resources update.""" # Configure no command line options - options = update_command.parse_options(["--update-resources"]) + options, _ = update_command.parse_options(["--update-resources"]) update_command(**options) @@ -136,7 +136,7 @@ def test_update_with_resources(update_command, first_app, second_app): def test_update_with_support(update_command, first_app, second_app): """The update command can be called, requesting an app support update.""" # Configure no command line options - options = update_command.parse_options(["--update-support"]) + options, _ = update_command.parse_options(["--update-support"]) update_command(**options) diff --git a/tests/platforms/android/gradle/test_run.py b/tests/platforms/android/gradle/test_run.py index 3549bbcfd..3db1db2ed 100644 --- a/tests/platforms/android/gradle/test_run.py +++ b/tests/platforms/android/gradle/test_run.py @@ -80,7 +80,7 @@ def test_binary_path(run_command, first_app_config, tmp_path): def test_device_option(run_command): """The -d option can be parsed.""" - options = run_command.parse_options(["-d", "myphone"]) + options, overrides = run_command.parse_options(["-d", "myphone"]) assert options == { "device_or_avd": "myphone", @@ -95,11 +95,12 @@ def test_device_option(run_command): "extra_emulator_args": None, "shutdown_on_exit": False, } + assert overrides == {} def test_extra_emulator_args_option(run_command): """The -d option can be parsed.""" - options = run_command.parse_options( + options, overrides = run_command.parse_options( ["--Xemulator=-no-window", "--Xemulator=-no-audio"] ) @@ -116,11 +117,12 @@ def test_extra_emulator_args_option(run_command): "extra_emulator_args": ["-no-window", "-no-audio"], "shutdown_on_exit": False, } + assert overrides == {} def test_shutdown_on_exit_option(run_command): """The -d option can be parsed.""" - options = run_command.parse_options(["--shutdown-on-exit"]) + options, overrides = run_command.parse_options(["--shutdown-on-exit"]) assert options == { "device_or_avd": None, @@ -135,6 +137,7 @@ def test_shutdown_on_exit_option(run_command): "extra_emulator_args": None, "shutdown_on_exit": True, } + assert overrides == {} def test_unsupported_template_version(run_command, first_app_generated): diff --git a/tests/platforms/iOS/xcode/test_run.py b/tests/platforms/iOS/xcode/test_run.py index 6f858af10..bb7896760 100644 --- a/tests/platforms/iOS/xcode/test_run.py +++ b/tests/platforms/iOS/xcode/test_run.py @@ -39,7 +39,7 @@ def mock_stream_app_logs(app, stop_func, **kwargs): def test_device_option(run_command): """The -d option can be parsed.""" - options = run_command.parse_options(["-d", "myphone"]) + options, overrides = run_command.parse_options(["-d", "myphone"]) assert options == { "udid": "myphone", @@ -52,6 +52,7 @@ def test_device_option(run_command): "passthrough": [], "appname": None, } + assert overrides == {} def test_run_multiple_devices_input_disabled(run_command, first_app_config): diff --git a/tests/platforms/linux/appimage/test_create.py b/tests/platforms/linux/appimage/test_create.py index 62674398c..0ab0bd169 100644 --- a/tests/platforms/linux/appimage/test_create.py +++ b/tests/platforms/linux/appimage/test_create.py @@ -21,18 +21,20 @@ def create_command(first_app_config, tmp_path): def test_default_options(create_command): """The default options are as expected.""" - options = create_command.parse_options([]) + options, overrides = create_command.parse_options([]) assert options == {} + assert overrides == {} assert create_command.use_docker def test_options(create_command): """The extra options can be parsed.""" - options = create_command.parse_options(["--no-docker"]) + options, overrides = create_command.parse_options(["--no-docker"]) assert options == {} + assert overrides == {} assert not create_command.use_docker diff --git a/tests/platforms/linux/system/test_create.py b/tests/platforms/linux/system/test_create.py index 2039dcaed..12a212a7d 100644 --- a/tests/platforms/linux/system/test_create.py +++ b/tests/platforms/linux/system/test_create.py @@ -7,18 +7,22 @@ def test_default_options(create_command): """The default options are as expected.""" - options = create_command.parse_options([]) + options, overrides = create_command.parse_options([]) assert options == {} + assert overrides == {} assert create_command.target_image is None def test_options(create_command): """The extra options can be parsed.""" - options = create_command.parse_options(["--target", "somevendor:surprising"]) + options, overrides = create_command.parse_options( + ["--target", "somevendor:surprising"] + ) assert options == {} + assert overrides == {} assert create_command.target_image == "somevendor:surprising" diff --git a/tests/platforms/macOS/app/test_package.py b/tests/platforms/macOS/app/test_package.py index 93dda82fa..fbc158ed7 100644 --- a/tests/platforms/macOS/app/test_package.py +++ b/tests/platforms/macOS/app/test_package.py @@ -38,7 +38,7 @@ def test_package_formats(package_command): def test_device_option(package_command): """The -d option can be parsed.""" - options = package_command.parse_options(["--no-notarize"]) + options, overrides = package_command.parse_options(["--no-notarize"]) assert options == { "adhoc_sign": False, @@ -47,6 +47,7 @@ def test_device_option(package_command): "packaging_format": "dmg", "update": False, } + assert overrides == {} def test_package_app(package_command, first_app_with_binaries, tmp_path, capsys): diff --git a/tests/platforms/web/static/test_run.py b/tests/platforms/web/static/test_run.py index 04e54f1db..dea04baa5 100644 --- a/tests/platforms/web/static/test_run.py +++ b/tests/platforms/web/static/test_run.py @@ -28,7 +28,7 @@ def run_command(tmp_path): def test_default_options(run_command): """The default options are as expected.""" - options = run_command.parse_options([]) + options, overrides = run_command.parse_options([]) assert options == { "appname": None, @@ -43,11 +43,12 @@ def test_default_options(run_command): "port": 8080, "open_browser": True, } + assert overrides == {} def test_options(run_command): """The extra options can be parsed.""" - options = run_command.parse_options( + options, overrides = run_command.parse_options( ["--host", "myhost", "--port", "1234", "--no-browser"] ) @@ -64,6 +65,7 @@ def test_options(run_command): "port": 1234, "open_browser": False, } + assert overrides == {} def test_run(monkeypatch, run_command, first_app_built): diff --git a/tests/platforms/windows/app/test_package.py b/tests/platforms/windows/app/test_package.py index eb9d99d62..28d30d9a6 100644 --- a/tests/platforms/windows/app/test_package.py +++ b/tests/platforms/windows/app/test_package.py @@ -190,9 +190,10 @@ def test_parse_options(package_command, cli_args, signing_options, is_sdk_needed ) expected_options = {**default_options, **signing_options} - options = package_command.parse_options(extra=cli_args) + options, overrides = package_command.parse_options(extra=cli_args) assert options == expected_options + assert overrides == {} assert package_command._windows_sdk_needed is is_sdk_needed diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 1ed6df5ef..db61cbcb9 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -35,8 +35,8 @@ def do_cmdline_parse(args: list, logger: Log, console: Console): """Simulate process to parse command line.""" Command, extra_cmdline = cmdline.parse_cmdline(args) cmd = Command(logger=logger, console=console) - options = cmd.parse_options(extra=extra_cmdline) - return cmd, options + options, overrides = cmd.parse_options(extra=extra_cmdline) + return cmd, options, overrides def test_empty(): @@ -93,9 +93,33 @@ def test_unknown_command(): ) -def test_new_command(logger, console): +@pytest.mark.parametrize( + "cmdline, expected_options, expected_overrides", + [ + ( + "new", + { + "template": None, + "template_branch": None, + }, + {}, + ), + ( + "new --template=path/to/template --template-branch=experiment -C version=\\'1.2.3\\' -C other=42", + { + "template": "path/to/template", + "template_branch": "experiment", + }, + { + "version": "1.2.3", + "other": 42, + }, + ), + ], +) +def test_new_command(logger, console, cmdline, expected_options, expected_overrides): """``briefcase new`` returns the New command.""" - cmd, options = do_cmdline_parse("new".split(), logger, console) + cmd, options, overrides = do_cmdline_parse(shlex.split(cmdline), logger, console) assert isinstance(cmd, NewCommand) assert cmd.platform == "all" @@ -104,47 +128,68 @@ def test_new_command(logger, console): assert cmd.logger.verbosity == LogLevel.INFO assert cmd.logger is logger assert cmd.input is console - assert options == {"template": None, "template_branch": None} + assert options == expected_options + assert overrides == expected_overrides # 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"]}), + (f"{command} {args}", expected, overrides) + for args, expected, overrides in [ + ("", {}, {}), + ("-r", {"update_requirements": True}, {}), + ( + "-r -C version=\\'1.2.3\\' -C other=42", + {"update_requirements": True}, + { + "version": "1.2.3", + "other": 42, + }, + ), + ("--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", + "cmdline, expected_options, expected_overrides", dev_run_parameters("dev") + [ - ("dev --no-run", {"run_app": False}), + ("dev --no-run", {"run_app": False}, {}), ], ) -def test_dev_command(monkeypatch, logger, console, cmdline, expected_options): +def test_dev_command( + monkeypatch, + logger, + console, + cmdline, + expected_options, + expected_overrides, +): """``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(shlex.split(cmdline), logger, console) + cmd, options, overrides = do_cmdline_parse(shlex.split(cmdline), logger, console) assert isinstance(cmd, DevCommand) assert cmd.platform == "macOS" @@ -161,25 +206,33 @@ def test_dev_command(monkeypatch, logger, console, cmdline, expected_options): "passthrough": [], **expected_options, } + assert overrides == expected_overrides @pytest.mark.parametrize( - "cmdline, expected_options", + "cmdline, expected_options, expected_overrides", dev_run_parameters("run") + [ - ("run -u", {"update": True}), - ("run --update", {"update": True}), - ("run --update-resources", {"update_resources": True}), - ("run --update-support", {"update_support": True}), - ("run --no-update", {"no_update": True}), + ("run -u", {"update": True}, {}), + ("run --update", {"update": True}, {}), + ("run --update-resources", {"update_resources": True}, {}), + ("run --update-support", {"update_support": True}, {}), + ("run --no-update", {"no_update": True}, {}), ], ) -def test_run_command(monkeypatch, logger, console, cmdline, expected_options): +def test_run_command( + monkeypatch, + logger, + console, + cmdline, + expected_options, + expected_overrides, +): """``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(shlex.split(cmdline), logger, console) + cmd, options, overrides = do_cmdline_parse(shlex.split(cmdline), logger, console) assert isinstance(cmd, macOSAppRunCommand) assert cmd.platform == "macOS" @@ -199,14 +252,46 @@ def test_run_command(monkeypatch, logger, console, cmdline, expected_options): "passthrough": [], **expected_options, } + assert overrides == expected_overrides -def test_upgrade_command(monkeypatch, logger, console): +@pytest.mark.parametrize( + "cmdline,expected_options,expected_overrides", + [ + ( + "upgrade", + { + "list_tools": False, + "tool_list": [], + }, + {}, + ), + ( + "upgrade -C version='1.2.3' -C other=42", + { + "list_tools": False, + "tool_list": [], + }, + { + "version": "1.2.3", + "other": 42, + }, + ), + ], +) +def test_upgrade_command( + monkeypatch, + logger, + console, + cmdline, + expected_options, + expected_overrides, +): """``briefcase upgrade`` returns the upgrade command.""" # Pretend we're on macOS, regardless of where the tests run. monkeypatch.setattr(sys, "platform", "darwin") - cmd, options = do_cmdline_parse("upgrade".split(), logger, console) + cmd, options, overrides = do_cmdline_parse(cmdline.split(), logger, console) assert isinstance(cmd, UpgradeCommand) assert cmd.platform == "macOS" @@ -215,10 +300,8 @@ def test_upgrade_command(monkeypatch, logger, console): assert cmd.logger.verbosity == LogLevel.INFO assert cmd.logger is logger assert cmd.input is console - assert options == { - "list_tools": False, - "tool_list": [], - } + assert options == expected_options + assert overrides == expected_overrides def test_bare_command(monkeypatch, logger, console): @@ -226,7 +309,7 @@ def test_bare_command(monkeypatch, logger, console): # Pretend we're on macOS, regardless of where the tests run. monkeypatch.setattr(sys, "platform", "darwin") - cmd, options = do_cmdline_parse("create".split(), logger, console) + cmd, options, overrides = do_cmdline_parse("create".split(), logger, console) assert isinstance(cmd, macOSAppCreateCommand) assert cmd.platform == "macOS" @@ -236,13 +319,14 @@ def test_bare_command(monkeypatch, logger, console): assert cmd.logger is logger assert cmd.input is console assert options == {} + assert overrides == {} @pytest.mark.skipif(sys.platform != "linux", reason="requires Linux") def test_linux_default(logger, console): """``briefcase create`` returns the linux create system command on Linux.""" - cmd, options = do_cmdline_parse("create".split(), logger, console) + cmd, options, overrides = do_cmdline_parse("create".split(), logger, console) assert isinstance(cmd, LinuxSystemCreateCommand) assert cmd.platform == "linux" @@ -258,7 +342,7 @@ def test_linux_default(logger, console): def test_macOS_default(logger, console): """``briefcase create`` returns the macOS create command on Linux.""" - cmd, options = do_cmdline_parse("create".split(), logger, console) + cmd, options, overrides = do_cmdline_parse("create".split(), logger, console) assert isinstance(cmd, macOSAppCreateCommand) assert cmd.platform == "macOS" @@ -268,13 +352,14 @@ def test_macOS_default(logger, console): assert cmd.logger is logger assert cmd.input is console assert options == {} + assert overrides == {} @pytest.mark.skipif(sys.platform != "win32", reason="requires Windows") def test_windows_default(logger, console): """``briefcase create`` returns the Windows create app command on Windows.""" - cmd, options = do_cmdline_parse("create".split(), logger, console) + cmd, options, overrides = do_cmdline_parse("create".split(), logger, console) assert isinstance(cmd, WindowsAppCreateCommand) assert cmd.platform == "windows" @@ -284,6 +369,7 @@ def test_windows_default(logger, console): assert cmd.logger is logger assert cmd.input is console assert options == {} + assert overrides == {} def test_bare_command_help(monkeypatch, capsys, logger, console): @@ -299,7 +385,8 @@ def test_bare_command_help(monkeypatch, capsys, logger, console): # Help message is for default platform and format output = capsys.readouterr().out assert output.startswith( - "usage: briefcase create macOS app [-h] [-v] [-V] [--no-input] [--log]\n" + "usage: briefcase create macOS app [-h] [-C KEY=VALUE] [-v] [-V] [--no-input]\n" + " [--log]\n" "\n" "Create and populate a macOS app.\n" ) @@ -337,7 +424,7 @@ def test_command_explicit_platform(monkeypatch, logger, console): # Pretend we're on macOS, regardless of where the tests run. monkeypatch.setattr(sys, "platform", "darwin") - cmd, options = do_cmdline_parse("create linux".split(), logger, console) + cmd, options, overrides = do_cmdline_parse("create linux".split(), logger, console) assert isinstance(cmd, LinuxSystemCreateCommand) assert cmd.platform == "linux" @@ -347,6 +434,7 @@ def test_command_explicit_platform(monkeypatch, logger, console): assert cmd.logger is logger assert cmd.input is console assert options == {} + assert overrides == {} def test_command_explicit_platform_case_handling(monkeypatch, logger, console): @@ -355,7 +443,7 @@ def test_command_explicit_platform_case_handling(monkeypatch, logger, console): monkeypatch.setattr(sys, "platform", "darwin") # This is all lower case; the command normalizes to macOS - cmd, options = do_cmdline_parse("create macOS".split(), logger, console) + cmd, options, overrides = do_cmdline_parse("create macOS".split(), logger, console) assert isinstance(cmd, macOSAppCreateCommand) assert cmd.platform == "macOS" @@ -365,6 +453,7 @@ def test_command_explicit_platform_case_handling(monkeypatch, logger, console): assert cmd.logger is logger assert cmd.input is console assert options == {} + assert overrides == {} def test_command_explicit_platform_help(monkeypatch, capsys, logger, console): @@ -380,7 +469,8 @@ def test_command_explicit_platform_help(monkeypatch, capsys, logger, console): # Help message is for default platform and format output = capsys.readouterr().out assert output.startswith( - "usage: briefcase create macOS app [-h] [-v] [-V] [--no-input] [--log]\n" + "usage: briefcase create macOS app [-h] [-C KEY=VALUE] [-v] [-V] [--no-input]\n" + " [--log]\n" "\n" "Create and populate a macOS app.\n" ) @@ -391,7 +481,9 @@ def test_command_explicit_format(monkeypatch, logger, console): # Pretend we're on macOS, regardless of where the tests run. monkeypatch.setattr(sys, "platform", "darwin") - cmd, options = do_cmdline_parse("create macOS app".split(), logger, console) + cmd, options, overrides = do_cmdline_parse( + "create macOS app".split(), logger, console + ) assert isinstance(cmd, macOSAppCreateCommand) assert cmd.platform == "macOS" @@ -401,6 +493,7 @@ def test_command_explicit_format(monkeypatch, logger, console): assert cmd.logger is logger assert cmd.input is console assert options == {} + assert overrides == {} def test_command_unknown_format(monkeypatch, logger, console): @@ -446,7 +539,8 @@ def test_command_explicit_format_help(monkeypatch, capsys, logger, console): # Help message is for default platform, but app format output = capsys.readouterr().out assert output.startswith( - "usage: briefcase create macOS app [-h] [-v] [-V] [--no-input] [--log]\n" + "usage: briefcase create macOS app [-h] [-C KEY=VALUE] [-v] [-V] [--no-input]\n" + " [--log]\n" "\n" "Create and populate a macOS app.\n" ) @@ -457,7 +551,9 @@ def test_command_disable_input(monkeypatch, logger, console): # Pretend we're on macOS, regardless of where the tests run. monkeypatch.setattr(sys, "platform", "darwin") - cmd, options = do_cmdline_parse("create --no-input".split(), logger, console) + cmd, options, overrides = do_cmdline_parse( + "create --no-input".split(), logger, console + ) assert isinstance(cmd, macOSAppCreateCommand) assert cmd.platform == "macOS" @@ -467,6 +563,7 @@ def test_command_disable_input(monkeypatch, logger, console): assert cmd.logger is logger assert cmd.input is console assert options == {} + assert overrides == {} def test_command_options(monkeypatch, capsys, logger, console): @@ -476,7 +573,9 @@ def test_command_options(monkeypatch, capsys, logger, console): # Invoke a command that is known to have its own custom arguments # (In this case, the channel argument for publication) - cmd, options = do_cmdline_parse("publish macos app -c s3".split(), logger, console) + cmd, options, overrides = do_cmdline_parse( + "publish macos app -c s3".split(), logger, console + ) assert isinstance(cmd, macOSAppPublishCommand) assert cmd.input.enabled @@ -484,6 +583,32 @@ def test_command_options(monkeypatch, capsys, logger, console): assert cmd.logger is logger assert cmd.input is console assert options == {"channel": "s3"} + assert overrides == {} + + +def test_command_overrides(monkeypatch, capsys, logger, console): + """Configuration overrides can be specified.""" + # Pretend we're on macOS, regardless of where the tests run. + monkeypatch.setattr(sys, "platform", "darwin") + + # Invoke a command that is known to have its own custom arguments + # (In this case, the channel argument for publication) + cmd, options, overrides = do_cmdline_parse( + "publish macos app -C version='1.2.3' -C extra=42".split(), + logger, + console, + ) + + assert isinstance(cmd, macOSAppPublishCommand) + assert cmd.input.enabled + assert cmd.logger.verbosity == LogLevel.INFO + assert cmd.logger is logger + assert cmd.input is console + assert options == {"channel": "s3"} + assert overrides == { + "version": "1.2.3", + "extra": 42, + } def test_unknown_command_options(monkeypatch, capsys, logger, console): @@ -501,7 +626,7 @@ def test_unknown_command_options(monkeypatch, capsys, logger, console): output = capsys.readouterr().err assert output.startswith( - "usage: briefcase publish macOS Xcode [-h] [-v] [-V] [--no-input] [--log]\n" - " [-c {s3}]\n" + "usage: briefcase publish macOS Xcode [-h] [-C KEY=VALUE] [-v] [-V]\n" + " [--no-input] [--log] [-c {s3}]\n" "briefcase publish macOS Xcode: error: unrecognized arguments: -x" )