Browse Source

initial commit

Médéric Hurier 4 months ago
commit
38e50e7d12

+ 15
- 0
.gitignore View File

@@ -0,0 +1,15 @@
1
+.tox
2
+.env
3
+dist/
4
+build/
5
+.venv/
6
+.cache/
7
+*.py[cod]
8
+*.zipapp/
9
+.coverage
10
+*.egg-info/
11
+__pycache__/
12
+.hypothesis/
13
+.mypy_cache/
14
+.pytest_cache/
15
+.ipynb_checkpoints/

+ 1
- 0
.python-version View File

@@ -0,0 +1 @@
1
+3.7.0

+ 1
- 0
LICENSE.txt View File

@@ -0,0 +1 @@
1
+EUPL-1.2

+ 16
- 0
Makefile View File

@@ -0,0 +1,16 @@
1
+MKFILES = $(wildcard */Makefile)
2
+
3
+.venv:
4
+	python -m venv .venv --clear
5
+
6
+init: .venv
7
+	@for MK in ${MKFILES}; do make --no-print-directory -f $$MK init-$$(dirname $$MK); done
8
+
9
+clean:
10
+	@for MK in ${MKFILES}; do make --no-print-directory -f $$MK clean-$$(dirname $$MK); done
11
+
12
+commit: .venv
13
+	@set -e; \
14
+	for MK in ${MKFILES}; do make --no-print-directory -f $$MK commit-$$(dirname $$MK); done
15
+
16
+include */Makefile

+ 1
- 0
README.md View File

@@ -0,0 +1 @@
1
+# funpy

+ 17
- 0
covers/Makefile View File

@@ -0,0 +1,17 @@
1
+.PHONY: cover
2
+
3
+NAME = $(shell .venv/bin/python setup.py --name)
4
+
5
+init-covers: .venv
6
+	.venv/bin/pip install '.[covers]'
7
+
8
+clean-covers:
9
+	find . -name '.mypy_cache' -exec rm -fr {} +
10
+
11
+commit-covers: report;
12
+
13
+cover: .venv
14
+	.venv/bin/coverage run --rcfile="covers/coverage.ini" --source ${NAME} -m pytest --doctest-modules -q
15
+
16
+report: .venv cover
17
+	.venv/bin/coverage report --rcfile="covers/coverage.ini"

+ 5
- 0
covers/coverage.ini View File

@@ -0,0 +1,5 @@
1
+[run]
2
+branch = True
3
+
4
+[report]
5
+fail_under = 80.0

+ 1
- 0
covers/requirements.txt View File

@@ -0,0 +1 @@
1
+coverage

+ 13
- 0
devs/Makefile View File

@@ -0,0 +1,13 @@
1
+init-devs: .venv
2
+	.venv/bin/pip install -e .
3
+	.venv/bin/pip install '.[devs]'
4
+
5
+clean-devs:
6
+	find . -name '*~' -exec rm -f {} +
7
+	find . -name '*.pyc' -exec rm -f {} +
8
+	find . -name '*.pyo' -exec rm -f {} +
9
+	find . -name '*.egg' -exec rm -f {} +
10
+	find . -name '*.egg-info' -exec rm -fr {} +
11
+	find . -name '__pycache__' -exec rm -fr {} +
12
+
13
+commit-devs: ;

+ 2
- 0
devs/requirements.txt View File

@@ -0,0 +1,2 @@
1
+pip>=18
2
+setuptools>=40

+ 11
- 0
formats/Makefile View File

@@ -0,0 +1,11 @@
1
+.PHONY: format
2
+
3
+init-formats: .venv
4
+	.venv/bin/pip install '.[formats]'
5
+
6
+clean-formats: ;
7
+
8
+commit-formats: format;
9
+
10
+format: .venv
11
+	.venv/bin/black --config=formats/black.ini .

+ 2
- 0
formats/black.ini View File

@@ -0,0 +1,2 @@
1
+[tool.black]
2
+line-length = 88

+ 1
- 0
formats/requirements.txt View File

@@ -0,0 +1 @@
1
+black

+ 0
- 0
funpy/__init__.py View File


+ 299
- 0
funpy/fn.py View File

