Replace occurrences of string(s) with other string(s) in (a) string(s).

    Unlike the built in :meth:`str.replace` method, this function provides
    clean API that clearly distinguishes the "needle" (string to replace),
    the replacement string, and the target string to perform replacement in
    (the "haystack").

    Additionally, a simultaneous replacement of several needles is possible.
    Note that this is different from performing multiple separate replacements
    one after another.

    Examples::

        replace('foo', with_='bar', in_=some_text)
        replace('foo', with_='bar').in_(other_text)
        replace('foo').with_('bar').in_(another_text)
        replace(['foo', 'bar']).with_('baz').in_(perhaps_a_long_text)
        replace({'foo': 'bar', 'baz': 'qud'}).in_(even_longer_text)

    :param needle: String to replace, iterable thereof,
                   or a mapping from needles to corresponding replacements
    :param with_: Replacement string, if ``needle`` was not a mapping
    :param in_: Optional string to perform replacement in

    :return: If all parameters were provided, result is the final string
             after performing a specified replacement.
             Otherwise, a :class:`Replacer` object is returned, allowing
             e.g. to perform the same replacements in many haystacks.

def replace(needle, with_=None, in_=None):
    """Replace occurrences of string(s) with other string(s) in (a) string(s).

    Unlike the built in :meth:`str.replace` method, this function provides
    clean API that clearly distinguishes the "needle" (string to replace),
    the replacement string, and the target string to perform replacement in
    (the "haystack").

    Additionally, a simultaneous replacement of several needles is possible.
    Note that this is different from performing multiple separate replacements
    one after another.

    Examples::

        replace('foo', with_='bar', in_=some_text)
        replace('foo', with_='bar').in_(other_text)
        replace('foo').with_('bar').in_(another_text)
        replace(['foo', 'bar']).with_('baz').in_(perhaps_a_long_text)
        replace({'foo': 'bar', 'baz': 'qud'}).in_(even_longer_text)

    :param needle: String to replace, iterable thereof,
                   or a mapping from needles to corresponding replacements
    :param with_: Replacement string, if ``needle`` was not a mapping
    :param in_: Optional string to perform replacement in

    :return: If all parameters were provided, result is the final string
             after performing a specified replacement.
             Otherwise, a :class:`Replacer` object is returned, allowing
             e.g. to perform the same replacements in many haystacks.
    """
    if needle is None:
        raise TypeError("replacement needle cannot be None")
    if not needle:
        raise ValueError("replacement needle cannot be empty")

    if is_string(needle):
        replacer = Replacer((needle,))
    else:
        ensure_iterable(needle)
        if not is_mapping(needle):
            if all(imap(is_pair, needle)):
                needle = dict(needle)
            elif not all(imap(is_string, needle)):
                raise TypeError("invalid replacement needle")
        replacer = Replacer(needle)

    if with_ is not None:
        ensure_string(with_)
        replacer = replacer.with_(with_)

    if in_ is not None:
        ensure_string(in_)
        return replacer.in_(in_)

    return replacer