def pytest_cmdline_main(config):
    "Parse the command line, adding the sys.path entries needed to support the app"
    app_name = config.getoption("app")
    platform = config.getoption("platform")

    # Load the platform module and determine the default output format.
    platforms = get_platforms()
    platform_module = platforms[platform]

    # Determine the output format to target
    try:
        output_format = config.getoption("output_format")
    except ValueError:
        output_format = platform_module.DEFAULT_OUTPUT_FORMAT

    # Load the application config from the pyproject.toml
    # in the pytest rootdir
    _, app_configs = parse_config(config.rootdir / 'pyproject.toml',
                                  platform=platform,
                                  output_format=output_format)

    # If no app name has been provided, check to see if there is
    # a single app in the project. If there is, use it. Otherwise,
    # raise an error.
    # If an app name has been provided, load that app config.
    if app_name == '*':
        if len(app_configs) == 1:
            app = AppConfig(**list(app_configs.values())[0])
        else:
            raise BriefcasePytestConfigError(
                'More than one app in the porject. Specify an app name with --app'
            )
    else:
        try:
            app = AppConfig(**app_configs[app_name])
        except KeyError:
            raise BriefcasePytestConfigError(
                "'{app_name}' is not an app name in this project".format(
                    app_name=app_name))

    # Process the `sources` list for the app, adding to the pythonpath.
    # This matches the PYTHONPATH configuration done by `briefcase dev`
    for path in app.PYTHONPATH:
        sys.path.insert(0, str(config.rootdir / path))