@@ -0,0 +1,299 @@
1
+"""Function library."""
2
+
3
+import copy as Copy
4
+import functools
5
+
6
+# TYPES {{{
7
+from typing import Callable, Iterable, TypeVar, Union
8
+
9
+Predicate = Callable[..., bool]
10
+
11
+X = TypeVar("X")
12
+Y = TypeVar("Y")
13
+Z = TypeVar("Z")
14
+
15
+# }}}
16
+# COPIERS {{{
17
+def copy(x: X, deep: bool = False) -> X:
18
+    """Return a copy of x.
19
+
20
+    >>> a = [1, 2, 3]
21
+    >>> a == copy(a, True)
22
+    True
23
+    >>> a == copy(a, False)
24
+    True
25
+    """
26
+    return Copy.deepcopy(x) if deep else Copy.copy(x)
27
+
28
+
29
+def copies(x: X, n: int = 2, deep: bool = False) -> Iterable[X]:
30
+    """Return n copy of x.
31
+
32
+    >>> a = [1, 2, 3]
33
+    >>> b, c = copies(a)
34
+    >>> a == b == c
35
+    True
36
+    """
37
+    assert n > 0, "n must be greater than 0"
38
+
39
+    for _ in range(n):
40
+        yield copy(x, deep)
41
+
42
+
43
+# }}}
44
+# WRAPPERS {{{
45
+wraps = functools.wraps
46
+
47
+partial = functools.partial
48
+
49
+partialme = functools.partialmethod
50
+
51
+
52
+def flip(f: Callable[..., Y]) -> Callable[..., Y]:
53
+    """"Flip f arguments.
54
+
55
+    >>> flip(print)(1, 2, 3, sep=',')
56
+    3,2,1
57
+    """
58
+
59
+    @functools.wraps(f)
60
+    def wrapped(*args, **kwargs):
61
+        return f(*reversed(args), **kwargs)
62
+
63
+    return wrapped
64
+
65
+
66
+def partialfp(f: Callable[..., Y], *args, **kwargs) -> Callable[..., Y]:
67
+    """Flip and partial f.
68
+
69
+    >>> partialfp(print, 0, 1, sep=',')(2, 3)
70
+    3,2,1,0
71
+    """
72
+    return partial(flip(f), *args, **kwargs)
73
+
74
+
75
+# }}}
76
+# LOW-ORDER {{{
77
+def ident(x: X) -> X:
78
+    """Return x unchanged.
79
+
80
+    >>> ident(0) is 0
81
+    True
82
+    >>> ident(1) is 1
83
+    True
84
+    >>> ident(None) is None
85
+    True
86
+    """
87
+    return x
88
+
89
+
90
+def constantly(x: X) -> Callable[..., X]:
91
+    """Constantly returns x through a function.
92
+
93
+    >>> constantly(0)(1, 2, x=3)
94
+    0
95
+    """
96
+
97
+    def constant(*_, **__):
98
+        return x
99
+
100
+    return constant
101
+
102
+
103
+# }}}
104
+# HIGH-ORDER {{{
105
+def compose(f: Callable[[Z], Y], g: Callable[..., Z]) -> Callable[..., Y]:
106
+    """Compose two functions left to right.
107
+
108
+    >>> compose(range, list)(3)
109
+    [0, 1, 2]
110
+    """
111
+
112
+    def composed(*args, **kwargs):
113
+        return g(f(*args, **kwargs))
114
+
115
+    return composed
116
+
117
+
118
+def comp(*fs: Callable) -> Callable:
119
+    """Compose functions from left to right.
120
+
121
+    >>> comp()(2)
122
+    2
123
+    >>> comp(float)(2)
124
+    2.0
125
+    >>> comp(range, list)(2)
126
+    [0, 1]
127
+    >>> comp(range, list, len)(2)
128
+    2
129
+    """
130
+    if not fs:
131
+        return ident
132
+
133
+    return functools.reduce(compose, fs)
134
+
135
+
136
+def juxt(*fs: Callable) -> Callable[..., tuple]:
137
+    """Juxtapose functions results.
138
+
139
+    >>> juxt()(2)
140
+    (2,)
141
+    >>> juxt(float)(2)
142
+    (2.0,)
143
+    >>> juxt(float, str)(2)
144
+    (2.0, '2')
145
+    >>> juxt(float, str, bin)(2)
146
+    (2.0, '2', '0b10')
147
+    """
148
+    if not fs:
149
+        fs = (ident,)
150
+
151
+    def juxted(*args, **kwargs):
152
+        return tuple(f(*args, **kwargs) for f in fs)
153
+
154
+    return juxted
155
+
156
+
157
+# }}}
158
+# DECORATORS {{{
159
+memoize = functools.lru_cache
160
+
161
+comparator = functools.cmp_to_key
162
+
163
+totalordering = functools.total_ordering
164
+
165
+singledispatch = functools.singledispatch
166
+
167
+
168
+def pre(
169
+    f: Callable[..., Y], do: Callable[[tuple, dict], None] = print
170
+) -> Callable[..., Y]:
171
+    """Call do before f (decorator).
172
+
173
+    >>> pre(float)(2)
174
+    (2,) {}
175
+    2.0
176
+    """
177
+
178
+    @wraps(f)
179
+    def wrapped(*args, **kwargs):
180
+        do(args, kwargs)
181
+
182
+        return f(*args, **kwargs)
183
+
184
+    return wrapped
185
+
186
+
187
+def post(
188
+    f: Callable[..., Y], do: Callable[[tuple, dict, Y], None] = print
189
+) -> Callable[..., Y]:
190
+    """Call do after f (decorator).
191
+
192
+    >>> post(float)(2)
193
+    (2,) {} 2.0
194
+    2.0
195
+    """
196
+
197
+    @wraps(f)
198
+    def wrapped(*args, **kwargs):
199
+        res = f(*args, **kwargs)
200
+
201
+        do(args, kwargs, res)
202
+
203
+        return res
204
+
205
+    return wrapped
206
+
207
+
208
+def fnil(f: Callable[..., Y], x: X) -> Callable[..., Y]:
209
+    """Replace the first argument of f by x if None.
210
+
211
+    >>> fnil(pow, 5)(2, 3)
212
+    8
213
+    >>> fnil(pow, 5)(None, 3)
214
+    125
215
+    """
216
+
217
+    @wraps(f)
218
+    def wrapped(z: X, *args, **kwargs):
219
+        return f(x if z is None else z, *args, **kwargs)
220
+
221
+    return wrapped
222
+
223
+
224
+def safe(f: Callable[..., Y], x: Z = None) -> Callable[..., Union[Y, Z]]:
225
+    """Return x if f throws an exception (decorator).
226
+
227
+    >>> safe(abs)(2) == 2
228
+    True
229
+    >>> safe(abs)('a') is None
230
+    True
231
+    >>> safe(abs, 2)('a') == 2
232
+    True
233
+    """
234
+
235
+    @wraps(f)
236
+    def wrapped(*args, **kwargs):
237
+        try:
238
+            return f(*args, **kwargs)
239
+        except Exception:
240
+            return x
241
+
242
+    return wrapped
243
+
244
+
245
+def pure(f: Callable[..., None], deep: bool = False) -> Callable:
246
+    """Purify the side effect of f (decorator).
247
+    >>> l, append = [], pure(list.append)
248
+    >>> append(append(append(l, 0), 1), 2)
249
+    [0, 1, 2]
250
+    >>> l
251
+    []
252
+    """
253
+
254
+    @wraps(f)
255
+    def wrapped(x, *args, **kwargs):
256
+        x = copy(x, deep=deep)
257
+
258
+        f(x, *args, **kwargs)
259
+
260
+        return x
261
+
262
+    return wrapped
263
+
264
+
265
+def fluent(f: Callable[..., None]) -> Callable:
266
+    """Grant a fluent interface to f (decorator).
267
+    >>> l, append = [], fluent(list.append)
268
+    >>> append(append(append(l, 0), 1), 2)
269
+    [0, 1, 2]
270
+    >>> l
271
+    [0, 1, 2]
272
+    """
273
+
274
+    @wraps(f)
275
+    def wrapped(x, *args, **kwargs):
276
+        f(x, *args, **kwargs)
277
+
278
+        return x
279
+
280
+    return wrapped
281
+
282
+
283
+def complement(p: Predicate) -> Predicate:
284
+    """Reverse the logic of p (decorator).
285
+
286
+    >>> complement(bool)(True)
287
+    False
288
+    >>> complement(bool)(False)
289
+    True
290
+    """
291
+
292
+    @wraps(p)
293
+    def wrapped(*args, **kwargs):
294
+        return not p(*args, **kwargs)
295
+
296
+    return wrapped
297
+
298
+
299
+# }}}

+ 128
- 0
funpy/io.py View File

