def get_report(self) -> str: """Get verbose report. :return: Report why the part is dirty. :rtype: str """ messages = [] # type: List[str] if self.dirty_properties: humanized_properties = formatting_utils.humanize_list( self.dirty_properties, "and") pluralized_connection = formatting_utils.pluralize( self.dirty_properties, "property appears", "properties appear") messages.append("The {} part {} to have changed.\n".format( humanized_properties, pluralized_connection)) if self.dirty_project_options: humanized_options = formatting_utils.humanize_list( self.dirty_project_options, "and") pluralized_connection = formatting_utils.pluralize( self.dirty_project_options, "option appears", "options appear") messages.append("The {} project {} to have changed.\n".format( humanized_options, pluralized_connection)) if self.changed_dependencies: dependencies = [d.part_name for d in self.changed_dependencies] messages.append("{} changed: {}\n".format( formatting_utils.pluralize(dependencies, "A dependency has", "Some dependencies have"), formatting_utils.humanize_list(dependencies, "and"), )) return "".join(messages)
def _handle_macaroon_permission_required(response_json): if "permission" in response_json and "channels" in response_json: if response_json["permission"] == "channel": channels = response_json["channels"] return "Lacking permission to release to channel(s) {}".format( formatting_utils.humanize_list(channels, "and")) return ""
def __init__(self, step: steps.Step, other_step: steps.Step, keys: List[str]) -> None: self.keys = keys super().__init__( step=step, other_step=other_step, humanized_keys=formatting_utils.humanize_list(keys, "and"), )
def test_two_items(self): items = ["foo", "bar"] output = formatting_utils.humanize_list(items, "and") self.assertThat( output, Equals("'bar' and 'foo'"), "Expected 'bar' before 'foo' due to sorting", )
def __init__(self, *, base: str, version: str, valid_versions: Sequence[str]) -> None: super().__init__( base=base, version=version, valid_versions=formatting_utils.humanize_list(valid_versions, conjunction="or"), )
def _monitor_build(lp: LaunchpadClient) -> None: target_list = humanize_list(lp.architectures, "and", "{}") echo.info( f"Building snap package for {target_list}. This may take some time to finish." ) lp.monitor_build() echo.info("Build complete.") lp.cleanup()
def __init__( self, *, snap_name: str, channel: channels.Channel, channel_outliers: List[status.SnapStatusChannelDetails], ) -> None: arches = formatting_utils.humanize_list( [c.arch for c in channel_outliers], "and") super().__init__(snap_name=snap_name, channel=channel, arches=arches)
def _determine_cause(error): messages = [] # error.validator_value may contain a custom validation error message. # If so, use it instead of the garbage message jsonschema gives us. with contextlib.suppress(TypeError, KeyError): messages.append( error.validator_value["validation-failure"].format(error)) # The schema itself may have a custom validation error message. If so, # use it as well. with contextlib.suppress(AttributeError, TypeError, KeyError): key = error if (error.schema.get("type") == "object" and error.validator == "additionalProperties"): key = list(error.instance.keys())[0] messages.append(error.schema["validation-failure"].format(key)) # anyOf failures might have usable context... try to improve them a bit if error.validator == "anyOf": contextual_messages: Dict[str, str] = OrderedDict() for contextual_error in error.context: key = contextual_error.schema_path.popleft() if key not in contextual_messages: contextual_messages[key] = [] message = contextual_error.message if message: # Sure it starts lower-case (not all messages do) contextual_messages[key].append(message[0].lower() + message[1:]) oneOf_messages: List[str] = [] for key, value in contextual_messages.items(): oneOf_messages.append( formatting_utils.humanize_list(value, "and", "{}")) messages.append( formatting_utils.humanize_list(oneOf_messages, "or", "{}")) return " ".join(messages)
def get_summary(self) -> str: """Get summarized report. :return: Short summary of why the step is dirty. :rtype: str """ reasons = [] if self.previous_step_modified: reasons.append("{!r} step".format(self.previous_step_modified.name)) if self.source_updated: reasons.append("source") return "{} changed".format(formatting_utils.humanize_list(reasons, "and", "{}"))
def _interpret_anyOf(error): """Interpret a validation error caused by the anyOf validator. Returns: A string containing a (hopefully) helpful validation error message. It may be empty. """ usages = [] try: for validator in error.validator_value: usages.append(validator["usage"]) except (TypeError, KeyError): return "" return "must be one of {}".format( formatting_utils.humanize_list(usages, "or", "{}"))
def _adopt_info( config_data: Dict[str, Any], extracted_metadata: _metadata.ExtractedMetadata, prime_dir: str, ): ignored_keys = _adopt_keys(config_data, extracted_metadata, prime_dir) if ignored_keys: logger.warning( "The {keys} {plural_property} {plural_is} specified in adopted " "info as well as the YAML: taking the {plural_property} from the " "YAML".format( keys=formatting_utils.humanize_list(list(ignored_keys), "and"), plural_property=formatting_utils.pluralize( ignored_keys, "property", "properties"), plural_is=formatting_utils.pluralize(ignored_keys, "is", "are"), ))
def warn_ld_library_paths(self) -> None: root_ld_library_path = self._snap_meta.environment.get( "LD_LIBRARY_PATH") # Dictionary of app names with LD_LIBRARY_PATH in their environment. app_environment: Dict[str, str] = dict() for app_name, app_props in self._config_data.get("apps", dict()).items(): with contextlib.suppress(KeyError): app_environment[app_name] = app_props["environment"][ "LD_LIBRARY_PATH"] if root_ld_library_path is None and not app_environment: return ld_library_path_empty: Set[str] = set() if root_ld_library_path is None and app_environment: ld_library_path_empty = { name for name, ld_env in app_environment.items() if "$LD_LIBRARY_PATH" in ld_env or "${LD_LIBRARY_PATH}" in ld_env } elif (root_ld_library_path is not None and "LD_LIBRARY_PATH" in root_ld_library_path): ld_library_path_empty = {"."} _EMPTY_LD_LIBRARY_PATH_ITEM_PATTERN = re.compile("^:|::|:$") for name, ld_env in app_environment.items(): if _EMPTY_LD_LIBRARY_PATH_ITEM_PATTERN.findall(ld_env): ld_library_path_empty.add(name) if (root_ld_library_path is not None and _EMPTY_LD_LIBRARY_PATH_ITEM_PATTERN.findall( root_ld_library_path)): ld_library_path_empty.add(".") if ld_library_path_empty: logger.warning( "CVE-2020-27348: A potentially empty LD_LIBRARY_PATH has been set for environment " "in {}. " "The current working directory will be added to the library path if empty. " "This can cause unexpected libraries to be loaded.".format( formatting_utils.humanize_list( sorted(ld_library_path_empty), "and")))
def get_summary(self) -> str: """Get summarized report. :return: Short summary of why the part is dirty. :rtype: str """ reasons = [] reasons_count = 0 if self.dirty_properties: reasons_count += 1 if self.dirty_project_options: reasons_count += 1 if self.changed_dependencies: reasons_count += 1 if self.dirty_properties: # Be specific only if this is the only reason if reasons_count > 1 or len(self.dirty_properties) > 1: reasons.append("properties") else: reasons.append("{!r} property".format( next(iter(self.dirty_properties)))) if self.dirty_project_options: # Be specific only if this is the only reason if reasons_count > 1 or len(self.dirty_project_options) > 1: reasons.append("options") else: reasons.append("{!r} option".format( next(iter(self.dirty_project_options)))) if self.changed_dependencies: # Be specific only if this is the only reason if reasons_count > 1 or len(self.changed_dependencies) > 1: reasons.append("dependencies") else: reasons.append("{!r}".format( next(iter(self.changed_dependencies)).part_name)) return "{} changed".format( formatting_utils.humanize_list(reasons, "and", "{}"))
def _clean_parts(part_names, step, config, staged_state, primed_state): if not step: step = steps.next_step(None) for part_name in part_names: dirty_parts = _clean_part(part_name, step, config, staged_state, primed_state) dirty_part_names = {p.name for p in dirty_parts} parts_not_being_cleaned = dirty_part_names.difference(part_names) if parts_not_being_cleaned: logger.warning( "Cleaned {!r}, which makes the following {} out of date: " "{}".format( part_name, formatting_utils.pluralize( parts_not_being_cleaned, "part", "parts" ), formatting_utils.humanize_list(parts_not_being_cleaned, "and"), ) )
def test_another_conjunction(self): items = ["foo", "bar", "baz", "qux"] output = formatting_utils.humanize_list(items, "or") self.assertThat(output, Equals("'bar', 'baz', 'foo', or 'qux'"))
def test_four_items(self): items = ["foo", "bar", "baz", "qux"] output = formatting_utils.humanize_list(items, "and") self.assertThat(output, Equals("'bar', 'baz', 'foo', and 'qux'"))
def __init__(self, *, architecture: str, supported: List[str]) -> None: super().__init__( architecture=architecture, supported=formatting_utils.humanize_list(supported, "and"), )
def test_one_item(self): items = ["foo"] output = formatting_utils.humanize_list(items, "and") self.assertThat(output, Equals("'foo'"))
def test_no_items(self): items = [] output = formatting_utils.humanize_list(items, "and") self.assertThat(output, Equals(""))
def __init__(self, keys: List[str]) -> None: super().__init__(keys=formatting_utils.humanize_list(keys, "and"))
def _validate_architectures(instance): standalone_build_ons = collections.Counter() build_ons = collections.Counter() run_ons = collections.Counter() saw_strings = False saw_dicts = False for item in instance: # This could either be a dict or a string. In the latter case, the # schema will take care of it. We just need to further validate the # dict. if isinstance(item, str): saw_strings = True elif isinstance(item, dict): saw_dicts = True build_on = _get_architectures_set(item, "build-on") build_ons.update(build_on) # Add to the list of run-ons. However, if no run-on is specified, # we know it's implicitly the value of build-on, so use that # for validation instead. run_on = _get_architectures_set(item, "run-on") if run_on: run_ons.update(run_on) else: standalone_build_ons.update(build_on) # Architectures can either be a list of strings, or a list of objects. # Mixing the two forms is unsupported. if saw_strings and saw_dicts: raise jsonschema.exceptions.ValidationError( "every item must either be a string or an object", path=["architectures"], instance=instance, ) # At this point, individual build-ons and run-ons have been validated, # we just need to validate them across each other. # First of all, if we have a `run-on: [all]` (or a standalone # `build-on: [all]`) then we should only have one item in the instance, # otherwise we know we'll have multiple snaps claiming they run on the same # architectures (i.e. all and something else). number_of_snaps = len(instance) if "all" in run_ons and number_of_snaps > 1: raise jsonschema.exceptions.ValidationError( "one of the items has 'all' in 'run-on', but there are {} " "items: upon release they will conflict. 'all' should only be " "used if there is a single item".format(number_of_snaps), path=["architectures"], instance=instance, ) if "all" in build_ons and number_of_snaps > 1: raise jsonschema.exceptions.ValidationError( "one of the items has 'all' in 'build-on', but there are {} " "items: snapcraft doesn't know which one to use. 'all' should " "only be used if there is a single item".format(number_of_snaps), path=["architectures"], instance=instance, ) # We want to ensure that multiple `run-on`s (or standalone `build-on`s) # don't include the same arch, or they'll clash with each other when # releasing. all_run_ons = run_ons + standalone_build_ons duplicates = {arch for (arch, count) in all_run_ons.items() if count > 1} if duplicates: raise jsonschema.exceptions.ValidationError( "multiple items will build snaps that claim to run on {}".format( formatting_utils.humanize_list(duplicates, "and")), path=["architectures"], instance=instance, ) # Finally, ensure that multiple `build-on`s don't include the same arch # or Snapcraft has no way of knowing which one to use. duplicates = {arch for (arch, count) in build_ons.items() if count > 1} if duplicates: raise jsonschema.exceptions.ValidationError( "{} {} present in the 'build-on' of multiple items, which means " "snapcraft doesn't know which 'run-on' to use when building on " "{} {}".format( formatting_utils.humanize_list(duplicates, "and"), formatting_utils.pluralize(duplicates, "is", "are"), formatting_utils.pluralize(duplicates, "that", "those"), formatting_utils.pluralize(duplicates, "architecture", "architectures"), ), path=["architectures"], instance=instance, ) return True
def __init__(self, source_type: str, options: List[str]) -> None: self.options = options super().__init__( source_type=source_type, humanized_options=formatting_utils.humanize_list(options, "and"), )
def get_brief(self) -> str: missing_pkgs = formatting_utils.humanize_list(self.packages, "and") return f"Failed to find installation candidate for packages: {missing_pkgs}"