Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify handling of command line arguments. #2028

Merged
merged 1 commit into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/2026.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Python 3.12.7 introduced an incompatibility with the handling of ``-C``, ``-d`` and other flags that accept values. This incompatibility has been corrected.
71 changes: 32 additions & 39 deletions src/briefcase/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@
from briefcase.console import MAX_TEXT_WIDTH, Console
from briefcase.platforms import get_output_formats, get_platforms

from .exceptions import InvalidFormatError, NoCommandError, UnsupportedCommandError
from .exceptions import (
InvalidFormatError,
InvalidPlatformError,
NoCommandError,
UnsupportedCommandError,
)

COMMANDS = [
NewCommand,
Expand Down Expand Up @@ -103,12 +108,6 @@ def parse_cmdline(args, console: Console | None = None):
help=argparse.SUPPRESS,
)

# To make the UX a little forgiving, we normalize *any* case to the case
# actually used to register the platform. This function maps the lower-case
# version of the registered name to the actual registered name.
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, passthrough = split_passthrough(args)
Expand Down Expand Up @@ -136,48 +135,42 @@ def parse_known_args(args):
Command = DevCommand
elif options.command == "upgrade":
Command = UpgradeCommand

# Commands dependent on the platform and format
else:
parser.add_argument(
"platform",
choices=list(platforms.keys()),
default={
# Commands dependent on the platform and format. The general form of such a
# command is `briefcase <cmd> <platform> <format>`; but the format will be
# inferred from the platform if one isn't specified, and the platform will be
# inferred from the operating system if it isn't explicitly given.
#
# <platform> and <format> aren't parsed as regular arguments due to ambiguities
# in interpreting those arguments; instead, they're handled directly from the
# argument list, with the expectation that they *must* be the first and second
# arguments (after the command) if provided. There's no other bare arguments, so
# we only need to look for whether the arguments start with "-".
if extra and not extra[0].startswith("-"):
name = extra.pop(0)
# Normalize the platform name to the registered capitalization
platform = {n.lower(): n for n in platforms.keys()}.get(name.lower(), name)
else:
platform = {
"darwin": "macOS",
"linux": "linux",
"win32": "windows",
}[sys.platform],
metavar="platform",
nargs="?",
type=normalize,
help="The platform to target (one of %(choices)s; default: %(default)s",
)

# <format> is also optional, with the default being platform dependent.
# There's no way to encode option-dependent choices, so allow *any*
# input, and we'll manually validate.
parser.add_argument(
"output_format",
metavar="format",
nargs="?",
help="The output format to use (the available output formats are platform dependent)",
)

# Re-parse the arguments, now that we know it is a command that makes use
# of platform/output_format.
options, extra = parse_known_args(args)
}[sys.platform]

# Import the platform module
platform_module = platforms[options.platform]
try:
platform_module = platforms[platform]
except KeyError:
raise InvalidPlatformError(platform, platforms.keys())

# If the output format wasn't explicitly specified, check to see
# Otherwise, extract and use the default output_format for the platform.
if options.output_format is None:
output_format = platform_module.DEFAULT_OUTPUT_FORMAT
if extra and not extra[0].startswith("-") and not extra[0] == "--":
output_format = extra.pop(0)
else:
output_format = options.output_format
output_format = platform_module.DEFAULT_OUTPUT_FORMAT

output_formats = get_output_formats(options.platform)
output_formats = get_output_formats(platform)

# Normalise casing of output_format to be more forgiving.
output_format = {n.lower(): n for n in output_formats}.get(
Expand All @@ -196,7 +189,7 @@ def parse_known_args(args):
)
except AttributeError:
raise UnsupportedCommandError(
platform=options.platform,
platform=platform,
output_format=output_format,
command=options.command,
)
Expand Down
11 changes: 11 additions & 0 deletions src/briefcase/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ def __str__(self):
return self.msg


class InvalidPlatformError(BriefcaseError):
def __init__(self, requested, choices):
super().__init__(error_code=-20, skip_logfile=True)
self.requested = requested
self.choices = choices

def __str__(self):
choices = ", ".join(sorted(self.choices, key=str.lower))
return f"Invalid platform {self.requested!r}; (choose from: {choices})"


class InvalidFormatError(BriefcaseError):
def __init__(self, requested, choices):
super().__init__(error_code=-21, skip_logfile=True)
Expand Down
10 changes: 3 additions & 7 deletions tests/test_cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from briefcase.console import Console, Log, LogLevel
from briefcase.exceptions import (
InvalidFormatError,
InvalidPlatformError,
NoCommandError,
UnsupportedCommandError,
)
Expand Down Expand Up @@ -456,15 +457,10 @@ def test_command_unknown_platform(monkeypatch, logger, console):
# Pretend we're on macOS, regardless of where the tests run.
monkeypatch.setattr(sys, "platform", "darwin")

with pytest.raises(SystemExit) as excinfo:
expected_exc_regex = r"Invalid platform 'foobar'; \(choose from: .*\)"
with pytest.raises(InvalidPlatformError, match=expected_exc_regex):
do_cmdline_parse("create foobar".split(), logger, console)

assert excinfo.value.code == 2
assert excinfo.value.__context__.argument_name == "platform"
assert excinfo.value.__context__.message.startswith(
"invalid choice: 'foobar' (choose from"
)


def test_command_explicit_platform(monkeypatch, logger, console):
"""``briefcase create linux`` returns linux create app command."""
Expand Down
Loading