@@ -0,0 +1,128 @@
1
+"""Input/Output library."""
2
+
3
+import fileinput
4
+import glob as Glob
5
+import pprint as Print
6
+import textwrap
7
+
8
+# TYPES {{{
9
+from typing import AnyStr, Callable, Iterable, Iterator
10
+
11
+Path = str
12
+Mode = str
13
+# }}}
14
+# PATHS {{{
15
+glob = Glob.glob
16
+
17
+iglob = Glob.iglob
18
+# }}}
19
+# INPUTS {{{
20
+combine = fileinput.input
21
+
22
+
23
+def slurp(path: Path, mode: Mode = "r") -> AnyStr:
24
+    """Slurp data from path."""
25
+    with open(path, mode) as r:
26
+        return r.read()
27
+
28
+
29
+# }}}
30
+# OUTPUTS {{{
31
+pprint = Print.pprint
32
+
33
+
34
+def spit(path: Path, s: AnyStr, mode: Mode = "w") -> None:
35
+    """Spit data to path."""
36
+    with open(path, mode) as w:
37
+        w.write(s)
38
+
39
+
40
+# }}}
41
+# STREAMS {{{
42
+def interact(
43
+    f: Callable[[AnyStr], AnyStr], files: Iterable[Path] = None, mode: Mode = "r"
44
+) -> Iterator[AnyStr]:
45
+    """Apply f on all line in files."""
46
+    return map(f, combine(files, mode=mode))
47
+
48
+
49
+# }}}
50
+# STRINGS {{{
51
+wrap = textwrap.wrap
52
+
53
+fill = textwrap.fill
54
+
55
+indent = textwrap.indent
56
+
57
+dedent = textwrap.dedent
58
+
59
+shorten = textwrap.shorten
60
+
61
+
62
+def unnl(l: Iterable[str]) -> Iterator[str]:
63
+    """Remove new lines from l.
64
+
65
+    >>> list(unnl([]))
66
+    []
67
+    >>> list(unnl(['hello\\n', '\\n', '', 'world']))
68
+    ['hello', '', '', 'world']
69
+    """
70
+    return (x.rstrip("\n") for x in l)
71
+
72
+
73
+def unbl(l: Iterable[str]) -> Iterator[str]:
74
+    """Remove blank lines from l.
75
+
76
+    >>> list(unbl([]))
77
+    []
78
+    >>> list(unbl(['hello\\n', '\\n', '', 'world']))
79
+    ['hello\\n', '\\n', 'world']
80
+    """
81
+    return (x for x in l if x != "")
82
+
83
+
84
+def words(s: str) -> Iterator[str]:
85
+    """Return a list of words from s.
86
+
87
+    >>> list(words(''))
88
+    []
89
+    >>> list(words('hello fp  world    !'))
90
+    ['hello', 'fp', 'world', '!']
91
+    """
92
+    return (x for x in s.split(None))
93
+
94
+
95
+def unwords(l: Iterable[str]) -> str:
96
+    """Return joined words from l.
97
+
98
+    >>> unwords([])
99
+    ''
100
+    >>> unwords(['hello', '', 'world'])
101
+    'hello  world'
102
+    """
103
+    return " ".join(l)
104
+
105
+
106
+def lines(s: str) -> Iterator[str]:
107
+    """Return a list of lines from s.
108
+
109
+    >>> list(lines(''))
110
+    []
111
+    >>> list(lines('hello\\r\\n\\nworld'))
112
+    ['hello', '', 'world']
113
+    """
114
+    return (x for x in s.splitlines())
115
+
116
+
117
+def unlines(l: Iterable[str]) -> str:
118
+    """Return joined lines from l.
119
+
120
+    >>> unlines([])
121
+    ''
122
+    >>> unlines(['hello', '', 'world'])
123
+    'hello\\n\\nworld'
124
+    """
125
+    return "\n".join(l)
126
+
127
+
128
+# }}}

+ 670
- 0
funpy/it.py View File

