Source code for scamp_extensions.utilities.sequences

"""
Subpackage containing utility functions for manipulating lists/sequences (some of which are imported from
:mod:`scamp.utilities`).
"""

#  ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++  #
#  This file is part of SCAMP (Suite for Computer-Assisted Music in Python)                      #
#  Copyright © 2020 Marc Evanstein <marc@marcevanstein.com>.                                     #
#                                                                                                #
#  This program is free software: you can redistribute it and/or modify it under the terms of    #
#  the GNU General Public License as published by the Free Software Foundation, either version   #
#  3 of the License, or (at your option) any later version.                                      #
#                                                                                                #
#  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;     #
#  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.     #
#  See the GNU General Public License for more details.                                          #
#                                                                                                #
#  You should have received a copy of the GNU General Public License along with this program.    #
#  If not, see <http://www.gnu.org/licenses/>.                                                   #
#  ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++  #

from itertools import count, chain
from typing import Sequence
from scamp.utilities import make_flat_list, sum_nested_list
from functools import wraps
import re


[docs]def rotate_sequence(s: Sequence, n: int) -> Sequence: """ Rotates a sequence s such that it begins with element l[n] and wraps back around to l[n-1]. :param s: the list to rotate :param n: number of elements to shift by (can be negative and/or greater than the length of l) :return: a "rotated" version of the input sequence """ return s[n:] + s[:n]
[docs]def cyclic_slice(l: Sequence, start: int, end: int) -> Sequence: """ Takes a slice that loops back to the beginning if end is before start :param l: the list to slice :param start: start index :param end: end index :return: list representing the cyclic slice """ if end >= start: # start by making both indices positive, since that's easier to handle while start < 0 or end < 0: start += len(l) end += len(l) if end >= start + len(l): out = [] while end - start >= len(l): out.extend(l[start:] + l[:start]) end -= len(l) out += cyclic_slice(l, start, end) return out else: end = end % len(l) start = start % len(l) if end >= start: return l[start:end] else: return l[start:] + l[:end] else: # if the end is before the beginning, we do a backwards slice # basically this means we reverse the list, and recalculate the start and end new_start = len(l) - start - 1 new_end = len(l) - end - 1 new_list = list(l) new_list.reverse() return cyclic_slice(new_list, new_start, new_end)
[docs]def sequence_depth(seq: Sequence) -> int: """ Find the maximum _depth of any element in a nested sequence. Slightly adapted from pillmuncher's answer here: https://stackoverflow.com/questions/6039103/counting-depth-or-the-deepest-level-a-nested-list-goes-to :param seq: a nested Sequence (list, tuple, etc) :return: int representing maximum _depth """ if not isinstance(seq, Sequence): return 0 seq = iter(seq) try: for level in count(): seq = chain([next(seq)], seq) seq = chain.from_iterable(s for s in seq if isinstance(s, Sequence)) except StopIteration: return level
# ------------------------------------ Sequence-optional function decorators -----------------------------------------
[docs]def multi_option_function(f): """ Decorator that allows the first argument of the function to be a list, tuple, or iterator, and in that case performs the function one each element individually, returning a list, tuple, or iterator. :param f: the function, taking at least one argument :return: a wrapped version that can take list, tuple, or iterator """ # adds an extra note to the docstring saying it supports lists/etc. insert_index = _get_docstring_insert_position(f.__doc__) f.__doc__ = f.__doc__[:insert_index] + \ "\n The first argument can optionally be a list, tuple, or iterator," \ " in which case this is performed on each element." + \ f.__doc__[insert_index:] @wraps(f) def wrapper(*args, **kwds): if isinstance(args[0], list): return [f(x, *args[1:], **kwds) for x in args[0]] elif isinstance(args[0], tuple): return tuple(f(x, *args[1:], **kwds) for x in args[0]) elif hasattr(args[0], '__next__'): return (f(x, *args[1:], **kwds) for x in args[0]) return f(*args, **kwds) return wrapper
[docs]def multi_option_method(f): """ Same as :func:`multi_option_function`, but used to decorate methods instead of functions. :param f: the method, taking at least one argument :return: a wrapped version that can take list, tuple, or iterator """ # adds an extra note to the docstring saying it supports lists/etc. insert_index = _get_docstring_insert_position(f.__doc__) f.__doc__ = f.__doc__[:insert_index] + \ "\n The first argument can optionally be a list, tuple, or iterator," \ " in which case this is performed on each element." + \ f.__doc__[insert_index:] @wraps(f) def wrapper(*args, **kwds): if isinstance(args[1], list): return [f(args[0], x, *args[2:], **kwds) for x in args[1]] elif isinstance(args[1], tuple): return tuple(f(args[0], x, *args[2:], **kwds) for x in args[1]) elif hasattr(args[1], '__next__'): return (f(args[0], x, *args[2:], **kwds) for x in args[1]) return f(*args, **kwds) return wrapper
def _get_docstring_insert_position(docstring): # first insert before ":param" and any spaces before that end_of_description = re.search(r'[\s\n]*:param', docstring) # if ":param" doesn't show up, insert before any spaces or returns at the end of the docstring if end_of_description is None: end_of_description = re.search(r'[\s\n]*$', docstring) # as a last resort, just stick it at the end insert_index = end_of_description.start() if end_of_description is not None else -1 return insert_index