From db19613d88139a44316546918291305235fe0dad Mon Sep 17 00:00:00 2001 From: Martin Matyasek Date: Wed, 10 Apr 2019 21:13:00 +0200 Subject: [PATCH 1/7] Adds functoolz and dicttoolz --- .pylintrc | 5 +- README.md | 61 ++++++ pytoolz/dicttoolz.py | 42 ++++ pytoolz/functoolz/__init__.py | 62 ++++++ pytoolz/functoolz/iter.py | 260 ++++++++++++++++++++++++ pytoolz/functoolz/opt.py | 270 +++++++++++++++++++++++++ pytoolz/functoolz/seq.py | 204 +++++++++++++++++++ pytoolz/functoolz/traverse/__init__.py | 0 pytoolz/functoolz/traverse/opt.py | 102 ++++++++++ pytoolz/itertoolz.py | 32 ++- setup.py | 5 +- 11 files changed, 1039 insertions(+), 4 deletions(-) create mode 100644 pytoolz/dicttoolz.py create mode 100644 pytoolz/functoolz/__init__.py create mode 100644 pytoolz/functoolz/iter.py create mode 100644 pytoolz/functoolz/opt.py create mode 100644 pytoolz/functoolz/seq.py create mode 100644 pytoolz/functoolz/traverse/__init__.py create mode 100644 pytoolz/functoolz/traverse/opt.py diff --git a/.pylintrc b/.pylintrc index 9ac93af..7d6bf0c 100644 --- a/.pylintrc +++ b/.pylintrc @@ -29,8 +29,9 @@ method-rgx=[a-z_][a-z0-9_]{2,60}$ # Good variable names which should always be accepted, separated by a comma # defaults were i,j,k,ex,Run,_ good-names= - i,j,k,ex,Run,_,e,op,m,t,it,id,fn,f,g,h,n,v,ds,xs,ys,zs,x,y,z, - ff,fa,fb,fc,a,b,c,A,B,C,E,F,G,K,R,T,R,V,Z + a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,K,R,T,R,V,Z, + ex,fa,fb,fc,ff,fn,id,it,op,xs,ys,zs,_,,applyN,fmap,fmapN, + A_in,B_in,C_in,D_in,A_out,A_out,B_out,C_out,D_out const-naming-style=any diff --git a/README.md b/README.md index 0f9f8b0..ced181d 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,66 @@ Collection of higher-order and utility functions built on top of `cytoolz`. ## Module overview Pytoolz are split into few generic modules. +### functoolz package +Package that provides higher-order functions commonly associated with Functor, Applicative and Monad. + +Moreover, there are general package-level functions implemented in `__init__.py`. + +| Function | Description | +|----------|-------------| +| `try_except(ex, f, g, args, kwargs)` | `f(args, kwargs)` and on exception(s) `ex` fallback to `g(args, kwargs)` | + +The package content is organized into modules by individual type class: +1. `iter.py` for class `Iterable`. **Warn** some functions might not be pure because input iterable is consumed. +1. `opt.py` for class `Optional` +1. `seq.py` for class `Seq` (`Sequece`). Methods typically return `tuple` instances to preserve immutability. + +#### Module function overview +| def / .py | iter | opt | seq | +|-----------|------|-----|-----| +|`apply`| x | x | x | +|`apply2`| - | x | - | +|`applyN`| - | + | - | +|`flatmap`| x | x | x | +|`flatten`| x | x | x | +|`fmap`| x | x | x | +|`fmap2`| x | x | x | +|`fmap3`| - | x | - | +|`fmapN`| - | + | - | +|`fproduct`| x | x | x | +|`lift`| x | x | x | +|`product`| x | x | x | +|`unit`| x | * | x | +|`zip_map`| + | - | + | + +* `x` - implemented, statically type checked +* `+` - implemented, possible runtime type errors +* `*` - not implemented, supported natively +* `-` - not implemented + +#### traverse package +Each module contains traversable-related functions for traversables `Iterable` and `Seq`. +Individual modules are named and reserved for single functor that wraps elements of the traversable sequence. + +List of currently implemented functions in modules (functors): + +| def / .py | opt | +|-----------|-----| +|`sequence_iter`| x | +|`sequence_seq`| x | +|`traverse_iter`| x | +|`traverse_iter`| x | + +### dicttoolz +This module contains functions that work with `Map` (`Mapping`) instances. + +Table of contents + +| Function | Description | +|----------|-------------| +| `swap(dict, key1, key2)` | swap arbitrary values for `key1` and `key2` in given mapping | +| `swap_values(dict, key1, key2)` | same as `swap` but preserving concrete value type `V` | + ### itertoolz This module contains functions that work with `Iterable` instances. @@ -21,6 +81,7 @@ selected by `key_fn` | | `filter_not_none(iterable)` | filter out `None` elements from iterable | | `find(predicate, iterable)` | find first element of iterable satisfying predicate | | `first(sequence)` | return first element of a sequence or `None` | +| `fold_right(op, iterable, z)` | fold iterable by applying binary operator `op` from the *right* | | `head_tail(iterable)` | split iterable into head element and tail iterable | | `head_tail_list(iterable)` | same as `head_tail` but materialized tail into list | | `iter_with_final(iterable)` | creates iterable of tuples of original element and final flag | diff --git a/pytoolz/dicttoolz.py b/pytoolz/dicttoolz.py new file mode 100644 index 0000000..a0b008e --- /dev/null +++ b/pytoolz/dicttoolz.py @@ -0,0 +1,42 @@ +from typing import Any, TypeVar + +from pytoolz.typing import Map + +K = TypeVar('K') +V = TypeVar('V') + + +def swap(d: Map[K, Any], key1: K, key2: K) -> Map[K, Any]: + """ + Swap arbitrary values for given keys creating new mapping. + + >>> swap({'k1': [1, 2, 3], 'k2': {1, 2, 3}}, 'k1', 'k2') + {'k1': {1, 2, 3}, 'k2': [1, 2, 3]} + + Original mapping is returned if at least one key does not exist in `d`. + + >>> swap({'k1': 1}, 'k1', 'k2') + {'k1': 1} + >>> swap({'k2': 2}, 'k1', 'k2') + {'k2': 2} + """ + return swap_values(d, key1, key2) + + +def swap_values(d: Map[K, V], key1: K, key2: K) -> Map[K, V]: + """ + Swap values for given keys creating new mapping. + + >>> swap_values({'k1': 1, 'k2': 2}, 'k1', 'k2') + {'k1': 2, 'k2': 1} + + Original mapping is returned if at least one key does not exist in `d`. + + >>> swap_values({'k1': 1}, 'k1', 'k2') + {'k1': 1} + >>> swap_values({'k2': 2}, 'k1', 'k2') + {'k2': 2} + """ + return {**d, key1: d[key2], key2: d[key1]} \ + if key1 in d and key2 in d \ + else d diff --git a/pytoolz/functoolz/__init__.py b/pytoolz/functoolz/__init__.py new file mode 100644 index 0000000..9452b4e --- /dev/null +++ b/pytoolz/functoolz/__init__.py @@ -0,0 +1,62 @@ +from typing import Any, Callable, Tuple, Type, TypeVar, Union + +# Invariant +A = TypeVar('A') +B = TypeVar('B') +C = TypeVar('C') +D = TypeVar('D') + +# Contravariant +A_in = TypeVar('A_in', contravariant=True) +B_in = TypeVar('B_in', contravariant=True) +C_in = TypeVar('C_in', contravariant=True) +D_in = TypeVar('D_in', contravariant=True) + +# Covariant +A_out = TypeVar('A_out', covariant=True) +B_out = TypeVar('B_out', covariant=True) +C_out = TypeVar('C_out', covariant=True) +D_out = TypeVar('D_out', covariant=True) + + +def try_except( + e: Union[Type[Exception], Tuple[Type[Exception], ...]], + f: Callable[..., A], + g: Callable[..., A], + *args: Any, + **kwargs: Any +) -> A: + """ + Call `f(args, kwargs)` and on exception `e` fallback to `g(args, kwargs)`. + + >>> try_except(ValueError, int, lambda s: s.upper(), '1') + 1 + >>> try_except(ValueError, int, lambda s: s.upper(), 'a') + 'A' + + One can pass both args and kwargs to `try_except`. + + >>> def f(x: int, y: str): + ... return x + int(y) + + >>> def g(x: int, y: str): + ... return f'error: {x} + {y}' + + >>> try_except(ValueError, f, g, 1, y='2') + 3 + + >>> try_except(ValueError, f, g, 1, y='x') + 'error: 1 + x' + + Errors that are not mentioned in `e` are propagated to outer scope. + + >>> try_except(ValueError, lambda d: d['k'], str, {'a': 1}) + Traceback (most recent call last): + ... + KeyError: 'k' + """ + # noinspection PyBroadException + try: + return f(*args, **kwargs) + except e: + return g(*args, **kwargs) diff --git a/pytoolz/functoolz/iter.py b/pytoolz/functoolz/iter.py new file mode 100644 index 0000000..0fa169e --- /dev/null +++ b/pytoolz/functoolz/iter.py @@ -0,0 +1,260 @@ +from itertools import starmap +from typing import Any, Callable, Iterable, Tuple + +from cytoolz.itertoolz import identity, mapcat + +from pytoolz.functoolz import A, A_in, A_out, B, B_in, B_out, C_out + + +def apply( + ff: Iterable[Callable[[A_in], B_out]], + fa: Iterable[A_in] +) -> Iterable[B_out]: + """ + Given a value and a function in the :class:`Iterable` context, + applies the function to the value. + + >>> f1 = lambda x: str(x + 1) + >>> f2 = lambda x: str(x + 3) + + Result is empty if at least one of given iterables is empty. + + >>> list(apply(iter([]), iter([]))) + [] + >>> list(apply(iter([f1, f2]), iter([]))) + [] + >>> list(apply(iter([]), iter([1, 2]))) + [] + + >>> list(apply(iter([f1]), iter([1, 2]))) + ['2', '3'] + + >>> list(apply(iter([f1, f2]), iter([1]))) + ['2', '4'] + + >>> ff = iter([f1, f2]) + >>> fa = iter([1, 2]) + >>> list(apply(ff, fa)) + ['2', '3', '4', '5'] + + **Warn**: This operation is terminal in both given iterables and thus + not a pure function. + + >>> next(ff) + Traceback (most recent call last): + ... + StopIteration + >>> next(fa) + Traceback (most recent call last): + ... + StopIteration + """ + fas = list(fa) + return flatmap(lambda f: fmap(f, fas), ff) + + +flatmap = mapcat + + +def flatten(ffa: Iterable[Iterable[A]]) -> Iterable[A]: + """ + Flatten a nested `Iterable` of `Iterable` structure into a single-layer + `Iterable` structure. + + >>> list(flatten(iter([]))) + [] + + >>> ffa = iter([[1], [2], [], [3, 4], []]) + >>> list(flatten(ffa)) + [1, 2, 3, 4] + + **Warn**: This operation is terminal in `ffa` and thus not a pure function. + + >>> next(ffa) + Traceback (most recent call last): + ... + StopIteration + """ + return flatmap(identity, ffa) + + +fmap = map + + +def fmap2( + f: Callable[[A_in, B_in], C_out], + fa: Iterable[A_in], + fb: Iterable[B_in] +) -> Iterable[C_out]: + """ + Bi-functor map for :class:`Iterable`. + + >>> def f(a: int, b: str) -> str: + ... return b * a + + Result is empty if at least one of given iterables is empty. + + >>> list(fmap2(f, iter([]), iter([]))) + [] + >>> list(fmap2(f, iter([1, 2, 3]), iter([]))) + [] + >>> list(fmap2(f, iter([]), iter(['a', 'b']))) + [] + + Input iterables do not have to be of equal size. + + >>> fa = iter([1, 2, 3]) + >>> fb = iter(['a', 'b']) + >>> list(fmap2(f, fa, fb)) + ['a', 'b', 'aa', 'bb', 'aaa', 'bbb'] + + **Warn**: This operation is terminal in both arguments and thus not a pure + function. + + >>> next(fa) + Traceback (most recent call last): + ... + StopIteration + >>> next(fb) + Traceback (most recent call last): + ... + StopIteration + """ + fbs = list(fb) + + def ff(a: A_in) -> Iterable[C_out]: + return fmap(lambda b: f(a, b), fbs) + + return flatmap(ff, fa) + + +def fproduct( + f: Callable[[A], B_out], + fa: Iterable[A] +) -> Iterable[Tuple[A, B_out]]: + """ + Tuple the values in fa with the result of applying a function with + the value. + + >>> def f(x: int) -> str: + ... return str(x) + + >>> list(fproduct(f, iter([]))) + [] + + >>> fa = iter([1, 2, 3]) + >>> list(fproduct(f, fa)) + [(1, '1'), (2, '2'), (3, '3')] + + **Warn**: This operation is terminal in `fa` and thus not a pure function. + + >>> next(fa) + Traceback (most recent call last): + ... + StopIteration + """ + return fmap(lambda a: (a, f(a)), fa) + + +def lift( + f: Callable[[A_in], B_out] +) -> Callable[[Iterable[A_in]], Iterable[B_out]]: + """ + Lift a function f to operate on :class:`Iterable`. + + >>> def f(x: int) -> str: + ... return str(x) + + >>> lifted = lift(f) + + >>> list(lifted(iter([]))) + [] + + >>> list(lifted(iter([1, 2, 3]))) + ['1', '2', '3'] + """ + return lambda fa: fmap(f, fa) + + +def product(fa: Iterable[A], fb: Iterable[B]) -> Iterable[Tuple[A, B]]: + """ + Combine an `Iterable[A]` and an `Iterable[B]` into an `Iterable[(A, B)]` + that maintains the effects of both `fa` and `fb`. + + Result is empty if at least one of given iterables is empty. + + >>> list(product(iter([]), iter([]))) + [] + >>> list(product(iter([1, 2, 3]), iter([]))) + [] + >>> list(product(iter([]), iter(['a', 'b']))) + [] + + Input iterables do not have to be of equal size. + + >>> fa = iter([1, 2, 3]) + >>> fb = iter(['a', 'b']) + >>> list(product(fa, fb)) + [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b'), (3, 'a'), (3, 'b')] + + **Warn**: This operation is terminal in both arguments and thus not a pure + function. + + >>> next(fa) + Traceback (most recent call last): + ... + StopIteration + >>> next(fb) + Traceback (most recent call last): + ... + StopIteration + """ + fbs = list(fb) + + # Because pylint does not allow `lambda a: fmap(lambda b: (a, b), fbs)`. + def ff(a: A) -> Iterable[Tuple[A, B]]: + return fmap(lambda b: (a, b), fbs) + + return flatmap(ff, fa) + + +def unit(a: A) -> Iterable[A]: + """ + Unit value for :class:`Iterable`. Returns single item generator. + + >>> list(unit(42)) + [42] + """ + yield a + + +def zip_map(f: Callable[..., A_out], *fx: Iterable[Any]) -> Iterable[A_out]: + """ + >>> def f(a: int, b: str) -> str: + ... return b * a + + >>> list(zip_map(f, iter([]), iter([]))) + [] + >>> list(zip_map(f, iter([1, 2, 3]), iter([]))) + [] + >>> list(zip_map(f, iter([]), iter(['a', 'b']))) + [] + + >>> fa = iter([1, 2, 3, 4]) + >>> fb = iter(['a', 'b', 'c']) + >>> list(zip_map(f, fa, fb)) + ['a', 'bb', 'ccc'] + + **Warn**: This operation is terminal in all given iterables and thus + not a pure function. + + >>> next(fa) + Traceback (most recent call last): + ... + StopIteration + >>> next(fb) + Traceback (most recent call last): + ... + StopIteration + """ + return starmap(f, zip(*fx)) diff --git a/pytoolz/functoolz/opt.py b/pytoolz/functoolz/opt.py new file mode 100644 index 0000000..776d4d1 --- /dev/null +++ b/pytoolz/functoolz/opt.py @@ -0,0 +1,270 @@ +from typing import Any, Callable, Optional, Tuple + +from pytoolz.functoolz import A, A_in, A_out, B, B_in, B_out, C_in, C_out, \ + D_out + + +def apply( + ff: Optional[Callable[[A_in], B_out]], + fa: Optional[A_in] +) -> Optional[B_out]: + """ + Given a value and a function in the :class:`Optional` context, + applies the function to the value. + + >>> ff = lambda x: str(x) + >>> apply(ff, None) + >>> apply(None, 42) + >>> apply(None, None) + >>> apply(ff, 42) + '42' + """ + if ff is not None and fa is not None: + return ff(fa) + return None + + +def apply2( + ff: Optional[Callable[[A_in, B_in], C_out]], + fa: Optional[A_in], + fb: Optional[B_in] +) -> Optional[C_out]: + """ + Given two values and a function in the :class:`Optional` context, + applies the function to the values. + + >>> ff = lambda x, y: f'{x}{y}' + >>> apply2(ff, None, None) + >>> apply2(None, 4, None) + >>> apply2(None, None, '2') + >>> apply2(None, None, None) + >>> apply2(None, 4, '2') + >>> apply2(ff, 4, '2') + '42' + """ + if ff is not None and fa is not None and fb is not None: + return ff(fa, fb) + return None + + +def applyN( + ff: Optional[Callable[..., A_out]], + *fx: Optional[Any] +) -> Optional[A_out]: + """ + Given N values and a function in the :class:`Optional` context, + applies the function to the values. + + >>> ff = lambda x, y: f'{x}{y}' + >>> applyN(ff, None, None) + >>> applyN(None, 4, None) + >>> applyN(None, None, '2') + >>> applyN(None, None, None) + >>> applyN(None, 4, '2') + >>> applyN(ff, 4, '2') + '42' + + **Warn**: For static type-safety, prefer :ref:`apply`, :ref:`apply2`, + this function may raise runtime errors. + + >>> applyN(lambda x, y: x + y, 1, '2') + Traceback (most recent call last): + ... + TypeError: unsupported operand type(s) for +: 'int' and 'str' + + >>> applyN(ff, 4, '2', 'fail') + Traceback (most recent call last): + ... + TypeError: () takes 2 positional arguments but 3 were given + """ + if ff is not None and all(x is not None for x in fx): + return ff(*fx) + return None + + +def flatmap( + f: Callable[[A_in], Optional[B_out]], + fa: Optional[A_in] +) -> Optional[B_out]: + """ + Feed value in context `(Optional[A_contra])` into a function that takes a + normal value and returns a value in a context + `(A_contra -> Optional[B_co])`. + + >>> def test_func(x: int) -> Optional[str]: + ... return str(100 // x) if x != 0 else None + + >>> flatmap(test_func, None) + + >>> flatmap(test_func, 42) + '2' + + >>> flatmap(test_func, 0) + """ + return None if fa is None else f(fa) + + +def flatten(ffa: Optional[Optional[A]]) -> Optional[A]: + """ + Flatten double :class:`Optional` context into single one. + + >>> flatten(None) + >>> flatten(42) + 42 + """ + return None if ffa is None else ffa + + +def fmap( + f: Callable[[A_in], B_out], + fa: Optional[A_in] +) -> Optional[B_out]: + """ + Functor map for :class:`Optional`. + + >>> add_one = lambda x: str(x + 1) + >>> one: Optional[int] = 1 + + >>> fmap(add_one, one) + '2' + >>> fmap(add_one, None) + """ + return f(fa) if fa is not None else None + + +def fmap2( + f: Callable[[A_in, B_in], C_out], + fa: Optional[A_in], + fb: Optional[B_in] +) -> Optional[C_out]: + """ + Bi-functor map for :class:`Optional`. + + >>> def f(a: int, b: str) -> Optional[str]: + ... return str(a) if b == 'x' else None + + >>> fmap2(f, None, None) + >>> fmap2(f, None, '2') + >>> fmap2(f, 1, None) + >>> fmap2(f, 1, '2') + >>> fmap2(f, 42, 'x') + '42' + """ + + def ff(a: A_in) -> Optional[C_out]: + return fmap(lambda b: f(a, b), fb) + + return flatmap(ff, fa) + + +def fmap3( + f: Callable[[A_in, B_in, C_in], D_out], + fa: Optional[A_in], + fb: Optional[B_in], + fc: Optional[C_in], +) -> Optional[D_out]: + """ + 3-functor map for :class:`Optional`. + + >>> def f(a: int, b: str, c: str) -> Optional[str]: + ... return str(a) if b == 'x' and c == 'y' else None + + >>> fmap3(f, None, None, None) + >>> fmap3(f, 1, None, None) + >>> fmap3(f, None, '2', None) + >>> fmap3(f, None, None, '3') + >>> fmap3(f, 1, '2', None) + >>> fmap3(f, 1, None, '3') + >>> fmap3(f, 1, 'x', 'z') + >>> fmap3(f, 42, 'x', 'y') + '42' + """ + + def ff(a: A_in) -> Optional[D_out]: + return fmap2(lambda b, c: f(a, b, c), fb, fc) + + return flatmap(ff, fa) + + +def fmapN(f: Callable[..., A_out], *fx: Optional[Any]) -> Optional[A_out]: + """ + Generic N-functor map for :class:`Optional`. + + >>> def f(x: int, y: str) -> str: + ... return f'{x}{y}' + + >>> fmapN(f, None, None) + >>> fmapN(f, 1, None) + >>> fmapN(f, None, '2') + >>> fmapN(f, 4, '2') + '42' + + **Warn**: For static type-safety, prefer :ref:`fmap`, :ref:`fmap2` and + :ref:`fmap3`, this function may raise runtime errors. + + >>> fmapN(lambda x, y: x + y, 1, '2') + Traceback (most recent call last): + ... + TypeError: unsupported operand type(s) for +: 'int' and 'str' + + >>> fmapN(f, 4, '2', 'fail') + Traceback (most recent call last): + ... + TypeError: f() takes 2 positional arguments but 3 were given + """ + return None if any(x is None for x in fx) else f(*fx) + + +def fproduct( + f: Callable[[A], B_out], + fa: Optional[A] +) -> Optional[Tuple[A, B_out]]: + """ + Tuple the values in fa with the result of applying a function with + the value. + + >>> def f(x: int) -> str: + ... return str(x) + + >>> fproduct(f, 42) + (42, '42') + >>> fproduct(f, None) + """ + return fmap(lambda a: (a, f(a)), fa) + + +def lift( + f: Callable[[A_in], B_out] +) -> Callable[[Optional[A_in]], Optional[B_out]]: + """ + Lift a function f to operate on :class:`Optional`. + + >>> def f(x: int) -> str: + ... return str(x) + + >>> lifted = lift(f) + + >>> lifted(None) + >>> lifted(42) + '42' + """ + return lambda fa: fmap(f, fa) + + +def product(fa: Optional[A], fb: Optional[B]) -> Optional[Tuple[A, B]]: + """ + Combine an `Optional[A]` and an `Optional[B]` into an `Optional[(A, B)]` + that maintains the effects of both `fa` and `fb`. + + >>> product(None, None) + >>> product(1, None) + >>> product(None, '2') + >>> product(4, '2') + (4, '2') + """ + + # Because pylint does not allow `lambda a: map_opt(lambda b: (a, b), fb)`. + def ff(a: A) -> Optional[Tuple[A, B]]: + return fmap(lambda b: (a, b), fb) + + return flatmap(ff, fa) diff --git a/pytoolz/functoolz/seq.py b/pytoolz/functoolz/seq.py new file mode 100644 index 0000000..41c4d0f --- /dev/null +++ b/pytoolz/functoolz/seq.py @@ -0,0 +1,204 @@ +from itertools import starmap +from typing import Any, Callable, Tuple + +from cytoolz.itertoolz import identity, mapcat + +from pytoolz.functoolz import A, A_in, A_out, B, B_in, B_out, C_out +from pytoolz.typing import Seq + + +def apply(ff: Seq[Callable[[A_in], B_out]], fa: Seq[A_in]) -> Seq[B_out]: + """ + Given a value and a function in the :class:`Seq` context, + applies the function to the value. + + >>> f1 = lambda x: str(x + 1) + >>> f2 = lambda x: str(x + 3) + + Result is empty if at least one of given iterables is empty. + + >>> apply(tuple(), tuple()) + () + >>> apply((f1, f2), tuple()) + () + >>> apply(tuple(), (1, 2)) + () + + >>> apply((f1,), (1, 2)) + ('2', '3') + + >>> apply((f1, f2), (1,)) + ('2', '4') + + >>> apply((f1, f2), (1, 2)) + ('2', '3', '4', '5') + """ + return flatmap(lambda f: fmap(f, fa), ff) + + +def flatmap(f: Callable[[A_in], Seq[B_out]], fa: Seq[A_in]) -> Seq[B_out]: + """ + Feed value in context `(Seq[A])` into a function that takes a normal + value and returns a value in a context `(A -> Seq[B])`. + + >>> def f(x: int) -> Seq[str]: + ... return str(x), str(x) + + >>> flatmap(f, tuple()) + () + >>> flatmap(f, (1, 2, 3)) + ('1', '1', '2', '2', '3', '3') + """ + return tuple(mapcat(f, fa)) + + +def flatten(ffa: Seq[Seq[A]]) -> Seq[A]: + """ + Flatten a nested `Seq` of `Seq` structure into a single-layer + `Seq` structure. + + >>> flatten((tuple(), tuple())) + () + >>> flatten(((1,), (2,), tuple(), (3, 4), tuple())) + (1, 2, 3, 4) + """ + return flatmap(identity, ffa) + + +def fmap(f: Callable[[A_in], B_out], fa: Seq[A_in]) -> Seq[B_out]: + """ + Functor map for :class:`Seq`. + + >>> def f(x: int) -> str: + ... return str(x) + + >>> fmap(f, tuple()) + () + >>> fmap(f, (1, 2, 3)) + ('1', '2', '3') + """ + return tuple(f(a) for a in fa) + + +def fmap2( + f: Callable[[A_in, B_in], C_out], + fa: Seq[A_in], + fb: Seq[B_in] +) -> Seq[C_out]: + """ + Bi-functor map for :class:`Seq`. + + >>> def f(a: int, b: str) -> str: + ... return b * a + + Result is empty if at least one of given sequences is empty. + + >>> fmap2(f, tuple(), tuple()) + () + >>> fmap2(f, (1, 2), tuple()) + () + >>> fmap2(f, tuple(), ('a', 'b')) + () + + Input sequences do not have to be of equal size. + + >>> fmap2(f, (1, 2, 3), ('a', 'b')) + ('a', 'b', 'aa', 'bb', 'aaa', 'bbb') + """ + + def ff(a: A_in) -> Seq[C_out]: + return fmap(lambda b: f(a, b), fb) + + return tuple() if not fb else flatmap(ff, fa) + + +def fproduct(f: Callable[[A], B_out], fa: Seq[A]) -> Seq[Tuple[A, B_out]]: + """ + Tuple the values in fa with the result of applying a function with + the value. + + >>> def f(x: int) -> str: + ... return str(x) + + >>> fproduct(f, tuple()) + () + >>> fproduct(f, (1, 2, 3)) + ((1, '1'), (2, '2'), (3, '3')) + """ + return fmap(lambda a: (a, f(a)), fa) + + +def lift(f: Callable[[A_in], B_out]) -> Callable[[Seq[A_in]], Seq[B_out]]: + """ + Lift a function f to operate on :class:`Seq`. + + >>> def f(x: int) -> str: + ... return str(x) + + >>> lifted = lift(f) + + >>> lifted(tuple()) + () + >>> lifted((1, 2, 3)) + ('1', '2', '3') + """ + return lambda fa: fmap(f, fa) + + +def product(fa: Seq[A], fb: Seq[B]) -> Seq[Tuple[A, B]]: + """ + Combine an `Seq[A]` and an `Seq[B]` into an `Seq[(A, B)]` + that maintains the effects of both `fa` and `fb`. + + Result is empty if at least one of given sequences is empty. + + >>> product(tuple(), tuple()) + () + >>> product((1, 2, 3), tuple()) + () + >>> product(tuple(), ('a', 'b')) + () + + Input sequences do not have to be of equal size. + + >>> product((1, 2, 3), ('a', 'b')) + ((1, 'a'), (1, 'b'), (2, 'a'), (2, 'b'), (3, 'a'), (3, 'b')) + """ + + # Because pylint does not allow `lambda a: fmap(lambda b: (a, b), fbs)`. + def ff(a: A) -> Seq[Tuple[A, B]]: + return fmap(lambda b: (a, b), fb) + + return flatmap(ff, fa) + + +def unit(a: A) -> Seq[A]: + """ + Unit value for :class:`Seq`. Returns 1-tuple. + + >>> unit(42) + (42,) + """ + return a, + + +def zip_map(f: Callable[..., A_out], *fx: Seq[Any]) -> Seq[A_out]: + """ + >>> def f(a: int, b: str) -> str: + ... return b * a + + Result is empty if at least one of given sequences is empty. + + >>> zip_map(f, tuple(), tuple()) + () + >>> zip_map(f, (1, 2, 3), tuple()) + () + >>> zip_map(f, tuple(), ('a', 'b')) + () + + Input sequences do not have to be of equal size. + + >>> zip_map(f, (1, 2, 3, 4), ('a', 'b', 'c')) + ('a', 'bb', 'ccc') + """ + return tuple(starmap(f, zip(*fx))) diff --git a/pytoolz/functoolz/traverse/__init__.py b/pytoolz/functoolz/traverse/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pytoolz/functoolz/traverse/opt.py b/pytoolz/functoolz/traverse/opt.py new file mode 100644 index 0000000..fa72702 --- /dev/null +++ b/pytoolz/functoolz/traverse/opt.py @@ -0,0 +1,102 @@ +from typing import Callable, Iterable, Optional + +from cytoolz.itertoolz import cons, identity + +from pytoolz.functoolz import A, A_in, B +from pytoolz.functoolz.opt import fmap, fmap2 +from pytoolz.itertoolz import fold_right +from pytoolz.typing import Seq + + +def sequence_iter(gfa: Iterable[Optional[A]]) -> Optional[Iterable[A]]: + """ + Thread all the `Optional` effects through the `Iterable` structure. + + >>> from typing import NamedTuple + >>> class Foo(NamedTuple): + ... bar: int + + >>> gfa = iter([Foo(1), Foo(2), Foo(3)]) + + >>> tuple(sequence_iter(gfa)) + (Foo(bar=1), Foo(bar=2), Foo(bar=3)) + + **Warn**: This operation is terminal in input argument and thus not a pure + function. + + >>> next(gfa) + Traceback (most recent call last): + ... + StopIteration + + >>> sequence_iter(iter([Foo(1), None, Foo(3)])) + + >>> tuple(sequence_iter(iter([]))) + () + + """ + return traverse_iter(identity, gfa) + + +def sequence_seq(gfa: Seq[Optional[A]]) -> Optional[Seq[A]]: + """ + Thread all the `Optional` effects through the `Seq` structure. + + >>> from typing import NamedTuple + >>> class Foo(NamedTuple): + ... bar: int + + >>> sequence_seq((Foo(1), Foo(2), Foo(3))) + (Foo(bar=1), Foo(bar=2), Foo(bar=3)) + + >>> sequence_seq((Foo(1), None, Foo(3))) + + >>> sequence_seq(tuple()) + () + """ + return fmap(tuple, traverse_iter(identity, gfa)) + + +def traverse_iter( + f: Callable[[A_in], Optional[B]], + seq: Iterable[A_in] +) -> Optional[Iterable[B]]: + """ + Given a function which returns a `Optional` effect, thread this effect + through the running of this function on all the values in `Iterable`, + returning an `Iterable[B]` in a `Optional` context. + + **Warn**: This operation is terminal in input iterable and thus not a pure + function. + """ + + def op(a: A_in, acc: Optional[Iterable[B]]) -> Optional[Iterable[B]]: + # FIXME: mypy cannot resolve correct type event though it's fine + op_res: Optional[Iterable[B]] = fmap2(cons, f(a), acc) + return op_res + + empty: Optional[Iterable[B]] = iter([]) + return fold_right(op, seq, empty) + + +def traverse_seq( + f: Callable[[A_in], Optional[B]], + fa: Seq[A_in] +) -> Optional[Seq[B]]: + """ + Given a function which returns a `Optional` effect, thread this effect + through the running of this function on all the values in `Seq`, + returning an `Seq[B]` in a `Optional` context. + + >>> def f(x: int) -> Optional[str]: + ... return str(x) if x > 0 else None + + >>> traverse_seq(f, (1, 2, 3)) + ('1', '2', '3') + + >>> traverse_seq(f, (0, 1, 2)) + + >>> traverse_seq(f, tuple()) + () + """ + return fmap(tuple, traverse_iter(f, fa)) diff --git a/pytoolz/itertoolz.py b/pytoolz/itertoolz.py index 65715df..9cf301f 100644 --- a/pytoolz/itertoolz.py +++ b/pytoolz/itertoolz.py @@ -1,5 +1,7 @@ +from functools import reduce from itertools import islice -from typing import Callable, Iterable, List, Optional, Tuple, TypeVar +from typing import Callable, Iterable, List, Optional, Reversible, Tuple, \ + TypeVar from cytoolz.functoolz import complement, compose from cytoolz.itertoolz import drop, identity, last as clast, peek, unique @@ -196,6 +198,34 @@ def first(seq: Seq[E]) -> Optional[E]: return seq[0] if seq else None +def fold_right(op: Callable[[A, B], B], xs: Iterable[A], z: B) -> B: + """ + Fold iterable `xs` by applying binary operator `op` from the *right* with + `z` being initial value. + + >>> def op(a: int, b: str) -> str: + ... return f'{b} then {a}' + + >>> xs = iter([1, 2, 3]) + >>> fold_right(op, xs, '4') + '4 then 3 then 2 then 1' + + This operation is terminal in `xs`. + + >>> next(xs) + Traceback (most recent call last): + ... + StopIteration + + Initial value is returned given an empty iterable. + + >>> fold_right(op, iter([]), '42') + '42' + """ + seq: Reversible[A] = xs if isinstance(xs, Reversible) else list(xs) + return reduce(lambda right, left: op(left, right), reversed(seq), z) + + def head_tail(it: Iterable[E]) -> Tuple[E, Iterable[E]]: """ Split provided iterable into head element and tail iterable. diff --git a/setup.py b/setup.py index 4115e97..9c79d39 100644 --- a/setup.py +++ b/setup.py @@ -16,13 +16,16 @@ def has_ext_modules(self) -> bool: test_requirements = [ + 'astroid==2.2.0', 'coverage==4.5.1', 'flake8==3.7.7', - 'mypy==0.700', + # 'mypy==0.700', + 'mypy==0.630', 'nose==1.3.7', # 'nose2==0.8.0', # 'nose2[coverage_plugin]>=0.6.5', 'pylint==2.3.0', + 'typed-ast==1.1.1', ] setup( From 8f98cad60f01dfd6ae998f43bae69b54832b28c8 Mon Sep 17 00:00:00 2001 From: Martin Matyasek Date: Thu, 11 Apr 2019 09:52:04 +0200 Subject: [PATCH 2/7] Adds travis release check --- .travis.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..67ef645 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,16 @@ +sudo: required +dist: xenial +language: python +python: + - "3.6" + - "3.7" + +install: + - make install + +script: + - make release-check + +after_success: + - pip install coveralls --use-mirrors + - coveralls From 397f2bc4681b22fcc4bb362b17fe62127b0b2ffa Mon Sep 17 00:00:00 2001 From: Martin Matyasek Date: Thu, 11 Apr 2019 10:59:45 +0200 Subject: [PATCH 3/7] Adds coverage badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ced181d..5bf7a10 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # pytoolz +[![Coverage Status](https://coveralls.io/repos/github/blindspot-ai/pytoolz/badge.svg?branch=master)](https://coveralls.io/github/blindspot-ai/pytoolz?branch=master) Collection of higher-order and utility functions built on top of `cytoolz`. From de8843f74726deaf44e82c9f424502b176a8ba63 Mon Sep 17 00:00:00 2001 From: Martin Matyasek Date: Thu, 11 Apr 2019 11:51:27 +0200 Subject: [PATCH 4/7] Adds build badge and fixes dependencies --- README.md | 2 +- setup.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5bf7a10..6606f97 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # pytoolz -[![Coverage Status](https://coveralls.io/repos/github/blindspot-ai/pytoolz/badge.svg?branch=master)](https://coveralls.io/github/blindspot-ai/pytoolz?branch=master) +[![Build Status](https://travis-ci.com/blindspot-ai/pytoolz.svg?branch=master)](https://travis-ci.com/blindspot-ai/pytoolz) [![Coverage Status](https://coveralls.io/repos/github/blindspot-ai/pytoolz/badge.svg?branch=master)](https://coveralls.io/github/blindspot-ai/pytoolz?branch=master) Collection of higher-order and utility functions built on top of `cytoolz`. diff --git a/setup.py b/setup.py index 9c79d39..0b5196a 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,8 @@ def has_ext_modules(self) -> bool: test_requirements = [ - 'astroid==2.2.0', + # 'astroid==2.2.5', + 'astroid==2.1.0', 'coverage==4.5.1', 'flake8==3.7.7', # 'mypy==0.700', @@ -24,7 +25,8 @@ def has_ext_modules(self) -> bool: 'nose==1.3.7', # 'nose2==0.8.0', # 'nose2[coverage_plugin]>=0.6.5', - 'pylint==2.3.0', + # 'pylint==2.3.0', + 'pylint==2.2.0', 'typed-ast==1.1.1', ] From 3b12d018578ed4249cc8ccc284739d4ac4ffeac0 Mon Sep 17 00:00:00 2001 From: Martin Matyasek Date: Thu, 11 Apr 2019 15:52:56 +0200 Subject: [PATCH 5/7] Renames project to ftoolz --- .coveragerc | 2 +- .gitignore | 1 + Makefile | 18 ++++++++++++------ README.md | 14 +++++++------- {pytoolz => ftoolz}/__init__.py | 0 {pytoolz => ftoolz}/dicttoolz.py | 2 +- {pytoolz => ftoolz}/functoolz/__init__.py | 0 {pytoolz => ftoolz}/functoolz/iter.py | 2 +- {pytoolz => ftoolz}/functoolz/opt.py | 2 +- {pytoolz => ftoolz}/functoolz/seq.py | 4 ++-- .../functoolz/traverse/__init__.py | 0 {pytoolz => ftoolz}/functoolz/traverse/opt.py | 8 ++++---- {pytoolz => ftoolz}/itertoolz.py | 2 +- {pytoolz => ftoolz}/predicates.py | 0 {pytoolz => ftoolz}/typing.py | 2 +- setup.py | 10 +++++----- stubs/nose/__init__.pyi | 2 +- tests/run.py | 4 ++-- unittest.cfg | 2 +- 19 files changed, 41 insertions(+), 34 deletions(-) rename {pytoolz => ftoolz}/__init__.py (100%) rename {pytoolz => ftoolz}/dicttoolz.py (97%) rename {pytoolz => ftoolz}/functoolz/__init__.py (100%) rename {pytoolz => ftoolz}/functoolz/iter.py (98%) rename {pytoolz => ftoolz}/functoolz/opt.py (98%) rename {pytoolz => ftoolz}/functoolz/seq.py (97%) rename {pytoolz => ftoolz}/functoolz/traverse/__init__.py (100%) rename {pytoolz => ftoolz}/functoolz/traverse/opt.py (94%) rename {pytoolz => ftoolz}/itertoolz.py (99%) rename {pytoolz => ftoolz}/predicates.py (100%) rename {pytoolz => ftoolz}/typing.py (87%) diff --git a/.coveragerc b/.coveragerc index d09e430..7994392 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,7 +1,7 @@ [run] branch = True source = - pytoolz/ + ftoolz/ omit = [report] diff --git a/.gitignore b/.gitignore index 88621d8..afc1335 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ __pycache__/ .Python build/ develop-eggs/ +dist/ downloads/ eggs/ .eggs/ diff --git a/Makefile b/Makefile index 7a778f5..c4aa87e 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,9 @@ -.PHONY: help clean setup setup-dev install release-check type-check flake8-check lint tests +.PHONY: help build clean setup setup-dev install release-check type-check flake8-check lint tests .DEFAULT: help help: + @echo "make build" + @echo " build distribution directory" @echo "make clean" @echo " clean virtual environment" @echo "make setup" @@ -21,6 +23,10 @@ help: @echo "make release-check" @echo " run type-check, flake8 check, linting and tests" +build: + @echo ">>> building ftoolz distribution" + python setup.py sdist bdist_wheel + clean: rm -rf venv rm -rf dist @@ -40,16 +46,16 @@ install: clean python setup.py install type-check: - @echo ">>> checking types in pytoolz and tests" - MYPYPATH=./stubs mypy pytoolz tests || ( echo ">>> type check failed"; exit 1; ) + @echo ">>> checking types in ftoolz and tests" + MYPYPATH=./stubs mypy ftoolz tests || ( echo ">>> type check failed"; exit 1; ) flake8-check: - @echo ">>> enforcing PEP 8 style with flake8 in pytoolz and tests" - flake8 --config=.flake8 pytoolz/ tests/ || ( echo ">>> flake8 check failed"; exit 1; ) + @echo ">>> enforcing PEP 8 style with flake8 in ftoolz and tests" + flake8 --config=.flake8 ftoolz/ tests/ || ( echo ">>> flake8 check failed"; exit 1; ) lint: @echo ">>> linting code" - pylint -j 0 --rcfile .pylintrc pytoolz tests || ( echo ">>> linting failed"; exit 1; ) + pylint -j 0 --rcfile .pylintrc ftoolz tests || ( echo ">>> linting failed"; exit 1; ) tests: @echo ">>> running tests" diff --git a/README.md b/README.md index 6606f97..31dab37 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# pytoolz -[![Build Status](https://travis-ci.com/blindspot-ai/pytoolz.svg?branch=master)](https://travis-ci.com/blindspot-ai/pytoolz) [![Coverage Status](https://coveralls.io/repos/github/blindspot-ai/pytoolz/badge.svg?branch=master)](https://coveralls.io/github/blindspot-ai/pytoolz?branch=master) +# ftoolz +[![Build Status](https://travis-ci.com/blindspot-ai/ftoolz.svg?branch=master)](https://travis-ci.com/blindspot-ai/ftoolz) [![Coverage Status](https://coveralls.io/repos/github/blindspot-ai/ftoolz/badge.svg?branch=master)](https://coveralls.io/github/blindspot-ai/ftoolz?branch=master) Collection of higher-order and utility functions built on top of `cytoolz`. ## Module overview -Pytoolz are split into few generic modules. +Ftoolz are split into few generic modules. ### functoolz package Package that provides higher-order functions commonly associated with Functor, Applicative and Monad. @@ -117,14 +117,14 @@ Cytoolz is a cython implementation of a python library supporting functional sty We highly recommend reading the API docs and using it in your project. -Pytoolz does not fork but rather extends cytoolz and provides typed stubs for it's API. +Ftoolz does not fork but rather extends cytoolz and provides typed stubs for it's API. Please note that the typed stubs do not cover all the functions from cytoolz. Also some valid cases might not be covered due to Python's restricted typing capabilities. ## Setup development environment -It is highly recommended to use virtual environment to develop and test `pytoolz`. For making things easy there are -two make targets to setup `pytoolz`: +It is highly recommended to use virtual environment to develop and test `ftoolz`. For making things easy there are +two make targets to setup `ftoolz`: * `make setup-dev` which creates new virtual environment in `./venv` * `make setup` that just installs dependencies for development @@ -145,7 +145,7 @@ make type-check ``` ### Code style checking -Pytoolz uses [Flake8](http://flake8.pycqa.org/en/latest/index.html) for enforcing PEP 8 and other code smells. +Ftoolz uses [Flake8](http://flake8.pycqa.org/en/latest/index.html) for enforcing PEP 8 and other code smells. ```bash make flake8-check ``` diff --git a/pytoolz/__init__.py b/ftoolz/__init__.py similarity index 100% rename from pytoolz/__init__.py rename to ftoolz/__init__.py diff --git a/pytoolz/dicttoolz.py b/ftoolz/dicttoolz.py similarity index 97% rename from pytoolz/dicttoolz.py rename to ftoolz/dicttoolz.py index a0b008e..58f8bb5 100644 --- a/pytoolz/dicttoolz.py +++ b/ftoolz/dicttoolz.py @@ -1,6 +1,6 @@ from typing import Any, TypeVar -from pytoolz.typing import Map +from ftoolz.typing import Map K = TypeVar('K') V = TypeVar('V') diff --git a/pytoolz/functoolz/__init__.py b/ftoolz/functoolz/__init__.py similarity index 100% rename from pytoolz/functoolz/__init__.py rename to ftoolz/functoolz/__init__.py diff --git a/pytoolz/functoolz/iter.py b/ftoolz/functoolz/iter.py similarity index 98% rename from pytoolz/functoolz/iter.py rename to ftoolz/functoolz/iter.py index 0fa169e..c93cbba 100644 --- a/pytoolz/functoolz/iter.py +++ b/ftoolz/functoolz/iter.py @@ -3,7 +3,7 @@ from cytoolz.itertoolz import identity, mapcat -from pytoolz.functoolz import A, A_in, A_out, B, B_in, B_out, C_out +from ftoolz.functoolz import A, A_in, A_out, B, B_in, B_out, C_out def apply( diff --git a/pytoolz/functoolz/opt.py b/ftoolz/functoolz/opt.py similarity index 98% rename from pytoolz/functoolz/opt.py rename to ftoolz/functoolz/opt.py index 776d4d1..cfad2ac 100644 --- a/pytoolz/functoolz/opt.py +++ b/ftoolz/functoolz/opt.py @@ -1,6 +1,6 @@ from typing import Any, Callable, Optional, Tuple -from pytoolz.functoolz import A, A_in, A_out, B, B_in, B_out, C_in, C_out, \ +from ftoolz.functoolz import A, A_in, A_out, B, B_in, B_out, C_in, C_out, \ D_out diff --git a/pytoolz/functoolz/seq.py b/ftoolz/functoolz/seq.py similarity index 97% rename from pytoolz/functoolz/seq.py rename to ftoolz/functoolz/seq.py index 41c4d0f..0f5c3d6 100644 --- a/pytoolz/functoolz/seq.py +++ b/ftoolz/functoolz/seq.py @@ -3,8 +3,8 @@ from cytoolz.itertoolz import identity, mapcat -from pytoolz.functoolz import A, A_in, A_out, B, B_in, B_out, C_out -from pytoolz.typing import Seq +from ftoolz.functoolz import A, A_in, A_out, B, B_in, B_out, C_out +from ftoolz.typing import Seq def apply(ff: Seq[Callable[[A_in], B_out]], fa: Seq[A_in]) -> Seq[B_out]: diff --git a/pytoolz/functoolz/traverse/__init__.py b/ftoolz/functoolz/traverse/__init__.py similarity index 100% rename from pytoolz/functoolz/traverse/__init__.py rename to ftoolz/functoolz/traverse/__init__.py diff --git a/pytoolz/functoolz/traverse/opt.py b/ftoolz/functoolz/traverse/opt.py similarity index 94% rename from pytoolz/functoolz/traverse/opt.py rename to ftoolz/functoolz/traverse/opt.py index fa72702..b09d06d 100644 --- a/pytoolz/functoolz/traverse/opt.py +++ b/ftoolz/functoolz/traverse/opt.py @@ -2,10 +2,10 @@ from cytoolz.itertoolz import cons, identity -from pytoolz.functoolz import A, A_in, B -from pytoolz.functoolz.opt import fmap, fmap2 -from pytoolz.itertoolz import fold_right -from pytoolz.typing import Seq +from ftoolz.functoolz import A, A_in, B +from ftoolz.functoolz.opt import fmap, fmap2 +from ftoolz.itertoolz import fold_right +from ftoolz.typing import Seq def sequence_iter(gfa: Iterable[Optional[A]]) -> Optional[Iterable[A]]: diff --git a/pytoolz/itertoolz.py b/ftoolz/itertoolz.py similarity index 99% rename from pytoolz/itertoolz.py rename to ftoolz/itertoolz.py index 9cf301f..ebe9e85 100644 --- a/pytoolz/itertoolz.py +++ b/ftoolz/itertoolz.py @@ -6,7 +6,7 @@ from cytoolz.functoolz import complement, compose from cytoolz.itertoolz import drop, identity, last as clast, peek, unique -from pytoolz.typing import Map, Seq +from ftoolz.typing import Map, Seq A = TypeVar('A') B = TypeVar('B') diff --git a/pytoolz/predicates.py b/ftoolz/predicates.py similarity index 100% rename from pytoolz/predicates.py rename to ftoolz/predicates.py diff --git a/pytoolz/typing.py b/ftoolz/typing.py similarity index 87% rename from pytoolz/typing.py rename to ftoolz/typing.py index a58c059..1dfbb91 100644 --- a/pytoolz/typing.py +++ b/ftoolz/typing.py @@ -20,7 +20,7 @@ def assert_some(a: Optional[A]) -> A: >>> assert_some(None) Traceback (most recent call last): ... - pytoolz.typing.TypingError: Item expected to be not None, None given. + ftoolz.typing.TypingError: Item expected to be not None, None given. """ if a is None: raise TypingError('Item expected to be not None, None given.') diff --git a/setup.py b/setup.py index 0b5196a..a0fb20a 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import find_packages, setup from setuptools.dist import Distribution -import pytoolz +import ftoolz class BinaryDistribution(Distribution): @@ -31,8 +31,8 @@ def has_ext_modules(self) -> bool: ] setup( - name='pytoolz', - version=pytoolz.__version__, + name='ftoolz', + version=ftoolz.__version__, description='Collection of higher-order and utility functions', long_description=(open('README.md').read() if exists('README.md') else ''), classifiers=[ @@ -46,11 +46,11 @@ def has_ext_modules(self) -> bool: 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', ], - url='https://bitbucket.org/blindspotsolutions/pytoolz/', + url='https://github.com/blindspot-ai/ftoolz', maintainer='Martin Matyasek', maintainer_email='martin.matyasek@blindspot.ai', keywords='functional utility cytoolz itertools functools', - packages=find_packages(include=['pytoolz']), + packages=find_packages(include=['ftoolz']), include_package_data=True, distclass=BinaryDistribution, zip_safe=False, diff --git a/stubs/nose/__init__.pyi b/stubs/nose/__init__.pyi index 011c2c4..05fa89b 100644 --- a/stubs/nose/__init__.pyi +++ b/stubs/nose/__init__.pyi @@ -1,6 +1,6 @@ from typing import Any -from pytoolz.typing import Seq +from ftoolz.typing import Seq def main(defaultTest: Seq[str], **kwargs: Any) -> None: ... diff --git a/tests/run.py b/tests/run.py index 88da5c4..9bcd167 100644 --- a/tests/run.py +++ b/tests/run.py @@ -6,6 +6,6 @@ sys.argv.append('--with-coverage') sys.argv.append('--cover-branches') sys.argv.append('--cover-erase') - sys.argv.append('--cover-package=pytoolz') + sys.argv.append('--cover-package=ftoolz') - nose.main(defaultTest=['pytoolz', '.']) + nose.main(defaultTest=['ftoolz', '.']) diff --git a/unittest.cfg b/unittest.cfg index b640d31..dc2089b 100644 --- a/unittest.cfg +++ b/unittest.cfg @@ -1,6 +1,6 @@ [unittest] start-dir = tests -code-directories = pytoolz +code-directories = ftoolz plugins = nose2.plugins.doctests [doctest] From 6bda3deb7e5038f83aa4836c84ee9de00e5a8000 Mon Sep 17 00:00:00 2001 From: Martin Matyasek Date: Thu, 11 Apr 2019 16:45:35 +0200 Subject: [PATCH 6/7] Fixes some build make targets and adds test dist via twine --- Makefile | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index c4aa87e..687aa57 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,13 @@ -.PHONY: help build clean setup setup-dev install release-check type-check flake8-check lint tests +.PHONY: help build clean clean-build setup setup-dev install release-check type-check flake8-check lint tests twine-release-test .DEFAULT: help help: @echo "make build" - @echo " build distribution directory" + @echo " build distribution directories" @echo "make clean" - @echo " clean virtual environment" + @echo " clean virtual environment and distribution" + @echo "make clean-build" + @echo " clean distribution directories" @echo "make setup" @echo " setup development environment" @echo "make setup-dev" @@ -22,13 +24,17 @@ help: @echo " run unit and doc tests" @echo "make release-check" @echo " run type-check, flake8 check, linting and tests" + @echo "make twine-release-test" + @echo " release ftoolz to test pypi using twine" -build: +build: clean-build @echo ">>> building ftoolz distribution" - python setup.py sdist bdist_wheel + python setup.py sdist -clean: +clean: clean-build rm -rf venv + +clean-build: rm -rf dist rm -rf build rm -rf *.egg-info @@ -63,3 +69,6 @@ tests: # python setup.py test release-check: type-check flake8-check lint tests + +twine-release-test: build + python3 -m twine upload --repository-url https://test.pypi.org/legacy/ dist/* From c396778c1b9b0f033cb7857d70a8838523f36451 Mon Sep 17 00:00:00 2001 From: Martin Matyasek Date: Thu, 11 Apr 2019 17:37:37 +0200 Subject: [PATCH 7/7] Fixes coveralls in travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 67ef645..00a8c4d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,10 +7,10 @@ python: install: - make install + - pip install coveralls script: - make release-check after_success: - - pip install coveralls --use-mirrors - coveralls