@@ -0,0 +1,670 @@
1
+"""Iterator library"""
2
+
3
+import builtins
4
+import functools
5
+import itertools
6
+
7
+# TYPES {{{
8
+from typing import Any, Callable, Container, Iterable, Iterator, Optional, Tuple
9
+
10
+from funpy import fn, op
11
+
12
+# }}}
13
+# INITS {{{
14
+slice = itertools.islice
15
+# }}}
16
+# MAPPING {{{
17
+map = builtins.map
18
+
19
+starmap = itertools.starmap
20
+
21
+enumerate = builtins.enumerate
22
+
23
+accumulate = itertools.accumulate
24
+
25
+
26
+def mapcat(f: Callable, *ls: Iterable) -> Iterator:
27
+    """Apply f on ls and concat it.
28
+
29
+    >>> list(mapcat(lambda a, b: (a, b), (0, 1, 2), (3, 4, 5)))
30
+    [0, 3, 1, 4, 2, 5]
31
+    """
32
+    return itertools.chain.from_iterable(map(f, *ls))
33
+
34
+
35
+def mapevery(f: Callable, n: int, *ls: Iterable) -> Iterator:
36
+    """Apply f on every n value of l.
37
+
38
+    >>> list(mapevery(pow, 2, (0, 1, 2, 3), (4, 5, 6, 7)))
39
+    [0, (1, 5), 64, (3, 7)]
40
+    """
41
+    assert n > 0, "n must be greater than 0"
42
+
43
+    for i, x in enumerate(zip(*ls)):
44
+        if i % n == 0:
45
+            yield f(*x)
46
+        else:
47
+            yield tuple(x)
48
+
49
+
50
+def replace(l: Iterable, m: dict, d: Any = None) -> Iterator:
51
+    """Map l item with l or yield d.
52
+
53
+    >>> mapping = {'a': 1, 'b': 2}
54
+    >>> list(replace(('a',  'b', 'c', 'b', 'd'), mapping))
55
+    [1, 2, None, 2, None]
56
+    """
57
+    for x in l:
58
+        yield m.get(x, d)
59
+
60
+
61
+# }}}
62
+# ORDERING {{{
63
+sorted = builtins.sorted
64
+
65
+reversed = builtins.reversed
66
+# }}}
67
+# GROUPING {{{
68
+groupby = itertools.groupby
69
+
70
+
71
+def slide(l: Iterable, n: int) -> Iterator[tuple]:
72
+    """Create slides of size n from l.
73
+
74
+    >>> list(slide(range(5), 3))
75
+    [(0, 1, 2), (1, 2, 3), (2, 3, 4)]
76
+    >>> list(slide(range(2), 3))
77
+    []
78
+    """
79
+
80
+    def window(x):
81
+        i, slided = x
82
+
83
+        return drop(slided, i)
84
+
85
+    slides = tee(l, n)
86
+    windows = map(window, enumerate(slides))
87
+
88
+    return zip(*windows)
89
+
90
+
91
+def split(l: Iterable, n: int) -> Tuple[Iterator, Iterator]:
92
+    """Split l based on n.
93
+
94
+    >>> list(map(list, split(range(5), 2)))
95
+    [[0, 1], [2, 3, 4]]
96
+    >>> list(map(list, split(range(1), 2)))
97
+    [[0], []]
98
+    """
99
+    left, right = tee(l)
100
+
101
+    return take(left, n), drop(right, n)
102
+
103
+
104
+def splitby(l: Iterable, p: fn.Predicate = bool) -> Tuple[Iterator, Iterator]:
105
+    """Split l based on p.
106
+
107
+    >>> list(map(list, splitby(range(5), op.iseven)))
108
+    [[1, 3], [0, 2, 4]]
109
+    >>> list(map(list, splitby(range(1), op.iseven)))
110
+    [[], [0]]
111
+    """
112
+    left, right = tee(l)
113
+
114
+    return remove(p, left), filter(p, right)
115
+
116
+
117
+def chunk(l: Iterable, n: int) -> Iterator[tuple]:
118
+    """Create chunks of size n from l.
119
+
120
+    >>> list(map(list, chunk(range(8), 3)))
121
+    [[0, 1, 2], [3, 4, 5]]
122
+    >>> list(map(list, chunk(range(2), 3)))
123
+    []
124
+    """
125
+    assert n > 0, "n must be greater than 0"
126
+
127
+    chunking: list = []
128
+
129
+    for i, x in enumerate(l, 1):
130
+        chunking.append(x)
131
+
132
+        if i % n == 0:
133
+            yield tuple(chunking)
134
+            chunking.clear()
135
+
136
+
137
+def chunkby(l: Iterable, p: fn.Predicate = bool) -> Iterator[tuple]:
138
+    """Create chunks from n based on consecutive value of p.
139
+
140
+    >>> list(map(list, chunkby((0, 2, 1, 3, 0, 1), op.iseven)))
141
+    [[0, 2], [1, 3], [0], [1]]
142
+    """
143
+    for _, items in groupby(l, p):
144
+        yield tuple(items)
145
+
146
+
147
+def chunkall(l: Iterable, n: int) -> Iterator[tuple]:
148
+    """Create chunks of size n at least from l.
149
+
150
+    >>> list(map(list, chunkall(range(2), 3)))
151
+    [[0, 1]]
152
+    >>> list(map(list, chunkall(range(8), 3)))
153
+    [[0, 1, 2], [3, 4, 5], [6, 7]]
154
+    >>> list(map(list, chunkall(range(9), 3)))
155
+    [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
156
+    """
157
+    assert n > 0, "n must be greater than 0"
158
+
159
+    chunking: list = []
160
+
161
+    for i, x in enumerate(l, 1):
162
+        chunking.append(x)
163
+
164
+        if i % n == 0:
165
+            yield tuple(chunking)
166
+            chunking.clear()
167
+
168
+    if op.notempty(chunking):
169
+        yield tuple(chunking)
170
+
171
+
172
+def grouped(l: Iterable, f: Callable = fn.ident) -> Iterator[Tuple[Any, tuple]]:
173
+    """Group items of l based on f.
174
+
175
+    >>> list(grouped((1, 2, 3, 1, 2, 3, 1, 2)))
176
+    [(1, (1, 1, 1)), (2, (2, 2, 2)), (3, (3, 3))]
177
+    >>> list(grouped(({'a': 1}, {'a': 2}, {'a': 1}), op.getit('a')))
178
+    [(1, ({'a': 1}, {'a': 1})), (2, ({'a': 2},))]
179
+    """
180
+    index: dict = {}
181
+
182
+    for x in l:
183
+        k = f(x)
184
+
185
+        index.setdefault(k, [])
186
+        index[k].append(x)
187
+
188
+    for k, vs in index.items():
189
+        yield k, tuple(vs)
190
+
191
+
192
+def groupkv(l: Iterable):
193
+    """Group key value items of l based on key.
194
+
195
+    >>> list(groupkv(((0, 0), (1, 1), (0, 2), (1, 3))))
196
+    [(0, (0, 2)), (1, (1, 3))]
197
+    """
198
+    index: dict = {}
199
+
200
+    for k, v in l:
201
+        index.setdefault(k, [])
202
+        index[k].append(v)
203
+
204
+    for k, vs in index.items():
205
+        yield k, tuple(vs)
206
+
207
+
208
+# }}}
209
+# FILTERING {{{
210
+filter = builtins.filter
211
+remove = itertools.filterfalse
212
+
213
+compress = itertools.compress
214
+
215
+dropwhile = itertools.dropwhile
216
+takewhile = itertools.takewhile
217
+
218
+
219
+def member(l: Iterable, s: Container) -> Iterator:
220
+    """Return item from l member of s.
221
+
222
+    >>> list(member(range(5), {5, 7}))
223
+    []
224
+    >>> list(member(range(5), {1, 3, 5, 7}))
225
+    [1, 3]
226
+    """
227
+    for x in l:
228
+        if x in s:
229
+            yield x
230
+
231
+
232
+def locate(l: Iterable, p: fn.Predicate = bool):
233
+    """Return l index when p is True.
234
+
235
+    >>> list(locate(range(5), lambda x: x % 2 == 0))
236
+    [0, 2, 4]
237
+    """
238
+    for i, x in enumerate(l):
239
+        if p(x):
240
+            yield i
241
+
242
+
243
+def dedupe(l: Iterable, f: Callable = fn.ident) -> Iterator:
244
+    """Remove consecutive items from l based on f.
245
+
246
+    >>> list(dedupe((0, 0, 2, 1, 1, 3)))
247
+    [0, 2, 1, 3]
248
+    >>> list(dedupe((0, 0, 2, 1, 1, 3), op.iseven))
249
+    [0, 1]
250
+    """
251
+    lasty = None
252
+
253
+    for x in l:
254
+        y = f(x)
255
+
256
+        if y != lasty:
257
+            lasty = y
258
+            yield x
259
+
260
+
261
+def distinct(l: Iterable, f: Callable = fn.ident) -> Iterator:
262
+    """Return distinct items from l based on f.
263
+
264
+    >>> list(distinct((0, 0, 2, 1, 1, 3)))
265
+    [0, 2, 1, 3]
266
+    >>> list(distinct((0, 0, 2, 1, 1, 3), op.iseven))
267
+    [0, 1]
268
+    """
269
+    seen: set = set()
270
+
271
+    for x in l:
272
+        y = f(x)
273
+
274
+        if y not in seen:
275
+            seen.add(y)
276
+            yield x
277
+
278
+
279
+# }}}
280
+# REDUCTIONS {{{
281
+all = builtins.all
282
+any = builtins.any
283
+
284
+max = builtins.max
285
+min = builtins.min
286
+sum = builtins.sum
287
+
288
+reduce = functools.reduce
289
+
290
+
291
+def len(l: Iterable) -> int:
292
+    """Return the length of l.
293
+
294
+    >>> len(range(5))
295
+    5
296
+    """
297
+    i = None
298
+
299
+    for i, _ in enumerate(l, 1):
300
+        pass
301
+
302
+    return i
303
+
304
+
305
+def mult(l: Iterable, start: Any = 1) -> Any:
306
+    """Return the product of l.
307
+
308
+    >>> mult(range(1, 5))
309
+    24
310
+    >>> mult(range(1, 5), 10)
311
+    240
312
+    """
313
+    return reduce(op.mul, l, start)
314
+
315
+
316
+def contains(l: Iterable, x: Any) -> bool:
317
+    """Return True if l contains x.
318
+
319
+    >>> contains(range(9), 3)
320
+    True
321
+    >>> contains(range(2), 3)
322
+    False
323
+    """
324
+    return any(x == z for z in l)
325
+
326
+
327
+def quantify(l: Iterable, p: fn.Predicate = bool) -> int:
328
+    """Count how many times p is True.
329
+
330
+    >>> quantify(range(9), lambda x: x % 2 == 0)
331
+    5
332
+    >>> quantify(range(9), lambda x: x % 2 == 1)
333
+    4
334
+    """
335
+    return sum(map(p, l))
336
+
337
+
338
+def consume(l: Iterable) -> None:
339
+    """Consume an iterable.
340
+
341
+    >>> consume(range(10)) is None
342
+    True
343
+    >>> consume(range(0)) is None
344
+    True
345
+    """
346
+    for _ in l:
347
+        pass
348
+
349
+
350
+# }}}
351
+# SELECTIONS {{{
352
+def nth(l: Iterable, n: int, d: Any = None) -> Optional[Any]:
353
+    """Return the nth item of l or d.
354
+
355
+    >>> nth(range(5), 3)
356
+    3
357
+    >>> nth(range(5), 9) is None
358
+    True
359
+    >>> nth(range(5), 9, False)
360
+    False
361
+    """
362
+    assert n >= 0, "n must be greater or equals to 0"
363
+
364
+    for i, x in enumerate(l):
365
+        if i == n:
366
+            return x
367
+
368
+    return d
369
+
370
+
371
+def first(l: Iterable, d: Any = None) -> Optional[Any]:
372
+    """Return the first item of l or d.
373
+
374
+    >>> first(range(1, 5))
375
+    1
376
+    >>> first(range(0)) is None
377
+    True
378
+    """
379
+    return nth(l, 0, d=d)
380
+
381
+
382
+def second(l: Iterable, d: Any = None) -> Optional[Any]:
383
+    """Return the second item of l or d.
384
+
385
+    >>> second(range(1, 5))
386
+    2
387
+    >>> second(range(0)) is None
388
+    True
389
+    """
390
+    return nth(l, 1, d=d)
391
+
392
+
393
+def third(l: Iterable, d: Any = None) -> Optional[Any]:
394
+    """Return the third item of l or d.
395
+
396
+    >>> third(range(1, 5))
397
+    3
398
+    >>> third(range(0)) is None
399
+    True
400
+    """
401
+    return nth(l, 2, d=d)
402
+
403
+
404
+def last(it: Iterable, d: Any = None) -> Optional[Any]:
405
+    """Return the last item of l or d.
406
+
407
+    >>> last(range(1, 5))
408
+    4
409
+    >>> last(range(0)) is None
410
+    True
411
+    """
412
+    x = None
413
+
414
+    for x in it:
415
+        pass
416
+
417
+    try:
418
+        return x
419
+    except UnboundLocalError:
420
+        return d
421
+
422
+
423
+def butlast(l: Iterable) -> Iterator:
424
+    """Return all but the last item of l.
425
+
426
+    >>> list(butlast(range(0)))
427
+    []
428
+    >>> list(butlast(range(5)))
429
+    [0, 1, 2, 3]
430
+    """
431
+    left, right = tee(l)
432
+    right = rest(right)
433
+
434
+    for a, _ in zip(left, right):
435
+        yield a
436
+
437
+
438
+def sub(l: Iterable, start: int, stop: int) -> Iterator:
439
+    """Return item ranging from start to stop in l.
440
+
441
+    >>> list(sub(range(5), 3, 6))
442
+    [3, 4]
443
+    >>> list(sub(range(9), 3, 6))
444
+    [3, 4, 5]
445
+    """
446
+    return slice(l, start, stop)
447
+
448
+
449
+def rest(l: Iterable) -> Iterator:
450
+    """Skip the first item of l.
451
+
452
+    >>> list(rest(range(0)))
453
+    []
454
+    >>> list(rest(range(5)))
455
+    [1, 2, 3, 4]
456
+    """
457
+    return slice(l, 1, None)
458
+
459
+
460
+def take(l: Iterable, n: int) -> Iterator:
461
+    """Return the first n item of l.
462
+
463
+    >>> list(take(range(1), 5))
464
+    [0]
465
+    >>> list(take(range(9), 5))
466
+    [0, 1, 2, 3, 4]
467
+    """
468
+    return slice(l, n)
469
+
470
+
471
+def takenth(l: Iterable, n: int, start: int = 0, end: int = None) -> Iterator:
472
+    """Return every n item of l.
473
+
474
+    >>> list(takenth(range(1), 2))
475
+    [0]
476
+    >>> list(takenth(range(9), 2))
477
+    [0, 2, 4, 6, 8]
478
+    """
479
+    return slice(l, start, end, n)
480
+
481
+
482
+def takelast(l: Iterable, n: int) -> Iterator:
483
+    """Return the last n item of l.
484
+
485
+    >>> list(takelast(range(2), 3))
486
+    []
487
+    >>> list(takelast(range(9), 3))
488
+    [6, 7, 8]
489
+    """
490
+    sl: tuple = tuple()
491
+
492
+    for sl in slide(l, n):
493
+        pass
494
+
495
+    for x in sl:
496
+        yield x
497
+
498
+
499
+def drop(l: Iterable, n: int) -> Iterator:
500
+    """Drop the first n item of l.
501
+
502
+    >>> list(drop(range(3), 5))
503
+    []
504
+    >>> list(drop(range(9), 5))
505
+    [5, 6, 7, 8]
506
+    """
507
+    return slice(l, n, None)
508
+
509
+
510
+def dropnth(l: Iterable, n: int) -> Iterator:
511
+    """Drop every n item of l.
512
+
513
+    >>> list(dropnth(range(1), 2))
514
+    [0]
515
+    >>> list(dropnth(range(9), 2))
516
+    [0, 2, 4, 6, 8]
517
+    """
518
+    assert n > 0, "n must be greater than 0"
519
+
520
+    for i, x in enumerate(l, 1):
521
+        if i % n != 0:
522
+            yield x
523
+
524
+
525
+def droplast(l: Iterable, n: int) -> Iterator:
526
+    """Return the last n item of l.
527
+
528
+    >>> list(droplast(range(2), 3))
529
+    []
530
+    >>> list(droplast(range(9), 3))
531
+    [0, 1, 2, 3, 4, 5]
532
+    """
533
+    left, right = tee(l)
534
+    right = butlast(slide(right, n))
535
+
536
+    for a, _ in zip(left, right):
537
+        yield a
538
+
539
+
540
+def find(l: Iterable, p: fn.Predicate = bool, d: Any = None) -> Optional[Any]:
541
+    """Return the first item where p is True or d.
542
+
543
+    >>> find(range(3), lambda x: x > 5) is None
544
+    True
545
+    >>> find(range(9), lambda x: x > 5)
546
+    6
547
+    """
548
+    for x in l:
549
+        if p(x):
550
+            return x
551
+
552
+    return d
553
+
554
+
555
+# }}}
556
+# CONSTRUCTIONS {{{
557
+tee = itertools.tee
558
+
559
+range = builtins.range
560
+count = itertools.count
561
+
562
+cycle = itertools.cycle
563
+repeat = itertools.repeat
564
+
565
+cat = itertools.chain
566
+concat = itertools.chain.from_iterable
567
+
568
+zip = builtins.zip
569
+transpose = builtins.zip
570
+ziplong = itertools.zip_longest
571
+
572
+
573
+def pad(l: Iterable, d: Any = None) -> Iterator:
574
+    """Yield items from l then always yield d.
575
+
576
+    >>> list(slice(pad(range(3)), 5))
577
+    [0, 1, 2, None, None]
578
+    >>> list(slice(pad(range(3), 0), 5))
579
+    [0, 1, 2, 0, 0]
580
+    """
581
+    return cat(l, repeat(d))
582
+
583
+
584
+def cons(l: Iterable, x: Any) -> Iterator:
585
+    """Prepend x to l.
586
+
587
+    >>> list(cons(range(1, 3), 0))
588
+    [0, 1, 2]
589
+    """
590
+    yield x
591
+    yield from l
592
+
593
+
594
+def conj(l: Iterable, x: Any) -> Iterator:
595
+    """Append x to l.
596
+
597
+    >>> list(conj(range(1, 3), 0))
598
+    [1, 2, 0]
599
+    """
600
+    yield from l
601
+    yield x
602
+
603
+
604
+def iterate(f: Callable, x: Any) -> Iterator:
605
+    """Call f on x and update x.
606
+
607
+    >>> list(slice(iterate(op.inc, 0), 3))
608
+    [0, 1, 2]
609
+    """
610
+    while True:
611
+        yield x
612
+        x = f(x)
613
+
614
+
615
+def iterwith(x: Any) -> Iterator:
616
+    """Yield from x in a context."""
617
+    with x:
618
+        yield from x
619
+
620
+
621
+def tabulate(f: Callable, start: int = 0, step: int = 1) -> Iterator:
622
+    """Apply f on every natural numbers.
623
+
624
+    >>> list(slice(tabulate(str), 5))
625
+    ['0', '1', '2', '3', '4']
626
+    >>> list(slice(tabulate(str, 2, 2), 5))
627
+    ['2', '4', '6', '8', '10']
628
+    """
629
+    yield from map(f, count(start, step))
630
+
631
+
632
+def interleave(*ls: Iterable) -> Iterator:
633
+    """Yield item from each iterator successively.
634
+
635
+    >>> list(interleave(range(3), range(3, 6)))
636
+    [0, 3, 1, 4, 2, 5]
637
+    >>> list(interleave(range(3), range(3, 7)))
638
+    [0, 3, 1, 4, 2, 5]
639
+    """
640
+    return concat(zip(*ls))
641
+
642
+
643
+def interchange(*ls: Iterable, x: Any = None) -> Iterator:
644
+    """Yield item from each iterator successively or fill with x.
645
+
646
+    >>> list(interchange(range(3), range(3, 6)))
647
+    [0, 3, 1, 4, 2, 5]
648
+    >>> list(interchange(range(3), range(3, 7)))
649
+    [0, 3, 1, 4, 2, 5, None, 6]
650
+    """
651
+    return concat(ziplong(*ls, fillvalue=x))
652
+
653
+
654
+def interpose(l: Iterable, x: Any) -> Iterator:
655
+    """Yield item from l alternating with x.
656
+
657
+    >>> list(interpose(range(1, 4), 0))
658
+    [1, 0, 2, 0, 3]
659
+    """
660
+    return drop(interleave(repeat(x), l), 1)
661
+
662
+
663
+# }}}
664
+# COMBINATORICS {{{
665
+product = itertools.product
666
+permutated = itertools.permutations
667
+permutations = itertools.permutations
668
+combinations = itertools.combinations
669
+combinatoric = itertools.combinations_with_replacement
670
+# }}}

