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