def run(self, parsed_args): extension_presentation: Dict[str, ExtensionModel] = {} # New extensions. for extension_name in extensions.registry.get_extension_names(): extension_class = extensions.registry.get_extension_class( extension_name) extension_bases = list(extension_class.get_supported_bases()) extension_presentation[extension_name] = ExtensionModel( name=extension_name, bases=extension_bases) # Extensions from snapcraft_legacy. for extension_name in supported_extension_names(): extension_class = find_extension(extension_name) extension_name = extension_name.replace("_", "-") extension_bases = list(extension_class.get_supported_bases()) if extension_name in extension_presentation: extension_presentation[extension_name].bases += extension_bases else: extension_presentation[extension_name] = ExtensionModel( name=extension_name, bases=extension_bases) printable_extensions = sorted( [v.marshal() for v in extension_presentation.values()], key=lambda d: d["Extension name"], ) emit.message(tabulate.tabulate(printable_extensions, headers="keys"))
def run(self, parsed_args): snap_file = pathlib.Path(parsed_args.snap_file) if not snap_file.exists() or not snap_file.is_file(): raise ArgumentParsingError( f"{str(snap_file)!r} is not a valid file") channels: Optional[List[str]] = None if parsed_args.channels: channels = parsed_args.channels.split(",") client = store.StoreClientCLI() snap_yaml = get_data_from_snap_file(snap_file) snap_name = snap_yaml["name"] built_at = snap_yaml.get("snapcraft-started-at") client.verify_upload(snap_name=snap_name) upload_id = client.store_client.upload_file( filepath=snap_file, monitor_callback=create_callback) revision = client.notify_upload( snap_name=snap_name, upload_id=upload_id, built_at=built_at, channels=channels, snap_file_size=snap_file.stat().st_size, ) message = f"Revision {revision!r} created for {snap_name!r}" if channels: message += f" and released to {utils.humanize_list(channels, 'and')}" emit.message(message)
def _pack_charm(self, parsed_args) -> List[pathlib.Path]: """Pack a charm.""" emit.progress("Packing the charm.") # adapt arguments to use the build infrastructure build_args = Namespace( **{ "debug": parsed_args.debug, "destructive_mode": parsed_args.destructive_mode, "from": self.config.project.dirpath, "entrypoint": parsed_args.entrypoint, "requirement": parsed_args.requirement, "shell": parsed_args.shell, "shell_after": parsed_args.shell_after, "bases_indices": parsed_args.bases_index, "force": parsed_args.force, }) # mimic the "build" command validator = build.Validator(self.config) args = validator.process(build_args) emit.trace(f"Working arguments: {args}") builder = build.Builder(args, self.config) charms = builder.run(parsed_args.bases_index, destructive_mode=build_args.destructive_mode) emit.message("Charms packed:") for charm in charms: emit.message(f" {charm}")
def _parse_and_reformat_section(self, *, section, icon_path: Optional[str] = None): if "Exec" not in self._parser[section]: raise errors.DesktopFileError(self._filename, "missing 'Exec' key") self._parse_and_reformat_section_exec(section) if "Icon" in self._parser[section]: icon = self._parser[section]["Icon"] if icon_path is not None: icon = icon_path # Strip any leading slash. icon = icon[1:] if icon.startswith("/") else icon # Strip any leading ${SNAP}. icon = icon[8:] if icon.startswith("${SNAP}") else icon # With everything stripped, check to see if the icon is there. # if it is, add "${SNAP}" back and set the icon if (self._prime_dir / icon).is_file(): self._parser[section]["Icon"] = os.path.join("${SNAP}", icon) else: emit.message( f"Icon {icon!r} specified in desktop file {self._filename!r} " f"not found in prime directory." )
def show_linting_results(self, linting_results): """Manage the linters results, show some in different conditions, decide if continue.""" attribute_results = [] lint_results_by_outcome = {} for result in linting_results: if result.result == linters.IGNORED: continue if result.check_type == linters.CheckType.attribute: attribute_results.append(result) else: lint_results_by_outcome.setdefault(result.result, []).append(result) # show attribute results for result in attribute_results: emit.trace( f"Check result: {result.name} [{result.check_type}] {result.result} " f"({result.text}; see more at {result.url}).", ) # show warnings (if any), then errors (if any) template = "- {0.name}: {0.text} ({0.url})" if linters.WARNINGS in lint_results_by_outcome: emit.message("Lint Warnings:", intermediate=True) for result in lint_results_by_outcome[linters.WARNINGS]: emit.message(template.format(result), intermediate=True) if linters.ERRORS in lint_results_by_outcome: emit.message("Lint Errors:", intermediate=True) for result in lint_results_by_outcome[linters.ERRORS]: emit.message(template.format(result), intermediate=True) if self.force_packing: emit.message("Packing anyway as requested.", intermediate=True) else: raise CommandError( "Aborting due to lint errors (use --force to override).", retcode=2)
def request(self, *args, **kwargs) -> requests.Response: """Request using the BaseClient and wrap responses that require action. Actionable items are those that could prompt a login or registration. """ try: return self.store_client.request(*args, **kwargs) except craft_store.errors.StoreServerError as store_error: if (store_error.response.status_code == requests.codes.unauthorized # pylint: disable=no-member ): if os.getenv(constants.ENVIRONMENT_STORE_CREDENTIALS): raise errors.SnapcraftError( "Provided credentials are no longer valid for the Snap Store.", resolution="Regenerate them and try again.", ) from store_error emit.message("You are required to re-login before continuing") self.store_client.logout() else: raise except craft_store.errors.CredentialsUnavailable: emit.message("You are required to login before continuing") self.login() return self.store_client.request(*args, **kwargs)
def _pack_charm(self, parsed_args) -> List[pathlib.Path]: """Pack a charm.""" emit.progress("Packing the charm.") # adapt arguments to use the build infrastructure build_args = Namespace( **{ "debug": parsed_args.debug, "destructive_mode": parsed_args.destructive_mode, "from": self.config.project.dirpath, "entrypoint": parsed_args.entrypoint, "requirement": parsed_args.requirement, "shell": parsed_args.shell, "shell_after": parsed_args.shell_after, "bases_indices": parsed_args.bases_index, "force": parsed_args.force, }) # mimic the "build" command validator = build.Validator(self.config) args = validator.process(build_args) emit.trace(f"Working arguments: {args}") builder = build.Builder(args, self.config) charms = builder.run(parsed_args.bases_index, destructive_mode=build_args.destructive_mode) # avoid showing results when run inside a container (the outer charmcraft # is responsible of the final message to the user) if not env.is_charmcraft_running_in_managed_mode(): emit.message("Charms packed:") for charm in charms: emit.message(f" {charm}")
def run(self, parsed_args): """Run the command.""" if parsed_args.directory: snap_filename = pack.pack_snap(parsed_args.directory, output=parsed_args.output) emit.message(f"Created snap package {snap_filename}") else: super().run(parsed_args)
def _prompt_login() -> Tuple[str, str]: emit.message("Enter your Ubuntu One e-mail address and password.") emit.message( "If you do not have an Ubuntu One account, you can create one " "at https://snapcraft.io/account", ) email = utils.prompt("Email: ") password = utils.prompt("Password: ", hide=True) return (email, password)
def run(self, parsed_args): """Run the command.""" project_path = self.config.project.dirpath metadata = parse_metadata_yaml(project_path) emit.progress(f"Cleaning project {metadata.name!r}.") provider = get_provider() provider.clean_project_environments(charm_name=metadata.name, project_path=project_path) emit.message(f"Cleaned project {metadata.name!r}.")
def run(self, parsed_args): if parsed_args.experimental_login: raise ArgumentParsingError( "--experimental-login no longer supported. " f"Set {store.constants.ENVIRONMENT_STORE_AUTH}=candid instead", ) kwargs: Dict[str, Union[str, int]] = {} if parsed_args.snaps: kwargs["packages"] = parsed_args.snaps.split(",") if parsed_args.channels: kwargs["channels"] = parsed_args.channels.split(",") if parsed_args.acls: kwargs["acls"] = parsed_args.acls.split(",") if parsed_args.expires is not None: for date_format in _VALID_DATE_FORMATS: with contextlib.suppress(ValueError): expiry_date = datetime.strptime(parsed_args.expires, date_format) break else: valid_formats = utils.humanize_list(_VALID_DATE_FORMATS, "or") raise ArgumentParsingError( f"The expiry follow an ISO 8601 format ({valid_formats})") kwargs["ttl"] = int((expiry_date - datetime.now()).total_seconds()) credentials = store.StoreClientCLI(ephemeral=True).login(**kwargs) # Support a login_file of '-', which indicates a desire to print to stdout if parsed_args.login_file.strip() == "-": message = f"Exported login credentials:\n{credentials}" else: # This is sensitive-- it should only be accessible by the owner private_open = functools.partial(os.open, mode=0o600) with open(parsed_args.login_file, "w", opener=private_open, encoding="utf-8") as login_fd: print(credentials, file=login_fd, end="") # Now that the file has been written, we can just make it # owner-readable os.chmod(parsed_args.login_file, stat.S_IRUSR) message = f"Exported login credentials to {parsed_args.login_file!r}" message += "\n\nThese credentials must be used on Snapcraft 7.0 or greater." if os.getenv(store.constants.ENVIRONMENT_STORE_AUTH) == "candid": message += ( f"\nSet '{store.constants.ENVIRONMENT_STORE_AUTH}=candid' for " "these credentials to work.") emit.message(message)
def run(self, parsed_args): snap_project = get_snap_project() yaml_data = process_yaml(snap_project.project_file) expanded_yaml_data = extensions.apply_extensions( yaml_data, arch=get_host_architecture(), target_arch=get_host_architecture(), ) Project.unmarshal(expanded_yaml_data) emit.message( yaml.safe_dump(expanded_yaml_data, indent=4, sort_keys=False))
def build_charm(self, bases_config: BasesConfiguration) -> str: """Build the charm. :param bases_config: Bases configuration to use for build. :returns: File name of charm. :raises CraftError: on lifecycle exception. :raises RuntimeError: on unexpected lifecycle exception. """ if env.is_charmcraft_running_in_managed_mode(): work_dir = env.get_managed_environment_home_path() else: work_dir = self.buildpath emit.progress(f"Building charm in {str(work_dir)!r}") if self._special_charm_part: # all current deprecated arguments set charm plugin parameters self._handle_deprecated_cli_arguments() # add charm files to the prime filter self._set_prime_filter() # set source if empty or not declared in charm part if not self._special_charm_part.get("source"): self._special_charm_part["source"] = str(self.charmdir) # run the parts lifecycle emit.trace(f"Parts definition: {self._parts}") lifecycle = parts.PartsLifecycle( self._parts, work_dir=work_dir, project_dir=self.charmdir, project_name=self.metadata.name, ignore_local_sources=["*.charm"], ) lifecycle.run(Step.PRIME) # run linters and show the results linting_results = linters.analyze(self.config, lifecycle.prime_dir) self.show_linting_results(linting_results) create_manifest( lifecycle.prime_dir, self.config.project.started_at, bases_config, linting_results, ) zipname = self.handle_package(lifecycle.prime_dir, bases_config) emit.message(f"Created '{zipname}'.", intermediate=True) return zipname
def run(self, parsed_args): channels = parsed_args.channels.split(",") store.StoreClientCLI().release( snap_name=parsed_args.name, revision=parsed_args.revision, channels=channels, progressive_percentage=parsed_args.progressive_percentage, ) humanized_channels = utils.humanize_list(channels, conjunction="and") emit.message(f"Released {parsed_args.name!r} " f"revision {parsed_args.revision!r} " f"to channels: {humanized_channels}")
def _run_dispatcher(dispatcher: craft_cli.Dispatcher) -> None: global_args = dispatcher.pre_parse_args(sys.argv[1:]) if global_args.get("version"): emit.message(f"snapcraft {__version__}") else: if global_args.get("trace"): emit.message( "Options -t and --trace are deprecated, use --verbosity=debug instead." ) emit.set_mode(EmitterMode.DEBUG) dispatcher.load_command(None) dispatcher.run() emit.ended_ok()
def notify_deprecation(deprecation_id): """Present proper messages to the user for the indicated deprecation id. Prevent issuing duplicate warnings to the user by ignoring notifications if: - running in managed-mode - already issued by running process """ if is_charmcraft_running_in_managed_mode() or deprecation_id in _ALREADY_NOTIFIED: return message = _DEPRECATION_MESSAGES[deprecation_id] emit.message(f"DEPRECATED: {message}", intermediate=True) url = _DEPRECATION_URL_FMT.format(deprecation_id=deprecation_id) emit.message(f"See {url} for more information.", intermediate=True) _ALREADY_NOTIFIED.add(deprecation_id)
def get_build_plan(yaml_data: Dict[str, Any], parsed_args: "argparse.Namespace") -> List[Tuple[str, str]]: """Get a list of all build_on->build_for architectures from the project file. Additionally, check for the command line argument `--build-for <architecture>` When defined, the build plan will only contain builds where `build-for` matches `SNAPCRAFT_BUILD_FOR`. Note: `--build-for` defaults to the environmental variable `SNAPCRAFT_BUILD_FOR`. :param yaml_data: The project YAML data. :param parsed_args: snapcraft's argument namespace :return: List of tuples of every valid build-on->build-for combination. """ archs = ArchitectureProject.unmarshal(yaml_data).architectures host_arch = get_host_architecture() build_plan: List[Tuple[str, str]] = [] # `isinstance()` calls are for mypy type checking and should not change logic for arch in [arch for arch in archs if isinstance(arch, Architecture)]: for build_on in arch.build_on: if build_on in host_arch and isinstance(arch.build_for, list): build_plan.append((host_arch, arch.build_for[0])) else: emit.verbose( f"Skipping build-on: {build_on} build-for: {arch.build_for}" f" because build-on doesn't match host arch: {host_arch}") # filter out builds not matching argument `--build_for` or env `SNAPCRAFT_BUILD_FOR` build_for_arg = parsed_args.build_for if build_for_arg is not None: build_plan = [ build for build in build_plan if build[1] == build_for_arg ] if len(build_plan) == 0: emit.message("Could not make build plan:" " build-on architectures in snapcraft.yaml" f" does not match host architecture ({host_arch}).") else: log_output = "Created build plan:" for build in build_plan: log_output += f"\n build-on: {build[0]} build-for: {build[1]}" emit.trace(log_output) return build_plan
def plan( self, *, bases_indices: Optional[List[int]], destructive_mode: bool, managed_mode: bool ) -> List[Tuple[BasesConfiguration, Base, int, int]]: """Determine the build plan based on user inputs and host environment. Provide a list of bases that are buildable and scoped according to user configuration. Provide all relevant details including the applicable bases configuration and the indices of the entries to build for. :returns: List of Tuples (bases_config, build_on, bases_index, build_on_index). """ build_plan: List[Tuple[BasesConfiguration, Base, int, int]] = [] for bases_index, bases_config in enumerate(self.config.bases): if bases_indices and bases_index not in bases_indices: emit.trace( f"Skipping 'bases[{bases_index:d}]' due to --base-index usage." ) continue for build_on_index, build_on in enumerate(bases_config.build_on): if managed_mode or destructive_mode: matches, reason = check_if_base_matches_host(build_on) else: matches, reason = self.provider.is_base_available(build_on) if matches: emit.trace( f"Building for 'bases[{bases_index:d}]' " f"as host matches 'build-on[{build_on_index:d}]'.", ) build_plan.append( (bases_config, build_on, bases_index, build_on_index)) break else: emit.progress( f"Skipping 'bases[{bases_index:d}].build-on[{build_on_index:d}]': " f"{reason}.", ) else: emit.message( "No suitable 'build-on' environment found " f"in 'bases[{bases_index:d}]' configuration.", intermediate=True, ) return build_plan
def run(self, parsed_args): # dest does not work when filling the parser so getattr instead snap_name = getattr(parsed_args, "snap-name") if parsed_args.private: emit.progress( _MESSAGE_REGISTER_PRIVATE.format(snap_name), permanent=True, ) if parsed_args.yes or utils.confirm_with_user( _MESSAGE_REGISTER_CONFIRM.format(snap_name)): store.StoreClientCLI().register(snap_name, is_private=parsed_args.private, store_id=parsed_args.store_id) emit.message(_MESSAGE_REGISTER_SUCCESS.format(snap_name)) else: emit.message(_MESSAGE_REGISTER_NO.format(snap_name))
def run(self, parsed_args): snap_channel_map = store.StoreClientCLI().get_channel_map( snap_name=parsed_args.name ) existing_architectures = snap_channel_map.get_existing_architectures() if not snap_channel_map.channel_map: emit.message("This snap has no released revisions") return architectures = existing_architectures if parsed_args.arch: architectures = set(parsed_args.arch) for architecture in architectures.copy(): if architecture not in existing_architectures: emit.progress(f"No revisions for architecture {architecture!r}") architectures.remove(architecture) # If we have no revisions for any of the architectures requested, there's # nothing to do here. if not architectures: return tracks: List[str] = [] if parsed_args.track: tracks = cast(list, parsed_args.track) existing_tracks = { s.track for s in snap_channel_map.snap.channels if s.track in tracks } for track in set(tracks) - existing_tracks: emit.progress(f"No revisions for track {track!r}") tracks = list(existing_tracks) # If we have no revisions in any of the tracks requested, there's # nothing to do here. if not tracks: return emit.message( get_tabulated_channel_map( snap_channel_map, architectures=list(architectures), tracks=tracks, ) )
def _clean_provider(project: Project, parsed_args: "argparse.Namespace") -> None: """Clean the provider environment. :param project: The project to clean. """ emit.debug("Clean build provider") provider_name = "lxd" if parsed_args.use_lxd else None provider = providers.get_provider(provider_name) instance_names = provider.clean_project_environments( project_name=project.name, project_path=Path().absolute(), build_on=get_host_architecture(), build_for=get_host_architecture(), ) if instance_names: emit.message(f"Removed instance: {', '.join(instance_names)}") else: emit.message("No instances to remove")
def run(self, parsed_args): if parsed_args.experimental_login: raise ArgumentParsingError( "--experimental-login no longer supported. " f"Set {store.constants.ENVIRONMENT_STORE_AUTH}=candid instead", ) if parsed_args.login_with: config_content = _read_config(parsed_args.login_with) emit.progress( "--with is no longer supported, export the auth to the environment " f"variable {store.constants.ENVIRONMENT_STORE_CREDENTIALS!r} instead", permanent=True, ) store.LegacyUbuntuOne.store_credentials(config_content) else: store.StoreClientCLI().login() emit.message("Login successful")
def get_elf_files(root: str, file_list: Set[str]) -> FrozenSet[ElfFile]: """Return a frozenset of ELF files from file_list prepended with root. :param str root: the root directory from where the file_list is generated. :param file_list: a list of file in root. :returns: a frozentset of ElfFile objects. """ elf_files: Set[ElfFile] = set() for part_file in file_list: # Filter out object (*.o) files-- we only care about binaries. if part_file.endswith(".o"): continue # No need to crawl links-- the original should be here, too. path = Path(root, part_file) if os.path.islink(path): emit.debug(f"Skipped link {path!r} while finding dependencies") continue # Ignore if file does not have ELF header. if not ElfFile.is_elf(path): continue try: elf_file = ElfFile(path=path) except elftools.common.exceptions.ELFError: # Ignore invalid ELF files. continue except errors.CorruptedElfFile as exception: # Log if the ELF file seems corrupted emit.message(str(exception)) continue # If ELF has dynamic symbols, add it. if elf_file.needed: elf_files.add(elf_file) return frozenset(elf_files)
def run(self, parsed_args): client = store.StoreClientCLI() # Account info request to retrieve the snap-id account_info = client.get_account_info() try: snap_id = account_info["snaps"][store.constants.DEFAULT_SERIES][ parsed_args.name]["snap-id"] except KeyError as key_error: emit.debug(f"{key_error!r} no found in {account_info!r}") raise errors.SnapcraftError( f"{parsed_args.name!r} not found or not owned by this account" ) from key_error client.close( snap_id=snap_id, channel=parsed_args.channel, ) emit.message( f"Channel {parsed_args.channel!r} for {parsed_args.name!r} is now closed" )
def run(self, parsed_args): whoami = store.StoreClientCLI().store_client.whoami() if whoami.get("permissions"): permissions = ", ".join(whoami["permissions"]) else: permissions = "no restrictions" if whoami.get("channels"): channels = ", ".join(whoami["channels"]) else: channels = "no restrictions" account = whoami["account"] message = textwrap.dedent(f"""\ email: {account["email"]} username: {account["username"]} id: {account["id"]} permissions: {permissions} channels: {channels} expires: {whoami["expires"]}Z""") emit.message(message)
def run(self, parsed_args): snap_channel_map = store.StoreClientCLI().get_channel_map( snap_name=parsed_args.name ) # Iterate over the entries, replace None with - for consistent presentation track_table: List[List[str]] = [ [ track.name, track.status, track.creation_date if track.creation_date else "-", track.version_pattern if track.version_pattern else "-", ] for track in snap_channel_map.snap.tracks ] emit.message( tabulate( # Sort by "creation-date". sorted(track_table, key=operator.itemgetter(2)), headers=["Name", "Status", "Creation-Date", "Version-Pattern"], tablefmt="plain", ) )
def run(self, parsed_args): account_info = store.StoreClientCLI().get_account_info() snaps = [ ( name, info["since"], "private" if info["private"] else "public", "-", ) for name, info in account_info["snaps"].get( store.constants.DEFAULT_SERIES, {}).items() # Presenting only approved snap registrations, which means name # disputes will be displayed/sorted some other way. if info["status"] == "Approved" ] if not snaps: emit.message("No registered snaps") else: tabulated_snaps = tabulate( sorted(snaps, key=operator.itemgetter(0)), headers=["Name", "Since", "Visibility", "Notes"], tablefmt="plain", ) emit.message(tabulated_snaps)
def _pack_bundle(self, parsed_args) -> List[pathlib.Path]: """Pack a bundle.""" emit.progress("Packing the bundle.") if parsed_args.shell: build.launch_shell() return [] project = self.config.project if self.config.parts: config_parts = self.config.parts.copy() else: # "parts" not declared, create an implicit "bundle" part config_parts = {"bundle": {"plugin": "bundle"}} # a part named "bundle" using plugin "bundle" is special and has # predefined values set automatically. bundle_part = config_parts.get("bundle") if bundle_part and bundle_part.get("plugin") == "bundle": special_bundle_part = bundle_part else: special_bundle_part = None # get the config files bundle_filepath = project.dirpath / "bundle.yaml" bundle_config = load_yaml(bundle_filepath) if bundle_config is None: raise CraftError( "Missing or invalid main bundle file: {!r}.".format( str(bundle_filepath))) bundle_name = bundle_config.get("name") if not bundle_name: raise CraftError( "Invalid bundle config; missing a 'name' field indicating the bundle's name in " "file {!r}.".format(str(bundle_filepath))) if special_bundle_part: # set prime filters for fname in MANDATORY_FILES: fpath = project.dirpath / fname if not fpath.exists(): raise CraftError("Missing mandatory file: {!r}.".format( str(fpath))) prime = special_bundle_part.setdefault("prime", []) prime.extend(MANDATORY_FILES) # set source if empty or not declared in charm part if not special_bundle_part.get("source"): special_bundle_part["source"] = str(project.dirpath) if env.is_charmcraft_running_in_managed_mode(): work_dir = env.get_managed_environment_home_path() else: work_dir = project.dirpath / build.BUILD_DIRNAME # run the parts lifecycle emit.trace(f"Parts definition: {config_parts}") lifecycle = parts.PartsLifecycle( config_parts, work_dir=work_dir, project_dir=project.dirpath, project_name=bundle_name, ignore_local_sources=[bundle_name + ".zip"], ) try: lifecycle.run(Step.PRIME) except (RuntimeError, CraftError) as error: if parsed_args.debug: emit.trace(f"Error when running PRIME step: {error}") build.launch_shell() raise # pack everything create_manifest(lifecycle.prime_dir, project.started_at, None, []) zipname = project.dirpath / (bundle_name + ".zip") build_zip(zipname, lifecycle.prime_dir) emit.message(f"Created {str(zipname)!r}.") if parsed_args.shell_after: build.launch_shell() return [zipname]
def run(self, parsed_args): """Run the command.""" emit.message(f"snapcraft {__version__}")
def run(self, parsed_args): """Run the command.""" tmpdir = self._unzip_charm(parsed_args.filepath) # run the analyzer override_ignore_config = bool(parsed_args.force) linting_results = linters.analyze( self.config, tmpdir, override_ignore_config=override_ignore_config, ) # if format is json almost no further processing is needed if parsed_args.format == JSON_FORMAT: info = [{ "name": r.name, "result": r.result, "url": r.url, "type": r.check_type, } for r in linting_results] emit.message(json.dumps(info, indent=4)) return # group by attributes and lint outcomes (discarding ignored ones) grouped = {} for result in linting_results: if result.check_type == linters.CheckType.attribute: group_key = linters.CheckType.attribute result_info = result.result else: # linters group_key = result.result if result.result == linters.OK: result_info = "no issues found" elif result.result in (linters.FATAL, linters.IGNORED): result_info = None else: result_info = result.text grouped.setdefault(group_key, []).append((result, result_info)) # present the results titles = [ ("Attributes", linters.CheckType.attribute), ("Lint Ignored", linters.IGNORED), ("Lint Warnings", linters.WARNINGS), ("Lint Errors", linters.ERRORS), ("Lint Fatal", linters.FATAL), ("Lint OK", linters.OK), ] for title, key in titles: results = grouped.get(key) if results is not None: emit.message(f"{title}:") for result, result_info in results: if result_info: emit.message( f"- {result.name}: { result_info} ({result.url})") else: emit.message(f"- {result.name} ({result.url})") # the return code depends on the presence of different issues if linters.FATAL in grouped: retcode = 1 elif linters.ERRORS in grouped: retcode = 2 elif linters.WARNINGS in grouped: retcode = 3 else: retcode = 0 return retcode