+ 285
- 0
funpy/op.py View File

@@ -0,0 +1,285 @@
1
+"""Operator library."""
2
+
3
+
4
+import builtins
5
+import fractions
6
+import math
7
+import operator
8
+import statistics
9
+
10
+# TYPES {{{
11
+from typing import Any, Container, Sized
12
+
13
+# }}}
14
+# NUMBERS {{{
15
+abs = operator.abs
16
+
17
+neg = operator.neg
18
+pos = operator.pos
19
+
20
+add = operator.add
21
+sub = operator.sub
22
+
23
+mul = operator.mul
24
+matmul = operator.matmul
25
+
26
+mod = operator.mod
27
+div = fractions.Fraction
28
+
29
+truediv = operator.truediv
30
+floordiv = operator.floordiv
31
+
32
+
33
+def dec(x: int) -> int:
34
+    """Return x - 1.
35
+
36
+    >>> dec(-1)
37
+    -2
38
+    >>> dec(0)
39
+    -1
40
+    >>> dec(1)
41
+    0
42
+    """
43
+    return x - 1
44
+
45
+
46
+def inc(x: int) -> int:
47
+    """Return x + 1.
48
+
49
+    >>> inc(-1)
50
+    0
51
+    >>> inc(0)
52
+    1
53
+    >>> inc(1)
54
+    2
55
+    """
56
+    return x + 1
57
+
58
+
59
+def isneg(x: int) -> bool:
60
+    """Return True if x is negative.
61
+
62
+    >>> isneg(-1)
63
+    True
64
+    >>> isneg(0)
65
+    False
66
+    >>> isneg(1)
67
+    False
68
+    """
69
+    return x < 0
70
+
71
+
72
+def iszero(x: int) -> bool:
73
+    """Return True if x is zero.
74
+
75
+    >>> iszero(-1)
76
+    False
77
+    >>> iszero(0)
78
+    True
79
+    >>> iszero(1)
80
+    False
81
+    """
82
+    return x == 0
83
+
84
+
85
+def ispos(x: int) -> bool:
86
+    """Return True if x is positive.
87
+
88
+    >>> ispos(-1)
89
+    False
90
+    >>> ispos(0)
91
+    False
92
+    >>> ispos(1)
93
+    True
94
+    """
95
+    return x > 0
96
+
97
+
98
+def isodd(x: int) -> bool:
99
+    """Return True if x is odd.
100
+
101
+    >>> isodd(2)
102
+    False
103
+    >>> isodd(3)
104
+    True
105
+    """
106
+    return x % 2 == 1
107
+
108
+
109
+def iseven(x: int) -> bool:
110
+    """Return True if x is even.
111
+
112
+    >>> iseven(2)
113
+    True
114
+    >>> iseven(3)
115
+    False
116
+    """
117
+    return x % 2 == 0
118
+
119
+
120
+# }}}
121
+# OBJECTS {{{
122
+hasat = builtins.hasattr
123
+
124
+isa = builtins.isinstance
125
+issub = builtins.issubclass
126
+
127
+getat = operator.attrgetter
128
+getit = operator.itemgetter
129
+getme = operator.methodcaller
130
+
131
+
132
+def issome(x: Any) -> bool:
133
+    """Return True if x is not None.
134
+    >>> issome(0)
135
+    True
136
+    >>> issome(1)
137
+    True
138
+    >>> issome(None)
139
+    False
140
+    """
141
+    return x is not None
142
+
143
+
144
+def isnone(x: Any) -> bool:
145
+    """Return True if x is None.
146
+    >>> isnone(0)
147
+    False
148
+    >>> isnone(1)
149
+    False
150
+    >>> isnone(None)
151
+    True
152
+    """
153
+    return x is None
154
+
155
+
156
+# }}}
157
+# BOOLEANS {{{
158
+is_ = operator.is_
159
+not_ = operator.not_
160
+isnot = operator.is_not
161
+istrue = operator.truth
162
+
163
+or_ = operator.or_
164
+and_ = operator.and_
165
+
166
+
167
+def isfalse(x: Any) -> bool:
168
+    """Return True if x is false.
169
+
170
+    >>> isfalse(0)
171
+    True
172
+    >>> isfalse(1)
173
+    False
174
+    >>> isfalse(None)
175
+    True
176
+    >>> isfalse(True)
177
+    False
178
+    >>> isfalse(False)
179
+    True
180
+    """
181
+    return not istrue(x)
182
+
183
+
184
+# }}}
185
+# BITWISES {{{
186
+inv = operator.inv
187
+xor = operator.xor
188
+
189
+lshift = operator.lshift
190
+rshift = operator.rshift
191
+# }}}
192
+# SEQUENCES {{{
193
+isin = operator.contains
194
+
195
+count = operator.countOf
196
+index = operator.indexOf
197
+
198
+addseq = operator.concat
199
+
200
+
201
+def notin(l: Container, x: Any) -> bool:
202
+    """Return True if x is not in l.
203
+
204
+    >>> notin({1, 2, 3}, 0)
205
+    True
206
+    >>> notin({1, 2, 3}, 1)
207
+    False
208
+    """
209
+    return x not in l
210
+
211
+
212
+def isempty(l: Sized) -> bool:
213
+    """Return True if l is empty.
214
+
215
+    >>> isempty([])
216
+    True
217
+    >>> isempty([0])
218
+    False
219
+    """
220
+    return len(l) == 0
221
+
222
+
223
+def notempty(l: Sized) -> bool:
224
+    """Return True if l is not empty.
225
+
226
+    >>> notempty([])
227
+    False
228
+    >>> notempty([0])
229
+    True
230
+    """
231
+    return len(l) > 0
232
+
233
+
234
+# }}}
235
+# STATISTICS {{{
236
+mode = statistics.mode
237
+means = statistics.mean
238
+median = statistics.median
239
+
240
+stdev = statistics.stdev
241
+variance = statistics.variance
242
+# }}}
243
+# MATHEMATICS {{{
244
+e = math.e
245
+pi = math.pi
246
+tau = math.tau
247
+nan = math.nan
248
+inf = math.inf
249
+
250
+sqrt = math.sqrt
251
+pow = builtins.pow
252
+
253
+exp = math.exp
254
+log = math.log
255
+log2 = math.log2
256
+log10 = math.log10
257
+
258
+cos = math.cos
259
+sin = math.sin
260
+tan = math.tan
261
+hypot = math.hypot
262
+
263
+gcd = math.gcd
264
+ceil = math.ceil
265
+floor = math.floor
266
+round = builtins.round
267
+
268
+degrees = math.degrees
269
+radians = math.radians
270
+
271
+isinf = math.isinf
272
+isnan = math.isnan
273
+isclose = math.isclose
274
+isfinite = math.isfinite
275
+
276
+factorial = math.factorial
277
+# }}}
278
+# COMPARATORS {{{
279
+lt = operator.lt
280
+le = operator.le
281
+eq = operator.eq
282
+ne = operator.ne
283
+ge = operator.ge
284
+gt = operator.gt
285
+# }}}

