Source code for iterchain.core

# import types
import builtins
import functools
import itertools

# from typing import Any, Callable, Generator, Iterable, TypeVar, Union
# T = TypeVar("T"); U = TypeVar("U")


[docs]def chainable(func=None, *, returns_iterable=True): """ Decorator that allows you to add your own custom chainable methods. The wrapped function should take the an :class:`Iterator` instance as the first argument, and should return an iterable object (does not have to be an :class:`Iterator` instance). The original function is not modified and can still be used as normal. Args: returns_iterable (bool): whether or not the wrapped function returns an iterable Example: :: >>> @iterchain.chainable >>> def plus(iterable, amount): ... return iterable.map(lambda x: x + amount) ... >>> iterchain([1, 2, 3]).plus(1).to_list() [2, 3, 4] """ def _chainable(func): @functools.wraps(func) def wrapper(self, *args, **kwargs): result = func(self, *args, **kwargs) if returns_iterable and not isinstance(result, Iterator): return Iterator(result) return result if hasattr(Iterator, func.__name__): raise AttributeError("Chainable method {} already exists.".format(func.__name__)) setattr(Iterator, func.__name__, wrapper) return func # to allow the decorator to be used with and without arguments if func is None: return _chainable return _chainable(func)
# simple QoL decorator to avoid repeated code. # all it does is wrap the return value into an Iterator instance if it isn't one already # def returns_iterator(func): # @functools.wraps(func) # def wrapper(*args, **kwargs): # result = func(*args, **kwargs) # if not isinstance(result, Iterator): # return Iterator(result) # return result # return wrapper def magicify(func): @functools.wraps(func) def wrapper(self, *args, **kwargs): # make sure self is an `Iterator` in case it's called as a "static" function if not isinstance(self, Iterator): self = Iterator(self) # make sure the output gets wrapped up as `Iterator` as well result = func(self, *args, **kwargs) if not isinstance(result, Iterator): result = Iterator(result) return result return wrapper
[docs]class Iterator(): """ Wrapper class around python iterators After wrapping any iterable in this class it will have access to all the methods listed below. These methods also return an `Iterator` instance to make them chainable. ``<3`` """ # ==== BASICS ==== # TODO: duplicate the `sentinel` behaviour of the builtin # (don't actually have to program it, just copy the args to builtins.iter(...)) def __init__(self, iterable): try: self._iterator = builtins.iter(iterable) except TypeError: raise ValueError("You must pass a valid iterable object") def __iter__(self): return self def __next__(self): return next(self._iterator) # ==== TRANSFORMATIONS (Iterator -> Iterator) ==== # TODO: # reversed # sorted # step_by # group_by (itertools)
[docs] def map(self, function) -> 'Iterator': """ Applies a given function to all elements. Args: function: the function to be called on each element """ return Iterator(builtins.map(function, self))
[docs] def flatten(self) -> 'Iterator': return Iterator(itertools.chain.from_iterable(self))
[docs] def flat_map(self, function) -> 'Iterator': return self.map(function).flatten()
[docs] def star_map(self, function) -> 'Iterator': return Iterator(itertools.starmap(function, self))
[docs] def filter(self, function) -> 'Iterator': return Iterator(builtins.filter(function, self))
[docs] def filter_false(self, predicate=None) -> 'Iterator': return Iterator(itertools.filterfalse(predicate, self))
[docs] def enumerate(self, start=0) -> 'Iterator': return Iterator(builtins.enumerate(self, start))
[docs] def slice(self, *args) -> 'Iterator': return Iterator(itertools.islice(self, *args))
[docs] def take(self, n) -> 'Iterator': return self.slice(0, n)
[docs] def take_while(self, predicate) -> 'Iterator': return Iterator(itertools.takewhile(predicate, self))
[docs] def skip(self, n) -> 'Iterator': # FIXME: kind of a sloppy implementation def _skip(iterable, n): for _ in range(n): try: next(self) except StopIteration: return for i in iterable: yield i return Iterator(_skip(self, n))
[docs] def drop(self, n) -> 'Iterator': return self.skip(n)
[docs] def skip_while(self, predicate) -> 'Iterator': return Iterator(itertools.dropwhile(predicate, self))
[docs] def drop_while(self, predicate) -> 'Iterator': return self.skip_while(predicate)
[docs] def inspect(self, function) -> 'Iterator': # FIXME: kind of a sloppy implementation def _inspect(iterator, function): for i in iterator: function(i) yield i return Iterator(_inspect(self, function))
[docs] def chain(self, *iterables) -> 'Iterator': return Iterator(itertools.chain(self, *iterables))
[docs] def compress(self, selectors) -> 'Iterator': return Iterator(itertools.compress(self, selectors))
[docs] def product(self, *iterables, repeat=1) -> 'Iterator': return Iterator(itertools.product(self, *iterables, repeat=repeat))
[docs] def permutations(self, r=None) -> 'Iterator': return Iterator(itertools.permutations(self, r=r))
[docs] def combinations(self, r) -> 'Iterator': return Iterator(itertools.combinations(self, r))
[docs] def combinations_with_replacement(self, r) -> 'Iterator': return Iterator(itertools.combinations_with_replacement(self, r))
[docs] def cycle(self) -> 'Iterator': return Iterator(itertools.cycle(self))
# ==== TERMINATORS (Iterator -> object) ==== # TODO: # all # any # min, max # sum # product # conflicts with the cartesian/set product # length # first # last # nth # for_each # partition # find / position'
[docs] def reduce(self, initial, function): return functools.reduce(function, self, initial)
[docs] def next(self): return next(self)
[docs] def collect(self, constructor=None): if constructor: return constructor(self) else: # just use up the iterator but don't do anything with it # could be useful if you somehow have side effects # you wanna execute but don't need the results for _ in self: pass return None
[docs] def to_list(self) -> list: """ Converts the Iterchain to a list Returns: new list containing all the elements in this Iterchain """ return self.collect(list)
# ==== Generators (new Iterator) ==== # TODO: # successors (rust) # zip (normal, strict, longest) / unzip
[docs] @classmethod def range(cls, *args) -> 'Iterator': """ Makes a new iterator that returns evenly spaced values. (similar to the ``range`` builtin) """ return Iterator(range(*args))
[docs] @classmethod def count(cls, start=0, step=1) -> 'Iterator': return Iterator(itertools.count(start, step))
[docs] @classmethod def repeat(cls, item, times=None) -> "Iterator": if times: return Iterator(itertools.repeat(item, times)) else: return Iterator(itertools.repeat(item))