def skip_if_requirement(*requirements_str: str): ''' Skip the decorated test if one or more of the dependencies identified by the passed :mod:`setuptools`-formatted requirement strings are **satisfiable** (i.e., importable *and* of satisfactory version). Parameters ---------- requirements_str : str Tuple of all :mod:`setuptools`-formatted requirement strings identifying these dependencies (e.g., `Numpy >= 1.8.0`). Returns ---------- pytest.skipif Decorator describing these requirements if unmet *or* the identity decorator reducing to a noop otherwise. ''' # Defer heavyweight imports. from betse.exceptions import BetseLibException from betse.lib.setuptools import setuptool from betse.util.type.text.string import strjoin # Human-readable message justifying the skipping of this test or fixture. reason = 'Test or fixture currently incompatible with {}.'.format( strjoin.join_as_conjunction_double_quoted(*requirements_str)) # Skip this test if one or more such dependences are satisfiable. return _skip_unless_callable_raises_exception( exception_type=BetseLibException, func=setuptool.die_unless_requirements_str, args=requirements_str, reason=reason, )
def die_unless_maps_keys_equal(*mappings: MappingType) -> None: ''' Raise an exception unless all of the passed dictionaries contain the exact same keys. Equivalently, this function raises an exception if any key of any passed dictionary is *not* a key of any other such dictionary. Parameters ---------- mappings : Tuple[MappingType] Tuple of all dictionaries to be validated. Raises ---------- BetseMappingKeyException If any key of any passed dictionary is *not* a key of any other such dictionary. See Also ---------- :func:`is_keys_equal` Further details. ''' # Avoid circular import dependencies. from betse.util.type.text.string import strjoin # If one or more of these dictionaries contain differing keys... if not is_maps_keys_equal(*mappings): # First passed mapping. Since the is_keys_equal() function necessarily # returns true if either no mappings or only one mapping are passed, # this function returning false implies that two or more mappings are # passed. Ergo, this mapping is guaranteed to exist. mapping_first = mappings[0] # For each mapping excluding the first... for mapping in mappings[1:]: # If the keys of this mapping differ from those of the first... if not is_maps_keys_equal(mapping, mapping_first): # Set of all keys differing between these two mappings. keys_unequal = mapping.keys().symmetric_difference( mapping_first.keys()) # Grammatically proper noun describing the number of such keys. keys_noun = 'key' if len(keys_unequal) == 1 else 'keys' # Raise an exception embedding this set. raise BetseMappingKeyException( 'Dictionary {} {} differ.'.format( keys_noun, strjoin.join_as_conjunction_double_quoted( *keys_unequal)))
def get_first_basename(command_basenames: SequenceTypes, exception_message: str = None) -> str: ''' First pathable string in the passed list (i.e., the first string that is the basename of a command in the current ``${PATH}``) if any *or* raise an exception otherwise. Parameters ---------- command_basenames : SequenceTypes List of the basenames of all commands to be iteratively searched for (in descending order of preference). exception_message : optional[str] Optional exception message to be raised if no such string is pathable. Defaults to ``None``, in which case an exception message synthesized from the passed strings is raised. Returns ---------- str First pathable string in the passed list. Raises ---------- BetseCommandException If no passed strings are pathable. ''' # Avoid circular import dependencies. from betse.util.type.text.string import strjoin # If this list contains the basename of a pathable command, return the # first such basename. for command_basename in command_basenames: if is_pathable(command_basename): return command_basename # Else, this list contains no such basename. Ergo, raise an exception. exception_message_suffix = ( '{} not found in the current ${{PATH}}.'.format( strjoin.join_as_conjunction_double_quoted(*command_basenames))) # If a non-empty exception message is passed, suffix this message with this # detailed explanation. if exception_message: exception_message += ' ' + exception_message_suffix # Else, default this message to this detailed explanation. else: exception_message = exception_message_suffix # Raise this exception. raise BetseCommandException(exception_message)
def get_first_writer_name(writer_names: SequenceTypes) -> str: ''' First name (e.g., ``imagemagick``) of the matplotlib animation writer class (e.g., :class:`ImageMagickWriter`) in the passed list recognized by both this application and :mod:`matplotlib` if any *or* raise an exception otherwise. This function iteratively searches for external commands in the same order as the passed list lists names. Parameters ---------- writer_names : SequenceTypes List of the alphanumeric lowercase names of all writers to search for. Returns ---------- str Alphanumeric lowercase name of the first such writer. Raises ---------- BetseMatplotlibException If either: * Any writer in the passed list is unrecognized by this application. * No such writer is registered with :mod:`matplotlib`. ''' # For the name of each passed writer... for writer_name in writer_names: # If this writer is unrecognized by BETSE, raise an exception. This # prevents BETSE from silently ignoring newly added writers not yet # recognized by BETSE. die_unless_writer_betse(writer_name) # If this writer is recognized by matplotlib, cease searching. if is_writer_mpl(writer_name): return writer_name # Else, no such command is in the ${PATH}. Raise an exception. raise BetseMatplotlibException( 'Matplotlib animation video writers {} not found.'.format( strjoin.join_as_conjunction_double_quoted(*writer_names)))
def die_unless_has_keys(mapping: MappingType, keys: IterableTypes) -> None: ''' Raise an exception unless the passed dictionary contains *all* passed keys. Equivalently, this function raises an exception if this dictionary does *not* contain one or more passed keys. Parameters ---------- mapping : MappingType Dictionary to be validated. keys : IterableTypes Iterable of all keys to be tested for. Raises ---------- BetseMappingKeyException If this dictionary does *not* contain one or more passed keys. See Also ---------- :func:`has_keys` Further details. ''' # Avoid circular import dependencies. from betse.util.type.text.string import strjoin # If this dictionary does *NOT* contain one or more passed keys... if not has_keys(mapping=mapping, keys=keys): # Set of all passed keys *NOT* in this dictionary. keys_missing = set(key for key in keys if key not in mapping) # Grammatically proper noun describing the number of such keys. keys_noun = 'key' if len(keys_missing) == 1 else 'keys' # Raise an exception embedding this set. raise BetseMappingKeyException('Dictionary {} {} not found.'.format( keys_noun, strjoin.join_as_conjunction_double_quoted(*keys_missing)))
def die_unless_values_unique(mapping: MappingType) -> None: ''' Raise an exception unless all values of the passed dictionary are unique. Equivalently, this function raises an exception if any two key-value pairs of this dictionary share the same values. Parameters ---------- mapping : MappingType Dictionary to be validated. Raises ---------- BetseMappingValueException If at least one value of this dictionary is a duplicate. See Also ---------- :func:`is_values_unique` Further details. ''' # Avoid circular import dependencies. from betse.util.type.iterable import iterget from betse.util.type.text.string import strjoin # If one or more values of this dictionary are duplicates... if not is_values_unique(mapping): # Set of all duplicate values in this dictionary. values_duplicate = iterget.get_items_duplicate(mapping.values()) # Grammatically proper noun describing the number of such values. values_noun = 'value' if len(values_duplicate) == 1 else 'values' # Raise an exception embedding this set. raise BetseMappingValueException('Dictionary {} {} duplicate.'.format( values_noun, strjoin.join_as_conjunction_double_quoted(*values_duplicate)))
def get_first_codec_name( writer_name: str, container_filetype: str, codec_names: SequenceTypes, ) -> (str, NoneType): ''' Name of the first video codec (e.g., ``libx264``) in the passed list supported by both the encoder with the passed matplotlib-specific name (e.g., ``ffmpeg``) and the container format with the passed filetype (e.g., ``mkv``, ``mp4``) if that writer supports codecs (as most do), ``None`` if this writer supports no codecs (e.g., ``imagemagick``) and the passed list contains ``None``, *or* raise an exception otherwise (i.e., if no passed codecs are supported by both this writer and container format). Algorithm ---------- This function iteratively searches for video codecs in the same order as listed in the passed list as follows: * If there are no remaining video codecs in this list to be examined, an exception is raised. * If the current video codec has the application-specific name ``auto``, the name of an intelligently selected codec supported by both this encoder and container if any is returned *or* an exception is raised otherwise (i.e., if no codecs are supported by both this encoder and container). Note that this codec's name rather than the application-specific name ``auto`` is returned. See this function's body for further commentary. * Else if the current video codec is supported by both this encoder and container, this codec's name is returned. * Else the next video codec in this list is examined. Parameters ---------- writer_name : str Matplotlib-specific alphanumeric lowercase name of the video encoder to search for the passed codecs. container_filetype: str Filetype of the video container format to constrain this search to. codec_names: SequenceTypes Sequence of the encoder-specific names of all codecs to search for (in descending order of preference). Returns ---------- str Name of the first codec in the passed list supported by both this encoder and container. Raises ---------- BetseMatplotlibException If any of the following errors arise: * This writer is either: * Unrecognized by this application or :mod:`matplotlib`. * Not found as an external command in the current ``${PATH}``. * This container format is unsupported by this writer. * No codec whose name is in the passed list is supported by both this writer and this container format. See Also ---------- :func:`is_writer` Tester validating this writer. ''' # If this writer is unrecognized, raise an exception. die_unless_writer(writer_name) # Basename of this writer's command. writer_basename = WRITER_NAME_TO_COMMAND_BASENAME[writer_name] # Dictionary mapping from the filetype of each video container format to a # list of the names of all video codecs supported by this writer. container_filetype_to_codec_names = ( WRITER_BASENAME_TO_CONTAINER_FILETYPE_TO_CODEC_NAMES[writer_basename]) # If the passed container is unsupported by this writer, raise an exception. if container_filetype not in container_filetype_to_codec_names: raise BetseMatplotlibException( 'Video container format "{}" unsupported by ' 'matplotlib animation video writer "{}".'.format( container_filetype, writer_name)) # List of the names of all candidate codecs supported by both this encoder # and this container. codec_names_supported = container_filetype_to_codec_names[ container_filetype] # List of the names of all candidate codecs to be detected below, with each # instance of "auto" in the original list of these names replaced by the # list of all codecs supported by both this encoder and container. codec_names_candidate = [] # For the name of each candidate codec... for codec_name in codec_names: # If this is the BETSE-specific name "auto", append the names of all # codecs supported by both this encoder and container. if codec_name == 'auto': codec_names_candidate.extend(codec_names_supported) # Else, append only the name of this codec. else: codec_names_candidate.append(codec_name) # Log this detection attempt. logs.log_debug('Detecting encoder "%s" codec from candidates: %r', writer_name, codec_names_candidate) # For the name of each preferred codec... for codec_name in codec_names_candidate: # If this encoder supports this codec *AND*... if is_writer_command_codec(writer_basename, codec_name): # Log this detection result. logs.log_debug('Detected encoder "%s" codec "%s".', writer_name, codec_name) # If this container is not known to support this codec, log a # non-fatal warning. Since what this application thinks it knows # and what reality actually is need not coincide, this container # could actually still support this codec. Hence, this edge case # does *NOT* constitute a hard, fatal error. if codec_name not in codec_names_supported: logs.log_warning( 'Encoder "%s" container "%s" ' 'not known to support codec "%s".', writer_name, container_filetype, codec_name) # Return the name of this codec. return codec_name # Else, no passed codecs are supported by this combination of writer and # container format. Raise an exception. raise BetseMatplotlibException( 'Codec(s) {} unsupported by ' 'encoder "{}" and/or container "{}".'.format( strjoin.join_as_conjunction_double_quoted(*codec_names), writer_name, container_filetype))
def die_if_maps_collide(*mappings: MappingType) -> None: ''' Raise an exception if two or more passed dictionaries **collide** (i.e., contain key-value pairs containing the same keys but differing values). Parameters ---------- mappings : Tuple[MappingType] Tuple of all dictionaries to be validated. Raises ---------- BetseMappingException If two or more passed dictionaries collide. See Also ---------- :func:`is_maps_collide` Further details. ''' # Avoid circular import dependencies. from betse.util.type.iterable import iteriter from betse.util.type.text.string import strjoin # If two or more of these dictionaries collide... if is_maps_collide(*mappings): # Iterable of all possible pairs of these dictionaries. mappings_pairs = iteriter.iter_pairs(mappings) # For each possible pair of these dictionaries... for mappings_pair in mappings_pairs: # If this pair of dictionaries collides... if is_maps_collide(*mappings_pair): # Set of all key-value pairs unique to a single mapping. items_unique = mappings[0].items() ^ mappings[1].items() # Set of all keys visited while iterating this set. keys_visited = set() # Set of all non-unique keys shared by two or more such pairs. keys_collide = set() # For each key of such a pair... for key, _ in items_unique: # If this key has already been visited, this is a # non-unique key shared by two or more such pairs. if key in keys_visited: keys_collide.add(key) # Mark this key as having been visited. keys_visited.add(key) # Grammatically proper noun describing the number of such keys. keys_noun = 'key' if len(keys_collide) == 1 else 'keys' # Raise an exception embedding this set. raise BetseMappingException( 'Dictionary {} {} not unique.'.format( keys_noun, strjoin.join_as_conjunction_double_quoted( *keys_collide)))