+ 60
- 0
funpy/pp.py View File

@@ -0,0 +1,60 @@
1
+"""Parallel library"""
2
+
3
+from concurrent import futures
4
+from typing import Callable, Iterable, Type
5
+
6
+from funpy import it
7
+
8
+# TYPES {{{
9
+
10
+Pool = Type[futures.Executor]
11
+
12
+ThreadPool = futures.ThreadPoolExecutor
13
+
14
+ProcessPool = futures.ProcessPoolExecutor
15
+# }}}
16
+# PARALLELS {{{
17
+def pmap(
18
+    f: Callable,
19
+    *ls: Iterable,
20
+    workers: int = None,
21
+    timeout: int = None,
22
+    chunksize: int = 1,
23
+    pool: Pool = ProcessPool,
24
+) -> Iterable:
25
+    """Parallel implementation of map (ordered).
26
+
27
+    >>> list(pmap(pow, range(1, 5), range(1, 6), pool=ThreadPool))
28
+    [1, 4, 27, 256]
29
+    """
30
+    with pool(max_workers=workers) as p:  # type: ignore
31
+        yield from p.map(f, *ls, timeout=timeout, chunksize=chunksize)
32
+
33
+
34
+def mapreduce(
35
+    mapper: Callable,
36
+    reducer: Callable,
37
+    *ls: Iterable,
38
+    workers: int = None,
39
+    timeout: int = None,
40
+    chunksize: int = 1,
41
+    pool: Pool = ProcessPool,
42
+) -> Iterable:
43
+    """Parallel implement of map reduce.
44
+
45
+    >>> def mapper(x): return x % 2, x
46
+    >>> def reducer(xs): return sum(xs)
47
+    >>> list(mapreduce(mapper, reducer, range(5), pool=ThreadPool))
48
+    [(0, 6), (1, 4)]
49
+    """
50
+    with pool(max_workers=workers) as p:  # type: ignore
51
+        mapped = p.map(mapper, *ls, timeout=timeout, chunksize=chunksize)
52
+
53
+        grouped = it.groupkv(mapped)
54
+
55
+        reduced = ((k, p.submit(reducer, v)) for k, v in grouped)
56
+
57
+        yield from ((k, v.result(timeout=timeout)) for k, v in reduced)
58
+
59
+
60
+# }}}

