Exemplo n.º 1
0
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,
    )
Exemplo n.º 2
0
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)))
Exemplo n.º 3
0
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)
Exemplo n.º 4
0
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)))
Exemplo n.º 5
0
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)))
Exemplo n.º 6
0
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)))
Exemplo n.º 7
0
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))
Exemplo n.º 8
0
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)))