Exemple #2
0
def parse_cmdline(args):
    parser = argparse.ArgumentParser(
        prog="briefcase",
        description="Package Python code for distribution.",
        usage="briefcase [-h] <command> [<platform>] [<format>] ...",
        epilog=("Each command, platform and format has additional options. "
                "Use the -h option on a specific command for more details."),
        add_help=False,
    )
    parser.add_argument(
        "-f",
        "--formats",
        action="store_true",
        dest="show_output_formats",
        help=
        "show the available output formats and exit (specify a platform for more details).",
    )
    parser.add_argument("-V",
                        "--version",
                        action="version",
                        version=__version__)

    # <command> isn't actually optional; but if it's marked as required,
    # there's no way to get help for subcommands. So; treat <command>
    # as optional, handle the case where <command> isn't provided
    # as the case where top-level help is displayed, and provide an explicit
    # usage string so that the instructions displayed are correct
    parser.add_argument(
        "command",
        choices=[
            "new",
            "dev",
            "upgrade",
            "create",
            "update",
            "build",
            "run",
            "package",
            "publish",
        ],
        metavar="command",
        nargs="?",
        help="the command to execute (one of: %(choices)s)",
    )

    # <platform> *is* optional, with the default value based on the platform
    # that you're on.
    platforms = get_platforms()

    # 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)

    # 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)

    # If no command has been provided, display top-level help.
    if options.command is None:
        raise NoCommandError(parser.format_help())
    elif options.command == "new":
        command = NewCommand(base_path=Path.cwd())
        options = command.parse_options(extra=extra)
        return command, options
    elif options.command == "dev":
        command = DevCommand(base_path=Path.cwd())
        options = command.parse_options(extra=extra)
        return command, options
    elif options.command == "upgrade":
        command = UpgradeCommand(base_path=Path.cwd())
        options = command.parse_options(extra=extra)
        return command, options

    parser.add_argument(
        "platform",
        choices=list(platforms.keys()),
        default={
            "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 aruments, now that we know it is a command that makes use
    # of platform/output_format.
    options, extra = parser.parse_known_args(args)

    # Import the platform module
    platform_module = platforms[options.platform]

    output_formats = get_output_formats(options.platform)
    # If the user requested a list of available output formats, output them.
    if options.show_output_formats:
        raise ShowOutputFormats(
            platform=options.platform,
            default=platform_module.DEFAULT_OUTPUT_FORMAT,
            choices=list(output_formats.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
    else:
        output_format = options.output_format

    # Normalise casing of output_format to be more forgiving.
    output_format = {n.lower(): n
                     for n in output_formats}.get(output_format.lower(),
                                                  output_format)

    # We now know the command, platform, and format.
    # Get the command class that corresponds to that definition.
    try:
        format_module = output_formats[output_format]
        Command = getattr(format_module, options.command)
    except KeyError:
        raise InvalidFormatError(
            requested=output_format,
            choices=list(output_formats.keys()),
        )
    except AttributeError:
        raise UnsupportedCommandError(
            platform=options.platform,
            output_format=output_format,
            command=options.command,
        )

    # Construct a command, and parse the remaining arguments.
    command = Command(base_path=Path.cwd())
    options = command.parse_options(extra=extra)
    return command, options
Exemple #3
0
def parse_config(config_file, platform, output_format):
    """
    Parse the briefcase section of the pyproject.toml configuration file.

    This method only does basic structural parsing of the TOML, looking for,
    at a minimum, a ``[tool.briefcase.app.<appname>]`` section declaring the
    existence of a single app. It will also search for:

      * ``[tool.briefcase]`` - global briefcase settings
      * ``[tool.briefcase.app.<appname>]`` - settings specific to the app
      * ``[tool.briefcase.app.<appname>.<platform>]`` - settings specific to
        the platform
      * ``[tool.briefcase.app.<appname>.<platform>.<format>]`` - settings
        specific to the output format

    A configuration can define multiple apps; the final output is the merged
    content of the global, app, platform and output format settings
    for each app, with output format definitions taking precendence over
    platform, over app-level, over global. The final result is a single
    (mostly) flat dictionary for each app.

    :param config_file: A file-like object containing TOML to be parsed.
    :param platform: The platform being targetted
    :param output_format: The output format
    :returns: A dictionary of configuration data. The top level dictionary is
        keyed by the names of the apps that are declared; each value is
        itself the configuration data merged from global, app, platform and
        format definitions.
    """
    try:
        pyproject = toml.load(config_file)

        global_config = pyproject['tool']['briefcase']
    except toml.TomlDecodeError as e:
        raise BriefcaseConfigError('Invalid pyproject.toml: {e}'.format(e=e))
    except KeyError:
        raise BriefcaseConfigError(
            'No tool.briefcase section in pyproject.toml')

    # For consistent results, sort the platforms and formats
    all_platforms = sorted(get_platforms().keys())
    all_formats = sorted(get_output_formats(platform).keys())

    try:
        all_apps = global_config.pop('app')
    except KeyError:
        raise BriefcaseConfigError(
            'No Briefcase apps defined in pyproject.toml')

    # Build the flat configuration for each app,
    # based on the requested platform and output format
    app_configs = {}
    for app_name, app_data in all_apps.items():
        # At this point, the base configuration will contain a section
        # for each configured platform. Iterate over all the known platforms,
        # and remove these platform configurations. Keep a copy of the platform
        # configuration if it matches the requested platform, and merge it
        # into the app's configuration
        platform_data = None
        for p in all_platforms:
            try:
                platform_block = app_data.pop(p)

                if p == platform:
                    # If the platform matches the requested format, preserve
                    # it for later use.
                    platform_data = platform_block
                    merge_config(platform_data, platform_data)

                    # The platform configuration will contain a section
                    # for each configured output format. Iterate over all
                    # the output format of the platform, and remove these
                    # output format configurations. Keep a copy of the output
                    # format configuration if it matches the requested output
                    # format, and merge it into the platform's configuration.
                    format_data = None
                    for f in all_formats:
                        try:
                            format_block = platform_data.pop(f)

                            if f == output_format:
                                # If the output format matches the requested
                                # one, preserve it.
                                format_data = format_block

                        except KeyError:
                            pass

                    # If we found a specific configuration for the requested
                    # output format, add it to the platform configuration,
                    # overwriting any platform-level settings with format-level
                    # values.
                    if format_data:
                        merge_config(platform_data, format_data)

            except KeyError:
                pass

        # Now construct the final configuration.
        # First, convert the requirement definition at the global level
        merge_config(global_config, global_config)

        # The app's config starts as a copy of the base briefcase configuation.
        config = copy.deepcopy(global_config)

        # The app name is both the key, and a property of the configuration
        config['app_name'] = app_name

        # Merge the app-specific requirements
        merge_config(config, app_data)

        # If there is platform-specific configuration, merge the requirements,
        # the overwrite the platform-specific values.
        # This will already include any format-specific configuration.
        if platform_data:
            merge_config(config, platform_data)

        # Construct a configuration object, and add it to the list
        # of configurations that are being handled.
        app_configs[app_name] = config

    return global_config, app_configs