+ 6
- 0
hooks/Makefile View File

@@ -0,0 +1,6 @@
1
+init-hooks:
2
+	git config --global core.hooksPath hooks/
3
+
4
+clean-hooks: ;
5
+
6
+commit-hooks: ;

+ 3
- 0
hooks/pre-commit View File

@@ -0,0 +1,3 @@
1
+#!/bin/sh
2
+
3
+make commit

+ 14
- 0
lints/Makefile View File

@@ -0,0 +1,14 @@
1
+.PHONY: lint
2
+
3
+NAME = $(shell .venv/bin/python setup.py --name)
4
+
5
+init-lints: .venv
6
+	.venv/bin/pip install '.[lints]'
7
+
8
+clean-lints:
9
+	find . -name '.mypy_cache' -exec rm -fr {} +
10
+
11
+commit-lints: lint;
12
+
13
+lint: .venv
14
+	.venv/bin/pylint --rcfile='lints/pylint.ini' ${NAME}

+ 6
- 0
lints/pylint.ini View File

@@ -0,0 +1,6 @@
1
+[MESSAGES CONTROL]
2
+disable = invalid-name,
3
+          broad-except,
4
+          bad-continuation,
5
+          redefined-builtin,
6
+          wrong-import-order

+ 1
- 0
lints/requirements.txt View File

@@ -0,0 +1 @@
1
+pylint

+ 16
- 0
packages/Makefile View File

@@ -0,0 +1,16 @@
1
+.PHONY: package upload
2
+
3
+init-packages: .venv
4
+	.venv/bin/pip install '.[packages]'
5
+
6
+clean-packages:
7
+	rm -rf build/lib/
8
+	rm -rf dist/*.whl
9
+
10
+commit-packages: package;
11
+
12
+package: .venv clean-packages
13
+	.venv/bin/python setup.py bdist_wheel --universal
14
+
15
+upload: package
16
+	.venv/bin/twine upload -r pypi dist/*.whl

+ 2
- 0
packages/requirements.txt View File

@@ -0,0 +1,2 @@
1
+twine
2
+wheel

+ 0
- 0
requirements.txt View File


+ 43
- 0
setup.py View File

@@ -0,0 +1,43 @@
1
+#!/usr/bin/env python
2
+
3
+import os
4
+import glob
5
+import setuptools  # type: ignore
6
+
7
+root = os.path.abspath(os.path.dirname(__file__))
8
+
9
+
10
+def requires(requirements="requirements.txt"):
11
+    path = os.path.join(root, requirements)
12
+
13
+    with open(path, "r") as f:
14
+        return f.read().splitlines()
15
+
16
+
17
+info = dict(
18
+    name="funpy",
19
+    version="0.5.5",
20
+    license="EUPL-1.2",
21
+    author="Médéric Hurier",
22
+    author_email="dev@fmind.me",
23
+    description="Functional and Pythonic stdlib.",
24
+    long_description_content_type="text/markdown",
25
+    long_description=open("README.md", "r").read(),
26
+    url="https://git.fmind.me/fmind/funpy",
27
+    packages=["funpy"],
28
+    keywords="operator function iterator parallel io",
29
+    classifiers=[
30
+        "Topic :: Software Development",
31
+        "Intended Audience :: Developers",
32
+        "Development Status :: 3 - Alpha",
33
+        "Programming Language :: Python :: 3",
34
+    ],
35
+    extras_require={
36
+        os.path.dirname(f): requires(f) for f in glob.glob("*/requirements.txt")
37
+    },
38
+    python_requires=">=3",
39
+    install_requires=requires(),
40
+)
41
+
42
+if __name__ == "__main__":
43
+    setuptools.setup(**info)

+ 9
- 0
shells/Makefile View File

@@ -0,0 +1,9 @@
1
+init-shells: .venv
2
+	.venv/bin/pip install '.[shells]'
3
+
4
+clean-shells: ;
5
+
6
+commit-shells: ;
7
+
8
+shell: .venv
9
+	.venv/bin/ipython --config='shells/ipython.py'

+ 16
- 0
shells/ipython.py View File

@@ -0,0 +1,16 @@
1
+# Configuration file for ipython.
2
+
3
+from traitlets.config import get_config
4
+
5
+c = get_config()
6
+
7
+c.TerminalIPythonApp.force_interact = True
8
+
9
+c.TerminalInteractiveShell.colors = "Linux"
10
+c.TerminalInteractiveShell.editing_mode = "vi"
11
+c.TerminalInteractiveShell.confirm_exit = False
12
+
13
+c.InteractiveShellApp.extensions = ["autoreload"]
14
+c.InteractiveShellApp.exec_lines = ["%autoreload 2"]
15
+
16
+c.TerminalInteractiveShell.extra_open_editor_shortcuts = True

+ 2
- 0
shells/requirements.txt View File

@@ -0,0 +1,2 @@
1
+ipdb
2
+ipython

+ 12
- 0
tests/Makefile View File

@@ -0,0 +1,12 @@
1
+.PHONY: test
2
+
3
+init-tests: .venv
4
+	.venv/bin/pip install '.[tests]'
5
+
6
+clean-tests:
7
+	find . -name '.pytest_cache' -exec rm -fr {} +
8
+
9
+commit-tests: test;
10
+
11
+test: .venv
12
+	.venv/bin/pytest -c tests/pytest.ini

+ 0
- 0
tests/conftest.py View File


+ 2
- 0
tests/pytest.ini View File

@@ -0,0 +1,2 @@
1
+[pytest]
2
+addopts = --doctest-modules

+ 1
- 0
tests/requirements.txt View File

@@ -0,0 +1 @@
1
+pytest

+ 12
- 0
tests/test_example.py View File

@@ -0,0 +1,12 @@
1
+#!/usr/bin/env pytest
2
+
3
+import pytest
4
+
5
+
6
+def test_success():
7
+    assert 1 + 1 == 2
8
+
9
+
10
+def test_error():
11
+    with pytest.raises(ZeroDivisionError):
12
+        assert 1 / 0

+ 12
- 0
types/Makefile View File

@@ -0,0 +1,12 @@
1
+.PHONY: type
2
+
3
+init-types: .venv
4
+	.venv/bin/pip install '.[types]'
5
+
6
+clean-types:
7
+	find . -name '.mypy_cache' -exec rm -fr {} +
8
+
9
+commit-types: type;
10
+
11
+type: .venv
12
+	.venv/bin/mypy . --config-file=types/mypy.ini

+ 2
- 0
types/mypy.ini View File

@@ -0,0 +1,2 @@
1
+[mypy]
2
+ignore_missing_imports = True

+ 1
- 0
types/requirements.txt View File

@@ -0,0 +1 @@
1
+mypy