├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── .readthedocs.yaml
├── CHANGELOG
├── LICENSE
├── MANIFEST.in
├── README.rst
├── TODO.rst
├── VERSION
├── docs
├── Makefile
├── _static
│ ├── nhpup_1.1.js
│ └── overrides.css
├── calc.rst
├── cheatsheet.rst
├── colls.rst
├── conf.py
├── debug.rst
├── decorators.rst
├── descriptions.html
├── extended_fns.rst
├── flow.rst
├── funcs.rst
├── index.rst
├── objects.rst
├── overview.rst
├── primitives.rst
├── requirements.txt
├── seqs.rst
├── strings.rst
└── types.rst
├── funcy
├── __init__.py
├── _inspect.py
├── calc.py
├── colls.py
├── debug.py
├── decorators.py
├── flow.py
├── funcmakers.py
├── funcolls.py
├── funcs.py
├── objects.py
├── primitives.py
├── seqs.py
├── strings.py
├── tree.py
└── types.py
├── publish.sh
├── setup.cfg
├── setup.py
├── test_requirements.txt
├── tests
├── __init__.py
├── py38_decorators.py
├── py38_funcs.py
├── test_calc.py
├── test_colls.py
├── test_debug.py
├── test_decorators.py
├── test_decorators.py.orig
├── test_flow.py
├── test_funcmakers.py
├── test_funcolls.py
├── test_funcs.py
├── test_interface.py
├── test_objects.py
├── test_seqs.py
├── test_strings.py
├── test_tree.py
└── test_types.py
└── tox.ini
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 |
9 | jobs:
10 | lint:
11 | runs-on: ubuntu-22.04
12 | steps:
13 | - uses: actions/checkout@v2
14 | - name: Set up Python 3.11
15 | uses: actions/setup-python@v4
16 | with:
17 | python-version: 3.11
18 | - name: Lint
19 | run: |
20 | pip install flake8
21 | flake8 funcy
22 | flake8 --select=F,E5,W tests
23 |
24 | docs:
25 | runs-on: ubuntu-22.04
26 | steps:
27 | - uses: actions/checkout@v2
28 | - name: Set up Python 3.11
29 | uses: actions/setup-python@v4
30 | with:
31 | python-version: 3.11
32 | - name: Build docs
33 | working-directory: ./docs
34 | run: |
35 | pip install -r requirements.txt
36 | make html SPHINXOPTS="-W"
37 |
38 | test:
39 | strategy:
40 | fail-fast: false
41 | matrix:
42 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.10"]
43 | include:
44 | - {os: "ubuntu-22.04"}
45 | - {os: "ubuntu-20.04", python-version: "3.6"}
46 | - {os: "ubuntu-20.04", python-version: "3.5"}
47 | # Doesn't really work
48 | # - {os: "ubuntu-18.04", python-version: "3.4"}
49 | runs-on: ${{ matrix.os }}
50 | steps:
51 | - uses: actions/checkout@v2
52 | - name: Set up Python ${{ matrix.python-version }}
53 | uses: actions/setup-python@v4
54 | with:
55 | python-version: ${{ matrix.python-version }}
56 | - name: Install dependencies
57 | run: |
58 | python -m pip install --upgrade pip
59 | pip install -r test_requirements.txt
60 | - name: Run tests
61 | run: pytest -W error
62 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | dist
3 | *.egg-info
4 | build
5 | docs/_build
6 | .tags*
7 | .tox
8 | .coverage
9 | htmlcov
10 | .cache
11 | .pytest_cache
12 |
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | build:
4 | os: "ubuntu-20.04"
5 | tools:
6 | python: "3.11"
7 |
8 | sphinx:
9 | configuration: docs/conf.py
10 |
11 | python:
12 | install:
13 | - requirements: docs/requirements.txt
14 |
--------------------------------------------------------------------------------
/CHANGELOG:
--------------------------------------------------------------------------------
1 | 2.0
2 | - support Python 3.11 officially
3 | - added get_lax()
4 | - added lzip(strict) param
5 | - made autocurry() and friends support kw-only and pos-only arguments
6 | - improved call._whatever_ arg introspection: pos-only, kw-only, kwargs and varargs are supported
7 | Backwards incompatible changes:
8 | - dropped Python 2 support
9 | - dropped namespace helper class
10 | - dropped old SkipMemoization alias for SkipMemory exception
11 | - @cache(key_func) param is now keyword only
12 | - @decorator's call won't access args capturesd by **kwargs individually anymore
13 |
14 | 1.18
15 | - added join_with(strict=)
16 | - use more precise timer `timeit.default_timer` for log*durations (Saugat Pachhai)
17 | - preserve metadata when using autocurry as a decorator (#117) (Kale Kundert)
18 | - doc improvements (thx to Tim Gates)
19 |
20 | 1.17
21 | - support Python 3.10 officially
22 | - added del_in()
23 | - made throttle() and limit_error_rate() work on methods
24 | - added str and repr to Call objects
25 | - migrated CI to Github actions (thx to Bruno Alla)
26 | - fixed doc[string] for zip_dicts (Tal Einat)
27 | - fixed some inspect issues
28 | - minor doc fixes
29 |
30 | 1.16
31 | - support Python 3.9 officially
32 | - unify @memoize() and @cache(): both have .skip/.memory/.invalidate/.invalidate_all now
33 | - support dynamic resulting exception in @reraise() (Laurens Duijvesteijn)
34 | - made () optional for @decorator-made decorators with kw-only args
35 | - added @throttle()
36 | - added has_path() (Denys Zorinets)
37 | - fixed autocurry kwargs handling
38 |
39 | 1.15
40 | - made rpartial accept keyworded arguments (Ruan Comelli)
41 | - made `@cache.invalidate()` idempotent (Dmitry Vasilyanov)
42 | - made raiser() accept a string as a shortcut
43 | - fixed cheatsheat description for 'distinct' helper (tsouvarev)
44 | - fixed some seqs docstrings
45 | - fixed some typos (Tim Gates)
46 |
47 | 1.14
48 | - stated Python 3.7 and 3.8 support
49 | - dropped Python 2.6
50 | - added @wrap_prop()
51 | - added filter_errors param to @retry()
52 | - published nullcontext properly
53 |
54 | 1.13
55 | - added @wrap_with()
56 | - added nullcontext
57 |
58 | 1.12
59 | - added @cached_readonly
60 | - more introspection in @decorator decorators
61 | - documented @cached_property inheritance limitations
62 | - included tests in pypi sdist tarball (Tomáš Chvátal)
63 |
64 | 1.11
65 | - switched docs and internals to Python 3
66 | - improved docs: better texts and examples here and there
67 | - support Python 3.7 officially
68 | - added popups over functions everywhere in docs
69 | - accept any iterables of errors in flow utils
70 | - fixed walk_values() for defaultdicts with empty factory
71 | - fixed xmap() signature introspection
72 | - documented lzip()
73 |
74 | 1.10.3
75 | - added repr_len param to various debug utils
76 | - dropped testing in Python 3.3
77 |
78 | 1.10.2
79 | - support extended function semantics in iffy (Eric Prykhodko)
80 | - distribute as a universal wheel.
81 |
82 | 1.10.1
83 | - use raise from in reraise()
84 | - fix @cache with mixed positional and keywords args (thx to adrian-dankiv)
85 |
86 | 1.10
87 | - added @reraise()
88 | - added unit and threshold params to *_durations() utils
89 | - published and documented LazyObject
90 | - fixed iffy() default argument when action is not present (Dmytro Kabakchei)
91 |
92 | 1.9.1
93 | - make where() skip nonexistent keys (Aleksei Voronov)
94 | - fixed r?curry() on funcy i?map(), i?mapcat() and merge_with()
95 |
96 | 1.9
97 | - filled in docstrings and some names
98 | - better currying:
99 | - all *curry() now work with builtins and classes
100 | - autocurry() is robust in deciding when to call
101 | - deprecated autocurry() n arg
102 | - @memoize now exposes its memory and accepts key_func arg
103 | - @cache also accepts key_func and supports funcs with **kwargs
104 | - added omit() (Petr Melnikov)
105 | - fixed/hacked PyCharm import introspection
106 | - optimized i?reductions() in Python 3
107 | - backported accumulate() to Python 2
108 |
109 | 1.8
110 | - added count_reps()
111 | - published namespace class
112 | - added LazyObject (simplistic, experimental and not documented)
113 | - support class dicts in walk*(), select*(), compact(), project() and empty()
114 | - support Python 3 dict.keys(), .values() and .items() in walk*() and friends
115 | - fixed empty() on iterators
116 | - optimized chunking range() in Python 3
117 |
118 | 1.7.5
119 | - fixed defaults in double @decorated function
120 | - fixed @decorator with more than one default
121 |
122 | 1.7.4
123 | - better error message on call.missed_arg access
124 | - optimized call.arg access in @decorator
125 |
126 | 1.7.3
127 | - support Python 3.6 officially
128 | - fix deprecation warnings in Python 3.5 and 3.6
129 |
130 | 1.7.2
131 | - added cheatsheet
132 | - many fixes in docs
133 | - documented @post_processing()
134 | - fixed (print|log)_* on non-function callables
135 |
136 | 1.7.1
137 | - fixed 3+ argument map() in Python 3
138 |
139 | 1.7
140 | - support Python 3.5 officially
141 | - added group_values()
142 | - fixed i?partition_by() for non-boolean extended mapper
143 | - cleanups and optimizations in colls and seqs
144 |
145 | 1.6
146 | - added i?tree_nodes()
147 | - added (log|print)_iter_durations() to debug utils
148 | - added lists support to get_in(), set_in() and update_in()
149 | - single argument takewhile() and dropwhile()
150 | - published iwhere(), ipluck(), ipluck_attr() and iinvoke()
151 | - support @retry() with list (not tuple) of errors (Zakhar Zibarov)
152 | - changed µs to mks in time messages
153 | - optimized update_in()
154 |
155 | 1.5
156 | - added rcompose()
157 | - added i?tree_leaves()
158 | - added pluck_attr() (Marcus McCurdy)
159 | - added set_in() and update_in()
160 | - added get_in() (Swaroop)
161 | - fixed bug with flatten() follow not passed deep
162 |
163 | 1.4
164 | - added rpartial() and rcurry()
165 | - support arguments in print_(calls|exits)
166 | - made print_(errors|durations) work both with and without arguments
167 | - made (log|print)_errors() work as context manager
168 | - made (log|print)_durations() work as context managers
169 | - pass func docstring to @cached_property
170 |
171 | 1.3
172 | - added with_next()
173 | - added timeout argument to @retry() (rocco66)
174 | - support kwargs in @memoize'd functions (Lukasz Dobrzanski)
175 | - do not cut result repr in @(log|print)_calls() and @(log|print)_exits
176 |
177 | 1.2
178 | - support pypy3
179 | - added @contextmanager, ContextDecorator
180 | - added @(log|print)_(enters|exits)
181 | - print stack trace in @(log|print)_(calls|errors)
182 | - added label argument for tap()
183 | - better formatted call signatures in debug utilities
184 | - added itervalues()
185 | - exposed empty(), iteritems()
186 | - exposed @wraps and unwrap()
187 | - slightly optimized last() and nth()
188 | - fixed signatures of functions wrapped with @wrap_(mapper|selector)
189 |
190 | 1.1
191 | - added merge_with() and join_with()
192 | - added @once, @once_per_args and @once_per()
193 | - added suppress() context manager
194 | - added is_set()
195 | - added name argument to @monkey
196 | - decorators created with @decorator now set __original__ attribute
197 | - optimized @decorator
198 | - optimized nth() and last()
199 | - lzip() is now exported by default from/for py3
200 | Backward incompatible fixes:
201 | - made pluck(), where() and invoke() return interators in python 3
202 | - __wrapped__ attribute added by decorators now correctly refers to immediate wrapped not innermost
203 |
204 | 1.0.0
205 | - @silent, @ignore() and decorators created with @decorator will now work
206 | with method_descriptors and other non-wrappable callables.
207 | - chained decorators now have access to arguments by name
208 | - exposed cut_prefix() and cut_suffix()
209 | - optimized re_tester()
210 | - fixed @retry in python 3
211 | Backward incompatible changes:
212 | - function made from dict will now use __getitem__ instead of get.
213 | Means possible KeyErrors for dicts and factory function calls for defaultdict.
214 | Use `a_dict.get` instead of just `a_dict` for old behaviour.
215 | - reverted imap(None, seq) to default strange behaviour.
216 |
217 | 0.10.1
218 | - optimized @decorator
219 |
220 | 0.10
221 | - added is_tuple()
222 | - raiser() can now be called without arguments, defaults to Exception
223 | - support del @cached_property
224 | - optimized and cleaned up @cached_property
225 | - optimized i?split(), split_at() and split_by()
226 | - optimized @memoize
227 | - optimized zipdict()
228 | Backward incompatible changes:
229 | - split(), split_at() and split_by() now return a tuple of two lists instead of list of them
230 | - @cached_property no longer uses _name to store cached value
231 | - partial() is now an alias to functools.partial, use func_partial() for old behaviour
232 |
233 | 0.9
234 | - added experimental python 3 support
235 | - added python 2.6 support
236 | - added autocurry()
237 | - published idistinct(), isplit(), isplit_at(), isplit_by()
238 | - some optimizations
239 |
240 | 0.8
241 | - added raiser()
242 | - added idistinct()
243 | - added key argument to i?distinct()
244 | - added key argument to is_distinct()
245 | - added group_by_keys()
246 | Backward incompatible changes:
247 | - walk_values() now updates defaultdict item factory to composition of mapper and old one
248 | - izip_dicts() now packs values in tuple separate from key
249 | - @decorator raises AttributeError not NameError when non-existent argument is accessed by name
250 |
251 | 0.7
252 | - added i?flatten()
253 | - added pairwise()
254 | - added nth()
255 | - added is_seqcont()
256 | - greatly optimized @decorator
257 | - added @log_durations and @print_durations
258 | - @logs_calls and @print_calls now provide call signature on return
259 | - @logs_calls and @print_calls now log errors, optional for @log_calls
260 | - better call signature stringification for @(log|print)_(calls|errors)
261 | - fixed i?partition() and i?chunks() with xrange()
262 | Backward incompatible changes:
263 | - is_iter() now returns False given xrange() object
264 |
265 | 0.6.0
266 | - added izip_values() and izip_dicts()
267 | - added last() and butlast()
268 | - added isnone() and notnone() primitives
269 | - added extended fn semantics to group_by(), count_by() and i?partition_by()
270 | - added fill argument to with_prev()
271 | - optimized ilen()
272 |
273 | 0.5.6
274 | - fixed installation issue
275 |
276 | 0.5.5
277 | - added count_by()
278 | - added i?partition_by()
279 |
280 | 0.5.4
281 | - added @post_processing() flow utility
282 | - partition() and chunks() can handle iterators now
283 | - added ipartition() and ichunks()
284 |
285 | 0.5.3
286 | - fixed decorators produced with @decorator over non-functions
287 | - optimized @ignore and @silent
288 |
289 | 0.5.2
290 | - added i?without()
291 | - more and better docs
292 | Backward incompatible changes:
293 | - compact() now strips all falsy values not just None
294 |
295 | 0.5.1
296 | - added ints and slices to extended fn semantics
297 | - added extended semantics to *_fn(), compose(), complement and i?juxt()
298 | - can now @monkey() patch modules
299 | - cached properties can now be set
300 |
301 | 0.5.0
302 | - added type testing utilities
303 | - added @monkey
304 | - added cut_prefix() and cut_suffix() privately
305 | - added @silent_lookuper
306 | - exported @retry directly from from funcy
307 | - better support for arg introspection in @decorator
308 | Backward incompatible changes:
309 | - removed defaults for log_calls() and log_errors()
310 | - @make_lookuper decorated functions now will raise LookupError on memory miss,
311 | use @silent_lookuper for old behavior
312 | - call object in @decorator access to func, args and kwargs
313 | is now done through _func, _args and _kwargs
314 |
315 | 0.4.1
316 | - decorators created with @decorator are now able to pass additional args and kwargs
317 | - @collecting, @joining() and @limit_error_rate() now exported directly from funcy
318 | - @tap(), @log_calls and @log_errors() now exported directly from funcy
319 | - added @print_calls and @print_errors
320 | - better handling passing None to optional parameter
321 | - docs for debugging utilities
322 | Backward incompatible changes:
323 | - @log renamed to @log_calls
324 |
325 | 0.4.0
326 | - extended predicate/mapping semantics for seq and coll utils
327 | - added str_join()
328 | - added @collecting and @joining()
329 | - added sums() and isums()
330 | - better docs
331 |
332 | 0.3.4
333 | - added with_prev()
334 | - added iterable()
335 | - support iterators in walk*(), select*(), empty() and project()
336 | - reexport itertools.chain()
337 | - faster curry
338 | - more docs
339 |
340 | 0.3.3
341 | - added compact(), i?reductions()
342 | - added default argument to @ignore()
343 | - added tap() experimental debug utility
344 | - @make_lookuper() now works on functions with arguments
345 | - exposed ilen() publicly
346 | - added default argument to @ignore()
347 | - fix: join() and merge() now correctly fail when receive [None, ...]
348 | - better docs
349 | Backward incompatible changes:
350 | - renamed @memoize.lookup() to @make_lookuper()
351 |
352 | 0.3.2
353 | - added ilen()
354 | - added some object helpers: namespace base class and @cached_property
355 | - more docs
356 |
357 | 0.3.1
358 | - added @memoize.lookup()
359 | - more and better docs
360 | Backward incompatible changes:
361 | - removed generator based @decorator version
362 | - pluck() now accepts key as first parameter
363 |
364 | 0.3.0
365 | - partial docs
366 | - added where(), pluck() and invoke() inspired by underscore
367 | - added split_by()
368 | - second() made public
369 | - reexport itertools.cycle()
370 | - walk() and select() work with strings now
371 | Backward incompatible changes:
372 | - renamed groupby() to group_by()
373 | - separated split_at() from split()
374 | - automatically unpack one-element tuples returned from re_*()
375 | - join() now returns None on empty input instead of TypeError
376 | - made fallback() accept multiple arguments
377 | Bugfixes:
378 | - fixed join() swallowing first coll from iterator of colls
379 |
380 | 0.2.1
381 | - one argument keep()
382 | - fallback() flow
383 |
384 | 0.2
385 | - added curry() to funcs
386 | - added re_test(), re_tester() and re_finder() to strings
387 | - added second() to seqs
388 | - added one() and one_fn() to colls and funcolls
389 | - support defaultdicts in walk*(), select*(), project(), empty()
390 | - one argument and uncallable default in iffy()
391 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012-2020, Alexander Schepanovski.
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification,
5 | are permitted provided that the following conditions are met:
6 |
7 | 1. Redistributions of source code must retain the above copyright notice,
8 | this list of conditions and the following disclaimer.
9 |
10 | 2. Redistributions in binary form must reproduce the above copyright
11 | notice, this list of conditions and the following disclaimer in the
12 | documentation and/or other materials provided with the distribution.
13 |
14 | 3. Neither the name of funcy nor the names of its contributors may
15 | be used to endorse or promote products derived from this software
16 | without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE
2 | include CHANGELOG
3 | include README.rst
4 | include VERSION
5 | recursive-include tests *
6 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Funcy |Build Status|
2 | =====
3 |
4 | A collection of fancy functional tools focused on practicality.
5 |
6 | Inspired by clojure, underscore and my own abstractions. Keep reading to get an overview
7 | or `read the docs `_.
8 | Or jump directly to `cheatsheet `_.
9 |
10 | Works with Python 3.4+ and pypy3.
11 |
12 |
13 | Installation
14 | -------------
15 |
16 | ::
17 |
18 | pip install funcy
19 |
20 |
21 | Overview
22 | --------------
23 |
24 | Import stuff from funcy to make things happen:
25 |
26 | .. code:: python
27 |
28 | from funcy import whatever, you, need
29 |
30 |
31 | Merge collections of same type
32 | (works for dicts, sets, lists, tuples, iterators and even strings):
33 |
34 | .. code:: python
35 |
36 | merge(coll1, coll2, coll3, ...)
37 | join(colls)
38 | merge_with(sum, dict1, dict2, ...)
39 |
40 |
41 | Walk through collection, creating its transform (like map but preserves type):
42 |
43 | .. code:: python
44 |
45 | walk(str.upper, {'a', 'b'}) # {'A', 'B'}
46 | walk(reversed, {'a': 1, 'b': 2}) # {1: 'a', 2: 'b'}
47 | walk_keys(double, {'a': 1, 'b': 2}) # {'aa': 1, 'bb': 2}
48 | walk_values(inc, {'a': 1, 'b': 2}) # {'a': 2, 'b': 3}
49 |
50 |
51 | Select a part of collection:
52 |
53 | .. code:: python
54 |
55 | select(even, {1,2,3,10,20}) # {2,10,20}
56 | select(r'^a', ('a','b','ab','ba')) # ('a','ab')
57 | select_keys(callable, {str: '', None: None}) # {str: ''}
58 | compact({2, None, 1, 0}) # {1,2}
59 |
60 |
61 | Manipulate sequences:
62 |
63 | .. code:: python
64 |
65 | take(4, iterate(double, 1)) # [1, 2, 4, 8]
66 | first(drop(3, count(10))) # 13
67 |
68 | lremove(even, [1, 2, 3]) # [1, 3]
69 | lconcat([1, 2], [5, 6]) # [1, 2, 5, 6]
70 | lcat(map(range, range(4))) # [0, 0, 1, 0, 1, 2]
71 | lmapcat(range, range(4)) # same
72 | flatten(nested_structure) # flat iter
73 | distinct('abacbdd') # iter('abcd')
74 |
75 | lsplit(odd, range(5)) # ([1, 3], [0, 2, 4])
76 | lsplit_at(2, range(5)) # ([0, 1], [2, 3, 4])
77 | group_by(mod3, range(5)) # {0: [0, 3], 1: [1, 4], 2: [2]}
78 |
79 | lpartition(2, range(5)) # [[0, 1], [2, 3]]
80 | chunks(2, range(5)) # iter: [0, 1], [2, 3], [4]
81 | pairwise(range(5)) # iter: [0, 1], [1, 2], ...
82 |
83 |
84 | And functions:
85 |
86 | .. code:: python
87 |
88 | partial(add, 1) # inc
89 | curry(add)(1)(2) # 3
90 | compose(inc, double)(10) # 21
91 | complement(even) # odd
92 | all_fn(isa(int), even) # is_even_int
93 |
94 | one_third = rpartial(operator.div, 3.0)
95 | has_suffix = rcurry(str.endswith, 2)
96 |
97 |
98 | Create decorators easily:
99 |
100 | .. code:: python
101 |
102 | @decorator
103 | def log(call):
104 | print(call._func.__name__, call._args)
105 | return call()
106 |
107 |
108 | Abstract control flow:
109 |
110 | .. code:: python
111 |
112 | walk_values(silent(int), {'a': '1', 'b': 'no'})
113 | # => {'a': 1, 'b': None}
114 |
115 | @once
116 | def initialize():
117 | "..."
118 |
119 | with suppress(OSError):
120 | os.remove('some.file')
121 |
122 | @ignore(ErrorRateExceeded)
123 | @limit_error_rate(fails=5, timeout=60)
124 | @retry(tries=2, errors=(HttpError, ServiceDown))
125 | def some_unreliable_action(...):
126 | "..."
127 |
128 | class MyUser(AbstractBaseUser):
129 | @cached_property
130 | def public_phones(self):
131 | return self.phones.filter(public=True)
132 |
133 |
134 | Ease debugging:
135 |
136 | .. code:: python
137 |
138 | squares = {tap(x, 'x'): tap(x * x, 'x^2') for x in [3, 4]}
139 | # x: 3
140 | # x^2: 9
141 | # ...
142 |
143 | @print_exits
144 | def some_func(...):
145 | "..."
146 |
147 | @log_calls(log.info, errors=False)
148 | @log_errors(log.exception)
149 | def some_suspicious_function(...):
150 | "..."
151 |
152 | with print_durations('Creating models'):
153 | Model.objects.create(...)
154 | # ...
155 | # 10.2 ms in Creating models
156 |
157 |
158 | And `much more `_.
159 |
160 |
161 | Dive in
162 | -------
163 |
164 | Funcy is an embodiment of ideas I explain in several essays:
165 |
166 | - `Why Every Language Needs Its Underscore `_
167 | - `Functional Python Made Easy `_
168 | - `Abstracting Control Flow `_
169 | - `Painless Decorators `_
170 |
171 | Related Projects
172 | ----------------
173 |
174 | - https://pypi.org/project/funcy-chain/
175 | - https://pypi.org/project/funcy-pipe/
176 |
177 | Running tests
178 | --------------
179 |
180 | To run the tests using your default python:
181 |
182 | ::
183 |
184 | pip install -r test_requirements.txt
185 | py.test
186 |
187 | To fully run ``tox`` you need all the supported pythons to be installed. These are
188 | 3.4+ and PyPy3. You can run it for particular environment even in absense
189 | of all of the above::
190 |
191 | tox -e py310
192 | tox -e pypy3
193 | tox -e lint
194 |
195 |
196 | .. |Build Status| image:: https://github.com/Suor/funcy/actions/workflows/test.yml/badge.svg
197 | :target: https://github.com/Suor/funcy/actions/workflows/test.yml?query=branch%3Amaster
198 |
--------------------------------------------------------------------------------
/TODO.rst:
--------------------------------------------------------------------------------
1 | TODO
2 | ====
3 |
4 | - public xfunc/xfn, xpred
5 | - where_not?
6 | - invalidate/invalidate_all() to (make|silent)_lookuper?
7 | - decorators with optional arguments?
8 |
9 | Or not TODO
10 | -----------
11 |
12 | - pre_walk, post_walk
13 | - tree-seq
14 | - (log|print)_errors to optionally hide causing call
15 | - log_* and print_* to optionally hide args
16 | - padding to chunks
17 | - partial.func interface or (func, arg1, arg2) extended fns
18 | - reject*(), disjoint*() collections
19 | - zip_with = map(f, zip(seqs))
20 | - starfilter()
21 | - one argument select*()? other name?
22 | - reversed() to work with iterators
23 | - vector chained boolean test (like perl 6 [<])
24 |
25 |
26 | Unknown future
27 | --------------
28 |
29 | - cython implementation? separate - cyfuncy? fallback transparently?
30 | - funcyx?
31 |
--------------------------------------------------------------------------------
/VERSION:
--------------------------------------------------------------------------------
1 | 2.0
2 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = _build
9 |
10 | # Internal variables.
11 | PAPEROPT_a4 = -D latex_paper_size=a4
12 | PAPEROPT_letter = -D latex_paper_size=letter
13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
14 | # the i18n builder cannot share the environment and doctrees with the others
15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
16 |
17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
18 |
19 | help:
20 | @echo "Please use \`make ' where is one of"
21 | @echo " html to make standalone HTML files"
22 | @echo " dirhtml to make HTML files named index.html in directories"
23 | @echo " singlehtml to make a single large HTML file"
24 | @echo " pickle to make pickle files"
25 | @echo " json to make JSON files"
26 | @echo " htmlhelp to make HTML files and a HTML help project"
27 | @echo " qthelp to make HTML files and a qthelp project"
28 | @echo " devhelp to make HTML files and a Devhelp project"
29 | @echo " epub to make an epub"
30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
31 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
32 | @echo " text to make text files"
33 | @echo " man to make manual pages"
34 | @echo " texinfo to make Texinfo files"
35 | @echo " info to make Texinfo files and run them through makeinfo"
36 | @echo " gettext to make PO message catalogs"
37 | @echo " changes to make an overview of all changed/added/deprecated items"
38 | @echo " linkcheck to check all external links for integrity"
39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
40 |
41 | clean:
42 | -rm -rf $(BUILDDIR)/*
43 |
44 | html:
45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
46 | @echo
47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
48 |
49 | coverage:
50 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
51 | @echo
52 | @echo "Build finished. The coverage pages are in $(BUILDDIR)/coverage."
53 |
54 | dirhtml:
55 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
56 | @echo
57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
58 |
59 | singlehtml:
60 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
61 | @echo
62 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
63 |
64 | pickle:
65 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
66 | @echo
67 | @echo "Build finished; now you can process the pickle files."
68 |
69 | json:
70 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
71 | @echo
72 | @echo "Build finished; now you can process the JSON files."
73 |
74 | htmlhelp:
75 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
76 | @echo
77 | @echo "Build finished; now you can run HTML Help Workshop with the" \
78 | ".hhp project file in $(BUILDDIR)/htmlhelp."
79 |
80 | qthelp:
81 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
82 | @echo
83 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
84 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
85 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/funcy.qhcp"
86 | @echo "To view the help file:"
87 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/funcy.qhc"
88 |
89 | devhelp:
90 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
91 | @echo
92 | @echo "Build finished."
93 | @echo "To view the help file:"
94 | @echo "# mkdir -p $$HOME/.local/share/devhelp/funcy"
95 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/funcy"
96 | @echo "# devhelp"
97 |
98 | epub:
99 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
100 | @echo
101 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
102 |
103 | latex:
104 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
105 | @echo
106 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
107 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
108 | "(use \`make latexpdf' here to do that automatically)."
109 |
110 | latexpdf:
111 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
112 | @echo "Running LaTeX files through pdflatex..."
113 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
114 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
115 |
116 | text:
117 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
118 | @echo
119 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
120 |
121 | man:
122 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
123 | @echo
124 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
125 |
126 | texinfo:
127 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
128 | @echo
129 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
130 | @echo "Run \`make' in that directory to run these through makeinfo" \
131 | "(use \`make info' here to do that automatically)."
132 |
133 | info:
134 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
135 | @echo "Running Texinfo files through makeinfo..."
136 | make -C $(BUILDDIR)/texinfo info
137 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
138 |
139 | gettext:
140 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
141 | @echo
142 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
143 |
144 | changes:
145 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
146 | @echo
147 | @echo "The overview file is in $(BUILDDIR)/changes."
148 |
149 | linkcheck:
150 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
151 | @echo
152 | @echo "Link check complete; look for any errors in the above output " \
153 | "or in $(BUILDDIR)/linkcheck/output.txt."
154 |
155 | doctest:
156 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
157 | @echo "Testing of doctests in the sources finished, look at the " \
158 | "results in $(BUILDDIR)/doctest/output.txt."
159 |
--------------------------------------------------------------------------------
/docs/_static/nhpup_1.1.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | --------------------------------------------------------------------------
4 | Code for link-hover text boxes
5 | By Nicolas Höning
6 | Usage: a link
7 | The configuration dict with CSS class and width is optional - default is class .pup and width of 200px.
8 | You can style the popup box via CSS, targeting its ID #pup.
9 | You can escape " in the popup text with ".
10 | Tutorial and support at http://nicolashoening.de?twocents&nr=8
11 | --------------------------------------------------------------------------
12 |
13 | The MIT License (MIT)
14 |
15 | Copyright (c) 2014 Nicolas Höning
16 |
17 | Permission is hereby granted, free of charge, to any person obtaining a copy
18 | of this software and associated documentation files (the "Software"), to deal
19 | in the Software without restriction, including without limitation the rights
20 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
21 | copies of the Software, and to permit persons to whom the Software is
22 | furnished to do so, subject to the following conditions:
23 |
24 | The above copyright notice and this permission notice shall be included in
25 | all copies or substantial portions of the Software.
26 |
27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
30 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
32 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
33 | THE SOFTWARE.
34 | */
35 |
36 | nhpup = {
37 |
38 | pup: null, // This is the popup box, represented by a div
39 | identifier: "pup", // Name of ID and class of the popup box
40 | minMargin: 15, // Set how much minimal space there should be (in pixels)
41 | // between the popup and everything else (borders, mouse)
42 | default_width: 200, // Will be set to width from css in document.ready
43 | move: false, // Move it around with the mouse? we are only ready for that when the mouse event is set up.
44 | // Besides, having this turned off initially is resource-friendly.
45 |
46 | /*
47 | Write message, show popup w/ custom width if necessary,
48 | make sure it disappears on mouseout
49 | */
50 | popup: function(p_msg, p_config)
51 | {
52 | return function(e) {
53 | // do track mouse moves and update position
54 | this.move = true;
55 | // restore defaults
56 | this.pup.removeClass()
57 | .addClass(this.identifier)
58 | .width(this.default_width);
59 |
60 | // custom configuration
61 | if (typeof p_config != 'undefined') {
62 | if ('class' in p_config) {
63 | this.pup.addClass(p_config['class']);
64 | }
65 | if ('width' in p_config) {
66 | this.pup.width(p_config['width']);
67 | }
68 | }
69 |
70 | if (nhpup.hiding) {
71 | clearTimeout(nhpup.hiding);
72 | nhpup.hiding = null;
73 | }
74 |
75 | // Write content and display
76 | this.pup.html(p_msg).show();
77 |
78 | // Make sure popup goes away on mouse out and we stop the constant
79 | // positioning on mouse moves.
80 | // The event obj needs to be gotten from the virtual
81 | // caller, since we use onmouseover='nhpup.popup(p_msg)'
82 | var t = this.getTarget(e);
83 | $jq(t).unbind('mouseout').bind('mouseout',
84 | function(e){
85 | nhpup.move = false;
86 | if (nhpup.hiding) clearTimeout(nhpup.hiding);
87 | nhpup.hiding = setTimeout(function () {
88 | nhpup.pup.hide();
89 | }, 50)
90 | }
91 | );
92 | }.bind(this);
93 | },
94 |
95 | // set the target element position
96 | setElementPos: function(x, y)
97 | {
98 | // Call nudge to avoid edge overflow. Important tweak: x+10, because if
99 | // the popup is where the mouse is, the hoverOver/hoverOut events flicker
100 | var x_y = this.nudge(x + 10, y);
101 | // remember: the popup is still hidden
102 | this.pup.css('top', x_y[1] + 'px')
103 | .css('left', x_y[0] + 'px');
104 | },
105 |
106 | /* Avoid edge overflow */
107 | nudge: function(x,y)
108 | {
109 | var win = $jq(window);
110 |
111 | // When the mouse is too far on the right, put window to the left
112 | var xtreme = $jq(document).scrollLeft() + win.width() - this.pup.width() - this.minMargin;
113 | if(x > xtreme) {
114 | x -= this.pup.width() + 2 * this.minMargin;
115 | }
116 | x = this.max(x, 0);
117 |
118 | // When the mouse is too far down, move window up
119 | if((y + this.pup.height()) > (win.height() + $jq(document).scrollTop())) {
120 | y -= this.pup.height() + this.minMargin;
121 | }
122 |
123 | return [ x, y ];
124 | },
125 |
126 | /* custom max */
127 | max: function(a,b)
128 | {
129 | if (a>b) return a;
130 | else return b;
131 | },
132 |
133 | /*
134 | Get the target (element) of an event.
135 | Inspired by quirksmode
136 | */
137 | getTarget: function(e)
138 | {
139 | var targ;
140 | if (!e) var e = window.event;
141 | if (e.target) targ = e.target;
142 | else if (e.srcElement) targ = e.srcElement;
143 | if (targ.nodeType == 3) // defeat Safari bug
144 | targ = targ.parentNode;
145 | return targ;
146 | },
147 |
148 | onTouchDevice: function()
149 | {
150 | var deviceAgent = navigator.userAgent.toLowerCase();
151 | return deviceAgent.match(/(iphone|ipod|ipad|android|blackberry|iemobile|opera m(ob|in)i|vodafone)/) !== null;
152 | },
153 |
154 | initialized: false,
155 | initialize : function(){
156 | if (this.initialized) return;
157 |
158 | window.$jq = jQuery; // this is safe in WP installations with noConflict mode (which is default)
159 |
160 | /* Prepare popup and define the mouseover callback */
161 | jQuery(document).ready(function () {
162 | // create default popup on the page
163 | $jq('body').append('');
164 | nhpup.pup = $jq('#' + nhpup.identifier);
165 |
166 | // set dynamic coords when the mouse moves
167 | $jq(document).mousemove(function (e) {
168 | if (!nhpup.onTouchDevice()) { // turn off constant repositioning for touch devices (no use for this anyway)
169 | if (nhpup.move) {
170 | nhpup.setElementPos(e.pageX, e.pageY);
171 | }
172 | }
173 | });
174 | });
175 |
176 | this.initialized = true;
177 | }
178 | };
179 |
180 | if ('jQuery' in window) nhpup.initialize();
181 |
--------------------------------------------------------------------------------
/docs/_static/overrides.css:
--------------------------------------------------------------------------------
1 | @import "css/theme.css";
2 |
3 | .rst-content dl:not(.docutils) dt {
4 | float: left;
5 | margin: 2px 2px 1px 0px !important;
6 | }
7 |
8 | .rst-content dl:not(.docutils) dt + dd {
9 | padding-top: 6px;
10 | }
11 |
12 | .rst-content dl dd {
13 | clear: both;
14 | }
15 |
16 |
17 | .rst-content div[class^='highlight'] pre {
18 | padding: 7px 7px 4px 7px;
19 | font-size: 14px;
20 | line-height: 140%;
21 | }
22 |
23 | code.literal .pre {
24 | font-size: 13px;
25 | }
26 |
27 | .rst-content .external code {
28 | color: #404040;
29 | }
30 |
31 | /* Tables with functions */
32 | .rst-content table.docutils td {vertical-align: top; padding: 6px 16px; line-height: 20px}
33 | .wy-table-responsive table td {white-space: normal}
34 | .wy-table-responsive table td:first-child {white-space: nowrap}
35 | .rst-content code.xref {color: #2E7FB3; font-size: 90%;
36 | padding: 0px 2px; border: none; background: none; line-height: 20px}
37 |
--------------------------------------------------------------------------------
/docs/calc.rst:
--------------------------------------------------------------------------------
1 | Calculation
2 | ===========
3 |
4 | .. decorator:: memoize(*, key_func=None)
5 |
6 | Memoizes decorated function results, trading memory for performance. Can skip memoization
7 | for failed calculation attempts::
8 |
9 | @memoize # Omitting parentheses is ok
10 | def ip_to_city(ip):
11 | try:
12 | return request_city_from_slow_service(ip)
13 | except NotFound:
14 | return None # return None and memoize it
15 | except Timeout:
16 | raise memoize.skip(CITY) # return CITY, but don't memoize it
17 |
18 | Additionally ``@memoize`` exposes its memory for you to manipulate::
19 |
20 | # Prefill memory
21 | ip_to_city.memory.update({...})
22 |
23 | # Forget everything
24 | ip_to_city.memory.clear()
25 |
26 | Custom `key_func` could be used to work with unhashable objects, insignificant arguments, etc::
27 |
28 | @memoize(key_func=lambda obj, verbose=None: obj.key)
29 | def do_things(obj, verbose=False):
30 | # ...
31 |
32 |
33 | .. decorator:: make_lookuper
34 |
35 | As :func:`@memoize`, but with prefilled memory. Decorated function should return all available arg-value pairs, which should be a dict or a sequence of pairs. Resulting function will raise ``LookupError`` for any argument missing in it::
36 |
37 | @make_lookuper
38 | def city_location():
39 | return {row['city']: row['location'] for row in fetch_city_locations()}
40 |
41 | If decorated function has arguments then separate lookuper with its own lookup table is created for each combination of arguments. This can be used to make lookup tables on demand::
42 |
43 | @make_lookuper
44 | def function_lookup(f):
45 | return {x: f(x) for x in range(100)}
46 |
47 | fast_sin = function_lookup(math.sin)
48 | fast_cos = function_lookup(math.cos)
49 |
50 | Or load some resources, memoize them and use as a function::
51 |
52 | @make_lookuper
53 | def translate(lang):
54 | return make_list_of_pairs(load_translation_file(lang))
55 |
56 | russian_phrases = lmap(translate('ru'), english_phrases)
57 |
58 |
59 | .. decorator:: silent_lookuper
60 |
61 | Same as :func:`@make_lookuper`, but returns ``None`` on memory miss.
62 |
63 |
64 | .. decorator:: cache(timeout, *, key_func=None)
65 |
66 | Caches decorated function results for ``timeout``.
67 | It can be either number of seconds or :class:`py3:datetime.timedelta`::
68 |
69 | @cache(60 * 60)
70 | def api_call(query):
71 | # ...
72 |
73 | Cache can be invalidated before timeout with::
74 |
75 | api_call.invalidate(query) # Forget cache for query
76 | api_call.invalidate_all() # Forget everything
77 |
78 | Custom ``key_func`` could be used same way as in :func:`@memoize`::
79 |
80 | # Do not use token in cache key
81 | @cache(60 * 60, key_func=lambda query, token=None: query)
82 | def api_call(query, token=None):
83 | # ...
84 |
85 |
86 | .. raw:: html
87 | :file: descriptions.html
88 |
--------------------------------------------------------------------------------
/docs/cheatsheet.rst:
--------------------------------------------------------------------------------
1 | .. _cheatsheet:
2 |
3 | Cheatsheet
4 | ==========
5 |
6 | Hover over function to get its description. Click to jump to docs.
7 |
8 |
9 | Sequences
10 | ---------
11 |
12 | ========== ==============================================================
13 | Create :func:`count` :func:`cycle` :func:`repeat` :func:`repeatedly` :func:`iterate` :func:`re_all` :func:`re_iter`
14 | Access :func:`first` :func:`second` :func:`last` :func:`nth` :func:`some` :func:`take`
15 | Slice :func:`take` :func:`drop` :func:`rest` :func:`butlast` :func:`takewhile` :func:`dropwhile` :func:`split_at` :func:`split_by`
16 | Transform :func:`map` :func:`mapcat` :func:`keep` :func:`pluck` :func:`pluck_attr` :func:`invoke`
17 | Filter :func:`filter` :func:`remove` :func:`keep` :func:`distinct` :func:`where` :func:`without`
18 | Join :func:`cat` :func:`concat` :func:`flatten` :func:`mapcat` :func:`interleave` :func:`interpose`
19 | Partition :func:`chunks` :func:`partition` :func:`partition_by` :func:`split_at` :func:`split_by`
20 | Group :func:`split` :func:`count_by` :func:`count_reps` :func:`group_by` :func:`group_by_keys` :func:`group_values`
21 | Aggregate :func:`ilen` :func:`reductions` :func:`sums` :func:`all` :func:`any` :func:`none` :func:`one` :func:`count_by` :func:`count_reps`
22 | Iterate :func:`pairwise` :func:`with_prev` :func:`with_next` :func:`zip_values` :func:`zip_dicts` :func:`tree_leaves` :func:`tree_nodes`
23 | ========== ==============================================================
24 |
25 |
26 | .. _colls:
27 |
28 | Collections
29 | -----------
30 |
31 | ===================== ==============================================================
32 | Join :func:`merge` :func:`merge_with` :func:`join` :func:`join_with`
33 | Transform :func:`walk` :func:`walk_keys` :func:`walk_values`
34 | Filter :func:`select` :func:`select_keys` :func:`select_values` :func:`compact`
35 | Dicts :ref:`*` :func:`flip` :func:`zipdict` :func:`pluck` :func:`where` :func:`itervalues` :func:`iteritems` :func:`zip_values` :func:`zip_dicts` :func:`project` :func:`omit`
36 | Misc :func:`empty` :func:`get_in` :func:`get_lax` :func:`set_in` :func:`update_in` :func:`del_in` :func:`has_path`
37 | ===================== ==============================================================
38 |
39 |
40 | Functions
41 | ---------
42 |
43 | .. :ref:`*`
44 |
45 | ========== ==============================================================
46 | Create :func:`identity` :func:`constantly` :func:`func_partial` :func:`partial` :func:`rpartial` :func:`iffy` :func:`caller` :func:`re_finder` :func:`re_tester`
47 | Transform :func:`complement` :func:`iffy` :func:`autocurry` :func:`curry` :func:`rcurry`
48 | Combine :func:`compose` :func:`rcompose` :func:`juxt` :func:`all_fn` :func:`any_fn` :func:`none_fn` :func:`one_fn` :func:`some_fn`
49 | ========== ==============================================================
50 |
51 |
52 | Other topics
53 | ------------
54 |
55 | ================== ==============================================================
56 | Content tests :func:`all` :func:`any` :func:`none` :func:`one` :func:`is_distinct`
57 | Type tests :func:`isa` :func:`is_iter` :func:`is_list` :func:`is_tuple` :func:`is_set` :func:`is_mapping` :func:`is_seq` :func:`is_seqcoll` :func:`is_seqcont` :func:`iterable`
58 | Decorators :func:`decorator` :func:`wraps` :func:`unwrap` :func:`autocurry`
59 | Control flow :func:`once` :func:`once_per` :func:`once_per_args` :func:`collecting` :func:`joining` :func:`post_processing` :func:`throttle` :func:`wrap_with`
60 | Error handling :func:`retry` :func:`silent` :func:`ignore` :func:`suppress` :func:`limit_error_rate` :func:`fallback` :func:`raiser` :func:`reraise`
61 | Debugging :func:`tap` :func:`log_calls` :func:`log_enters` :func:`log_exits` :func:`log_errors` :func:`log_durations` :func:`log_iter_durations`
62 | Caching :func:`memoize` :func:`cache` :func:`cached_property` :func:`cached_readonly` :func:`make_lookuper` :func:`silent_lookuper`
63 | Regexes :func:`re_find` :func:`re_test` :func:`re_all` :func:`re_iter` :func:`re_finder` :func:`re_tester`
64 | Strings :func:`cut_prefix` :func:`cut_suffix` :func:`str_join`
65 | Objects :func:`cached_property` :func:`cached_readonly` :func:`wrap_prop` :func:`monkey` :func:`invoke` :func:`pluck_attr` :class:`LazyObject`
66 | Primitives :func:`isnone` :func:`notnone` :func:`inc` :func:`dec` :func:`even` :func:`odd`
67 | ================== ==============================================================
68 |
69 |
70 | .. raw:: html
71 | :file: descriptions.html
72 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # funcy documentation build configuration file, created by
4 | # sphinx-quickstart on Tue Dec 18 21:32:23 2012.
5 | #
6 | # This file is execfile()d with the current directory set to its containing dir.
7 | #
8 | # Note that not all possible configuration values are present in this
9 | # autogenerated file.
10 | #
11 | # All configuration values have a default; values that are commented out
12 | # serve to show the default.
13 |
14 | import sys, os, re
15 | sys.path.insert(0, os.path.abspath('..'))
16 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
17 |
18 | # If extensions (or modules to document with autodoc) are in another directory,
19 | # add these directories to sys.path here. If the directory is relative to the
20 | # documentation root, use os.path.abspath to make it absolute, like shown here.
21 | #sys.path.insert(0, os.path.abspath('.'))
22 |
23 | # -- General configuration -----------------------------------------------------
24 |
25 | # If your documentation needs a minimal Sphinx version, state it here.
26 | needs_sphinx = '3.5.3'
27 |
28 | # Add any Sphinx extension module names here, as strings. They can be extensions
29 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
30 | extensions = [
31 | 'sphinx.ext.coverage',
32 | 'sphinx.ext.intersphinx',
33 | 'sphinx.ext.autodoc',
34 | 'sphinxcontrib.jquery'
35 | ]
36 |
37 | intersphinx_mapping = {
38 | 'py2': ('http://docs.python.org/2', None),
39 | 'py3': ('http://docs.python.org/3', None),
40 | }
41 |
42 | # Add any paths that contain templates here, relative to this directory.
43 | templates_path = ['_templates']
44 |
45 | # The suffix of source filenames.
46 | source_suffix = '.rst'
47 |
48 | # The encoding of source files.
49 | #source_encoding = 'utf-8-sig'
50 |
51 | # The master toctree document.
52 | master_doc = 'index'
53 | toc_object_entries = False
54 |
55 | # General information about the project.
56 | project = u'funcy'
57 | copyright = u'2012-2024, Alexander Schepanovski'
58 |
59 | # The version info for the project you're documenting, acts as replacement for
60 | # |version| and |release|, also used in various other places throughout the
61 | # built documents.
62 | #
63 | VERSION = open('../VERSION').read().strip()
64 | # The short X.Y version.
65 | version = re.match(r'^\d+\.\d+', VERSION).group(0)
66 | # The full version, including alpha/beta/rc tags.
67 | release = VERSION
68 |
69 | # The language for content autogenerated by Sphinx. Refer to documentation
70 | # for a list of supported languages.
71 | #language = None
72 |
73 | # There are two options for replacing |today|: either, you set today to some
74 | # non-false value, then it is used:
75 | #today = ''
76 | # Else, today_fmt is used as the format for a strftime call.
77 | #today_fmt = '%B %d, %Y'
78 |
79 | # List of patterns, relative to source directory, that match files and
80 | # directories to ignore when looking for source files.
81 | exclude_patterns = ['_build']
82 |
83 | # The reST default role (used for this markup: `text`) to use for all documents.
84 | #default_role = None
85 |
86 | # If true, '()' will be appended to :func: etc. cross-reference text.
87 | #add_function_parentheses = True
88 |
89 | # If true, the current module name will be prepended to all description
90 | # unit titles (such as .. function::).
91 | add_module_names = False
92 |
93 | # If true, sectionauthor and moduleauthor directives will be shown in the
94 | # output. They are ignored by default.
95 | #show_authors = False
96 |
97 | # The name of the Pygments (syntax highlighting) style to use.
98 | pygments_style = 'sphinx'
99 |
100 | # A list of ignored prefixes for module index sorting.
101 | #modindex_common_prefix = []
102 |
103 |
104 | # -- Options for HTML output ---------------------------------------------------
105 |
106 | html_theme = "sphinx_rtd_theme"
107 |
108 | # The theme to use for HTML and HTML Help pages. See the documentation for
109 | # a list of builtin themes.
110 | # html_style = '...'
111 |
112 | # Theme options are theme-specific and customize the look and feel of a theme
113 | # further. For a list of options available for each theme, see the
114 | # documentation.
115 | #html_theme_options = {}
116 |
117 | # Add any paths that contain custom themes here, relative to this directory.
118 | # html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
119 |
120 | # The name for this set of Sphinx documents. If None, it defaults to
121 | # " v documentation".
122 | #html_title = None
123 |
124 | # A shorter title for the navigation bar. Default is the same as html_title.
125 | #html_short_title = None
126 |
127 | # The name of an image file (relative to this directory) to place at the top
128 | # of the sidebar.
129 | #html_logo = None
130 |
131 | # The name of an image file (within the static path) to use as favicon of the
132 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
133 | # pixels large.
134 | #html_favicon = None
135 |
136 | # Add any paths that contain custom static files (such as style sheets) here,
137 | # relative to this directory. They are copied after the builtin static files,
138 | # so a file named "default.css" will overwrite the builtin "default.css".
139 | html_static_path = ['_static']
140 |
141 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
142 | # using the given strftime format.
143 | #html_last_updated_fmt = '%b %d, %Y'
144 |
145 | # If true, SmartyPants will be used to convert quotes and dashes to
146 | # typographically correct entities.
147 | #html_use_smartypants = True
148 |
149 | # Custom sidebar templates, maps document names to template names.
150 | #html_sidebars = {}
151 |
152 | # Additional templates that should be rendered to pages, maps page names to
153 | # template names.
154 | #html_additional_pages = {}
155 |
156 | # If false, no module index is generated.
157 | #html_domain_indices = True
158 |
159 | # If false, no index is generated.
160 | #html_use_index = True
161 |
162 | # If true, the index is split into individual pages for each letter.
163 | #html_split_index = False
164 |
165 | # If true, links to the reST sources are added to the pages.
166 | #html_show_sourcelink = True
167 |
168 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
169 | #html_show_sphinx = True
170 |
171 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
172 | #html_show_copyright = True
173 |
174 | # If true, an OpenSearch description file will be output, and all pages will
175 | # contain a tag referring to it. The value of this option must be the
176 | # base URL from which the finished HTML is served.
177 | #html_use_opensearch = ''
178 |
179 | # This is the file name suffix for HTML files (e.g. ".xhtml").
180 | #html_file_suffix = None
181 |
182 | # Output file base name for HTML help builder.
183 | htmlhelp_basename = 'funcydoc'
184 |
185 |
186 | # -- Options for LaTeX output --------------------------------------------------
187 |
188 | latex_elements = {
189 | # The paper size ('letterpaper' or 'a4paper').
190 | #'papersize': 'letterpaper',
191 |
192 | # The font size ('10pt', '11pt' or '12pt').
193 | #'pointsize': '10pt',
194 |
195 | # Additional stuff for the LaTeX preamble.
196 | #'preamble': '',
197 | }
198 |
199 | # Grouping the document tree into LaTeX files. List of tuples
200 | # (source start file, target name, title, author, documentclass [howto/manual]).
201 | latex_documents = [
202 | ('index', 'funcy.tex', u'funcy documentation',
203 | u'Alexander Schepanovski', 'manual'),
204 | ]
205 |
206 | # The name of an image file (relative to this directory) to place at the top of
207 | # the title page.
208 | #latex_logo = None
209 |
210 | # For "manual" documents, if this is true, then toplevel headings are parts,
211 | # not chapters.
212 | #latex_use_parts = False
213 |
214 | # If true, show page references after internal links.
215 | #latex_show_pagerefs = False
216 |
217 | # If true, show URL addresses after external links.
218 | #latex_show_urls = False
219 |
220 | # Documents to append as an appendix to all manuals.
221 | #latex_appendices = []
222 |
223 | # If false, no module index is generated.
224 | #latex_domain_indices = True
225 |
226 |
227 | # -- Options for manual page output --------------------------------------------
228 |
229 | # One entry per manual page. List of tuples
230 | # (source start file, name, description, authors, manual section).
231 | man_pages = [
232 | ('index', 'funcy', u'funcy documentation',
233 | [u'Alexander Schepanovski'], 1)
234 | ]
235 |
236 | # If true, show URL addresses after external links.
237 | #man_show_urls = False
238 |
239 |
240 | # -- Options for Texinfo output ------------------------------------------------
241 |
242 | # Grouping the document tree into Texinfo files. List of tuples
243 | # (source start file, target name, title, author,
244 | # dir menu entry, description, category)
245 | texinfo_documents = [
246 | ('index', 'funcy', u'funcy documentation',
247 | u'Alexander Schepanovski', 'funcy', 'A fancy and practical functional tools.',
248 | 'Miscellaneous'),
249 | ]
250 |
251 | # Documents to append as an appendix to all manuals.
252 | #texinfo_appendices = []
253 |
254 | # If false, no module index is generated.
255 | #texinfo_domain_indices = True
256 |
257 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
258 | #texinfo_show_urls = 'footnote'
259 |
260 | # rst_prolog = """
261 | # .. module:: funcy
262 |
263 | # """
264 |
265 |
266 | def setup(app):
267 | app.add_css_file('overrides.css')
268 |
--------------------------------------------------------------------------------
/docs/debug.rst:
--------------------------------------------------------------------------------
1 | Debugging
2 | =========
3 |
4 | .. function:: tap(value, label=None)
5 |
6 | Prints a value and then returns it. Useful to tap into some functional pipeline for debugging::
7 |
8 | fields = (f for f in fields_for(category) if section in tap(tap(f).sections))
9 | # ... do something with fields
10 |
11 | If ``label`` is specified then it's printed before corresponding value::
12 |
13 | squares = {tap(x, 'x'): tap(x * x, 'x^2') for x in [3, 4]}
14 | # x: 3
15 | # x^2: 9
16 | # x: 4
17 | # x^2: 16
18 | # => {3: 9, 4: 16}
19 |
20 |
21 | .. decorator:: log_calls(print_func, errors=True, stack=True, repr_len=25)
22 | print_calls(errors=True, stack=True, repr_len=25)
23 |
24 | Will log or print all function calls, including arguments, results and raised exceptions. Can be used as a decorator or tapped into call expression::
25 |
26 | sorted_fields = sorted(fields, key=print_calls(lambda f: f.order))
27 |
28 | If ``errors`` is set to ``False`` then exceptions are not logged. This could be used to separate channels for normal and error logging::
29 |
30 | @log_calls(log.info, errors=False)
31 | @log_errors(log.exception)
32 | def some_suspicious_function(...):
33 | # ...
34 | return result
35 |
36 |
37 | .. decorator:: log_enters(print_func, repr_len=25)
38 | print_enters(repr_len=25)
39 | log_exits(print_func, errors=True, stack=True, repr_len=25)
40 | print_exits(errors=True, stack=True, repr_len=25)
41 |
42 | Will log or print every time execution enters or exits the function. Should be used same way as :func:`@log_calls()` and :func:`@print_calls()` when you need to track only one event per function call.
43 |
44 |
45 | .. decorator:: log_errors(print_func, label=None, stack=True, repr_len=25)
46 | print_errors(label=None, stack=True, repr_len=25)
47 |
48 | Will log or print all function errors providing function arguments causing them. If ``stack``
49 | is set to ``False`` then each error is reported with simple one line message.
50 |
51 | Can be combined with :func:`@silent` or :func:`@ignore()` to trace occasionally misbehaving function::
52 |
53 | @ignore(...)
54 | @log_errors(logging.warning)
55 | def guess_user_id(username):
56 | initial = first_guess(username)
57 | # ...
58 |
59 | Can also be used as context decorator::
60 |
61 | with print_errors('initialization', stack=False):
62 | load_this()
63 | load_that()
64 | # ...
65 | # SomeException: a bad thing raised in initialization
66 |
67 |
68 | .. decorator:: log_durations(print_func, label=None, unit='auto', threshold=None, repr_len=25)
69 | print_durations(label=None, unit='auto', threshold=None, repr_len=25)
70 |
71 | Will time each function call and log or print its duration::
72 |
73 | @log_durations(logging.info)
74 | def do_hard_work(n):
75 | samples = range(n)
76 | # ...
77 |
78 | # 121 ms in do_hard_work(10)
79 | # 143 ms in do_hard_work(11)
80 | # ...
81 |
82 | A block of code could be timed with a help of context manager::
83 |
84 | with print_durations('Creating models'):
85 | Model.objects.create(...)
86 | # ...
87 |
88 | # 10.2 ms in Creating models
89 |
90 | ``unit`` argument can be set to ``'ns'``, ``'mks'``, ``'ms'`` or ``'s'`` to use uniform time unit. If ``threshold`` is set then durations under this number of seconds are not logged. Handy to capture slow queries or API calls::
91 |
92 | @log_durations(logging.warning, threshold=0.5)
93 | def make_query(sql, params):
94 | # ...
95 |
96 |
97 | .. function:: log_iter_durations(seq, print_func, label=None, unit='auto')
98 | print_iter_durations(seq, label=None, unit='auto')
99 |
100 | Wraps iterable ``seq`` into generator logging duration of processing of each item::
101 |
102 |
103 | for item in print_iter_durations(seq, label='hard work'):
104 | do_smth(item)
105 |
106 | # 121 ms in iteration 0 of hard work
107 | # 143 ms in iteration 1 of hard work
108 | # ...
109 |
110 | ``unit`` can be set to ``'ns'``, ``'mks'``, ``'ms'`` or ``'s'``.
111 |
112 |
113 | .. raw:: html
114 | :file: descriptions.html
115 |
--------------------------------------------------------------------------------
/docs/decorators.rst:
--------------------------------------------------------------------------------
1 | Decorators
2 | ==========
3 |
4 | .. module:: funcy
5 |
6 | .. decorator:: decorator
7 |
8 | Transforms a flat wrapper into a decorator with or without arguments.
9 | ``@decorator`` passes special ``call`` object as a first argument to a wrapper.
10 | A resulting decorator will preserve function module, name and docstring.
11 | It also adds ``__wrapped__`` attribute referring to wrapped function
12 | and ``__original__`` attribute referring to innermost wrapped one.
13 |
14 | Here is a simple logging decorator::
15 |
16 | @decorator
17 | def log(call):
18 | print(call._func.__name__, call._args, call._kwargs)
19 | return call()
20 |
21 | ``call`` object also supports by name arg introspection and passing additional arguments to decorated function::
22 |
23 | @decorator
24 | def with_phone(call):
25 | # call.request gets actual request value upon function call
26 | request = call.request
27 | # ...
28 | phone = Phone.objects.get(number=request.GET['phone'])
29 | # phone arg is added to *args passed to decorated function
30 | return call(phone)
31 |
32 | @with_phone
33 | def some_view(request, phone):
34 | # ... some code using phone
35 | return # ...
36 |
37 | A better practice would be adding keyword argument not positional. This makes such decorators more composable::
38 |
39 | @decorator
40 | def with_phone(call):
41 | # ...
42 | return call(phone=phone)
43 |
44 | @decorator
45 | def with_user(call):
46 | # ...
47 | return call(user=user)
48 |
49 | @with_phone
50 | @with_user
51 | def some_view(request, phone=None, user=None):
52 | # ...
53 | return # ...
54 |
55 | If a function wrapped with ``@decorator`` has arguments other than ``call``, then decorator with arguments is created::
56 |
57 | @decorator
58 | def joining(call, sep):
59 | return sep.join(call())
60 |
61 | Generally a decorator with arguments is required to be called with ``()`` when applied to function. However, if you use only keyword only parameters aside from ``call`` then you can omit them::
62 |
63 | @decorator
64 | def rate_limit(call, *, extra_labels=None):
65 | # ...
66 |
67 | @rate_limit # no extra labels, parentheses are optional
68 | def func(request, ...):
69 | # ...
70 |
71 | @rate_limit(extra_labels=lambda r: [f"user:{r.user.pk}"])
72 | def func(request, ...):
73 | # ...
74 |
75 | You can see more examples in :mod:`flow` and :mod:`debug` submodules source code.
76 |
77 |
78 | .. decorator:: contextmanager
79 |
80 | A decorator helping to create context managers. Resulting functions also
81 | behave as decorators. This is a reexport or backport of :func:`py3:contextlib.contextmanager`.
82 |
83 |
84 | .. autodecorator:: wraps(wrapped, [assigned], [updated])
85 |
86 | .. autofunction:: unwrap
87 |
88 | .. autoclass:: ContextDecorator
89 |
90 |
91 | .. raw:: html
92 | :file: descriptions.html
93 |
--------------------------------------------------------------------------------
/docs/extended_fns.rst:
--------------------------------------------------------------------------------
1 | .. _extended_fns:
2 |
3 | Extended function semantics
4 | ===================================
5 |
6 | Many of funcy functions expecting predicate or mapping function as an argument can take something uncallable instead of it with semantics described in this table:
7 |
8 | ============ ================================= =================================
9 | f passed Function Predicate
10 | ============ ================================= =================================
11 | ``None`` :func:`identity ` bool
12 | string :func:`re_finder(f) ` :func:`re_tester(f) `
13 | int or slice ``itemgetter(f)`` ``itemgetter(f)``
14 | mapping ``lambda x: f[x]`` ``lambda x: f[x]``
15 | set ``lambda x: x in f`` ``lambda x: x in f``
16 | ============ ================================= =================================
17 |
18 |
19 | Supporting functions
20 | --------------------
21 |
22 | Here is a full list of functions supporting extended function semantics:
23 |
24 | ========================= ==============================================================
25 | Group Functions
26 | ========================= ==============================================================
27 | Sequence transformation :func:`map` :func:`keep` :func:`mapcat`
28 | Sequence filtering :func:`filter` :func:`remove` :func:`distinct`
29 | Sequence splitting :func:`dropwhile` :func:`takewhile` :func:`split` :func:`split_by` :func:`partition_by`
30 | Aggregration :func:`group_by` :func:`count_by` :func:`group_by_keys`
31 | Collection transformation :func:`walk` :func:`walk_keys` :func:`walk_values`
32 | Collection filtering :func:`select` :func:`select_keys` :func:`select_values`
33 | Content tests :func:`all` :func:`any` :func:`none` :func:`one` :func:`some` :func:`is_distinct`
34 | Function logic :func:`all_fn` :func:`any_fn` :func:`none_fn` :func:`one_fn` :func:`some_fn`
35 | Function tools :func:`iffy` :func:`compose` :func:`rcompose` :func:`complement` :func:`juxt` :func:`all_fn` :func:`any_fn` :func:`none_fn` :func:`one_fn` :func:`some_fn`
36 | ========================= ==============================================================
37 |
38 | List or iterator versions of same functions not listed here for brevity but also support extended semantics.
39 |
40 | .. raw:: html
41 | :file: descriptions.html
42 |
--------------------------------------------------------------------------------
/docs/flow.rst:
--------------------------------------------------------------------------------
1 | Flow
2 | ====
3 |
4 | .. decorator:: silent
5 |
6 | Ignore all real exceptions (descendants of :exc:`~py3:exceptions.Exception`). Handy for cleaning data such as user input::
7 |
8 |
9 | brand_id = silent(int)(request.GET['brand_id'])
10 | ids = keep(silent(int), request.GET.getlist('id'))
11 |
12 | And in data import/transform::
13 |
14 | get_greeting = compose(silent(string.lower), re_finder(r'(\w+)!'))
15 | map(get_greeting, ['a!', ' B!', 'c.'])
16 | # -> ['a', 'b', None]
17 |
18 | .. note:: Avoid silencing non-primitive functions, use :func:`@ignore()` instead and even then be careful not to swallow exceptions unintentionally.
19 |
20 |
21 | .. decorator:: ignore(errors, default=None)
22 |
23 | Same as :func:`@silent`, but able to specify ``errors`` to catch and ``default`` to return in case of error caught. ``errors`` can either be exception class or a tuple of them.
24 |
25 |
26 | .. function:: suppress(*errors)
27 |
28 | A context manager which suppresses given exceptions under its scope::
29 |
30 | with suppress(HttpError):
31 | # Assume this request can fail, and we are ok with it
32 | make_http_request()
33 |
34 |
35 | .. function:: nullcontext(enter_result=None)
36 |
37 | A noop context manager that returns ``enter_result`` from ``__enter__``::
38 |
39 | ctx = nullcontext()
40 | if threads:
41 | ctx = op_thread_lock
42 |
43 | with ctx:
44 | # ... do stuff
45 |
46 |
47 | .. decorator:: once
48 | once_per_args
49 | once_per(*argnames)
50 |
51 | Call function only once, once for every combination of values of its arguments or once for every combination of given arguments. Thread safe. Handy for various initialization purposes::
52 |
53 | # Global initialization
54 | @once
55 | def initialize_cache():
56 | conn = some.Connection(...)
57 | # ... set up everything
58 |
59 | # Per argument initialization
60 | @once_per_args
61 | def initialize_language(lang):
62 | conf = load_language_conf(lang)
63 | # ... set up language
64 |
65 | # Setup each class once
66 | class SomeManager(Manager):
67 | @once_per('cls')
68 | def _initialize_class(self, cls):
69 | pre_save.connect(self._pre_save, sender=cls)
70 | # ... set up signals, no dups
71 |
72 |
73 | .. function:: raiser(exception_or_class=Exception, *args, **kwargs)
74 |
75 | Constructs function that raises given exception with given arguments on any invocation. You may pass a string instead of exception as a shortcut::
76 |
77 | mocker.patch('mod.Class.propname', property(raiser("Shouldn't be called")))
78 |
79 | This will raise an ``Exception`` with a corresponding message.
80 |
81 |
82 | .. decorator:: reraise(errors, into)
83 |
84 | Intercepts any error of ``errors`` classes and reraises it as ``into`` error. Can be used as decorator or a context manager::
85 |
86 | with reraise(json.JSONDecodeError, SuspiciousOperation('Invalid JSON')):
87 | return json.loads(text)
88 |
89 | ``into`` can also be a callable to transform the error before reraising::
90 |
91 | @reraise(requests.RequestsError, lambda e: MyAPIError(error_desc(e)))
92 | def api_call(...):
93 | # ...
94 |
95 |
96 | .. decorator:: retry(tries, errors=Exception, timeout=0, filter_errors=None)
97 |
98 | Every call of the decorated function is tried up to ``tries`` times. The first attempt counts as a try. Retries occur when any subclass of ``errors`` is raised, where``errors`` is an exception class or a list/tuple of exception classes. There will be a delay in ``timeout`` seconds between tries.
99 |
100 | A common use is to wrap some unreliable action::
101 |
102 | @retry(3, errors=HttpError)
103 | def download_image(url):
104 | # ... make http request
105 | return image
106 |
107 | Errors to retry may addtionally be filtered with ``filter_errors`` when classes are not specific enough::
108 |
109 | @retry(3, errors=HttpError, filter_errors=lambda e: e.status_code >= 500)
110 | def download_image(url):
111 | # ...
112 |
113 | You can pass a callable as ``timeout`` to achieve exponential delays or other complex behavior::
114 |
115 | @retry(3, errors=HttpError, timeout=lambda a: 2 ** a)
116 | def download_image(url):
117 | # ... make http request
118 | return image
119 |
120 |
121 | .. function:: fallback(*approaches)
122 |
123 | Tries several approaches until one works. Each approach is either callable or a tuple ``(callable, errors)``, where errors is an exception class or a tuple of classes, which signal to fall back to next approach. If ``errors`` is not supplied then fall back is done for any :exc:`~py3:exceptions.Exception`::
124 |
125 | fallback(
126 | (partial(send_mail, ADMIN_EMAIL, message), SMTPException),
127 | partial(log.error, message), # Handle any Exception
128 | (raiser(FeedbackError, "Failed"), ()) # Handle nothing
129 | )
130 |
131 |
132 | .. function:: limit_error_rate(fails, timeout, exception=ErrorRateExceeded)
133 |
134 | If function fails to complete ``fails`` times in a row, calls to it will be intercepted for ``timeout`` with ``exception`` raised instead. A clean way to short-circuit function taking too long to fail::
135 |
136 | @limit_error_rate(fails=5, timeout=60,
137 | exception=RequestError('Temporary unavailable'))
138 | def do_request(query):
139 | # ... make a http request
140 | return data
141 |
142 | Can be combined with :func:`ignore` to silently stop trying for a while::
143 |
144 | @ignore(ErrorRateExceeded, default={'id': None, 'name': 'Unknown'})
145 | @limit_error_rate(fails=5, timeout=60)
146 | def get_user(id):
147 | # ... make a http request
148 | return data
149 |
150 |
151 | .. function:: throttle(period)
152 |
153 | Only runs a decorated function once in a ``period``::
154 |
155 | @throttle(60)
156 | def process_beat(pk, progress):
157 | Model.objects.filter(pk=pk).update(beat=timezone.now(), progress=progress)
158 |
159 | # Processing something, update progress info no more often then once a minute
160 | for i in ...:
161 | process_beat(pk, i / n)
162 | # ... do actual processing
163 |
164 |
165 | .. decorator:: collecting
166 |
167 | Transforms generator or other iterator returning function into list returning one.
168 |
169 | Handy to prevent quirky iterator-returning properties::
170 |
171 | @property
172 | @collecting
173 | def path_up(self):
174 | node = self
175 | while node:
176 | yield node
177 | node = node.parent
178 |
179 | Also makes list constructing functions beautifully yielding.
180 |
181 | .. Or you could just write::
182 |
183 | .. @property
184 | .. def path_up(self):
185 | .. going_up = iterate(attrgetter('parent'), self)
186 | .. return list(takewhile(bool, going_up))
187 |
188 |
189 | .. decorator:: joining(sep)
190 |
191 | Wraps common python idiom "collect then join" into a decorator. Transforms generator or alike into function, returning string of joined results. Automatically converts all elements to separator type for convenience.
192 |
193 | Goes well with generators with some ad-hoc logic within::
194 |
195 | @joining(', ')
196 | def car_desc(self):
197 | yield self.year_made
198 | if self.engine_volume: yield '%s cc' % self.engine_volume
199 | if self.transmission: yield self.get_transmission_display()
200 | if self.gear: yield self.get_gear_display()
201 | # ...
202 |
203 | Use ``bytes`` separator to get bytes result::
204 |
205 | @joining(b' ')
206 | def car_desc(self):
207 | yield self.year_made
208 | # ...
209 |
210 | See also :func:`str_join`.
211 |
212 |
213 | .. decorator:: post_processing(func)
214 |
215 | Passes decorated function result through ``func``. This is the generalization of :func:`@collecting` and :func:`@joining()`. Could save you writing a decorator or serve as an extended comprehension:
216 |
217 | ::
218 |
219 | @post_processing(dict)
220 | def make_cond(request):
221 | if request.GET['new']:
222 | yield 'year__gt', 2000
223 | for key, value in request.GET.items():
224 | if value == '':
225 | continue
226 | # ...
227 |
228 |
229 | .. decorator:: wrap_with(ctx)
230 |
231 | Turns a context manager into a decorator::
232 |
233 | @wrap_with(threading.Lock())
234 | def protected_func(...):
235 | # ...
236 |
237 | .. raw:: html
238 | :file: descriptions.html
239 |
--------------------------------------------------------------------------------
/docs/funcs.rst:
--------------------------------------------------------------------------------
1 | Functions
2 | =========
3 |
4 | .. function:: identity(x)
5 |
6 | Returns its argument.
7 |
8 |
9 | .. function:: constantly(x)
10 |
11 | Returns function accepting any args, but always returning ``x``.
12 |
13 |
14 | .. function:: caller(*args, **kwargs)
15 |
16 | Returns function calling its argument with passed arguments.
17 |
18 |
19 | .. function:: partial(func, *args, **kwargs)
20 |
21 | Returns partial application of ``func``. A re-export of :func:`py3:functools.partial`. Can be used in a variety of ways. DSLs is one of them::
22 |
23 | field = dict
24 | json_field = partial(field, json=True)
25 |
26 |
27 | .. function:: rpartial(func, *args)
28 |
29 | Partially applies last arguments in ``func``::
30 |
31 | from operator import div
32 | one_third = rpartial(div, 3.0)
33 |
34 | Arguments are passed to ``func`` in the same order as they came to :func:`rpartial`::
35 |
36 | separate_a_word = rpartial(str.split, ' ', 1)
37 |
38 |
39 | .. function:: func_partial(func, *args, **kwargs)
40 |
41 | Like :func:`partial` but returns a real function. Which is useful when, for example, you want to create a method of it::
42 |
43 | setattr(self, 'get_%s_display' % field.name,
44 | func_partial(_get_FIELD_display, field))
45 |
46 | Use :func:`partial` if you are ok to get callable object instead of function as it's faster.
47 |
48 |
49 | .. function:: curry(func[, n])
50 |
51 | Curries function. For example, given function of two arguments ``f(a, b)`` returns function::
52 |
53 | lambda a: lambda b: f(a, b)
54 |
55 | Handy to make a partial factory::
56 |
57 | make_tester = curry(re_test)
58 | is_word = make_tester(r'^\w+$')
59 | is_int = make_tester(r'^[1-9]\d*$')
60 |
61 | But see :func:`re_tester` if you need this particular one.
62 |
63 |
64 | .. function:: rcurry(func[, n])
65 |
66 | Curries function from last argument to first::
67 |
68 | has_suffix = rcurry(str.endswith, 2)
69 | lfilter(has_suffix("ce"), ["nice", "cold", "ice"])
70 | # -> ["nice", "ice"]
71 |
72 | Can fix number of arguments when it's ambiguous::
73 |
74 | to_power = rcurry(pow, 2) # curry 2 first args in reverse order
75 | to_square = to_power(2)
76 | to_cube = to_power(3)
77 |
78 |
79 | .. function:: autocurry(func)
80 |
81 | Constructs a version of ``func`` returning its partial applications until sufficient arguments are passed::
82 |
83 | def remainder(what, by):
84 | return what % by
85 | rem = autocurry(remainder)
86 |
87 | assert rem(10, 3) == rem(10)(3) == rem()(10, 3) == 1
88 | assert map(rem(by=3), range(5)) == [0, 1, 2, 0, 1]
89 |
90 | Can clean your code a bit when :func:`partial` makes it too cluttered.
91 |
92 |
93 | .. function:: compose(*fs)
94 |
95 | Returns composition of functions::
96 |
97 | extract_int = compose(int, r'\d+')
98 |
99 | Supports :ref:`extended_fns`.
100 |
101 |
102 | .. function:: rcompose(*fs)
103 |
104 | Returns composition of functions, with functions called from left to right. Designed to facilitate transducer-like pipelines::
105 |
106 | # Note the use of iterator function variants everywhere
107 | process = rcompose(
108 | partial(remove, is_useless),
109 | partial(map, process_row),
110 | partial(chunks, 100)
111 | )
112 |
113 | for chunk in process(data):
114 | write_chunk_to_db(chunk)
115 |
116 | Supports :ref:`extended_fns`.
117 |
118 |
119 | .. function:: juxt(*fs)
120 | ljuxt(*fs)
121 |
122 | Takes several functions and returns a new function that is the juxtaposition of those. The resulting function takes a variable number of arguments, and returns an iterator or a list containing the result of applying each function to the arguments.
123 |
124 |
125 | .. function:: iffy([pred], action, [default=identity])
126 |
127 | Returns function, which conditionally, depending on ``pred``, applies ``action`` or ``default``. If ``default`` is not callable then it is returned as is from resulting function. E.g. this will call all callable values leaving rest of them as is::
128 |
129 | map(iffy(callable, caller()), values)
130 |
131 | Common use it to deal with messy data::
132 |
133 | dirty_data = ['hello', None, 'bye']
134 | lmap(iffy(len), dirty_data) # => [5, None, 3]
135 | lmap(iffy(isa(str), len, 0), dirty_data) # => [5, 0, 3], also safer
136 |
137 | See also :func:`silent` for easier use cases.
138 |
139 |
140 | Function logic
141 | --------------
142 |
143 | This family of functions supports creating predicates from other predicates and regular expressions.
144 |
145 |
146 | .. function:: complement(pred)
147 |
148 | Constructs a negation of ``pred``, i.e. a function returning a boolean opposite of original function::
149 |
150 | is_private = re_tester(r'^_')
151 | is_public = complement(is_private)
152 |
153 | # or just
154 | is_public = complement(r'^_')
155 |
156 |
157 | .. function:: all_fn(*fs)
158 | any_fn(*fs)
159 | none_fn(*fs)
160 | one_fn(*fs)
161 |
162 | Construct a predicate returning ``True`` when all, any, none or exactly one of ``fs`` return ``True``. Support short-circuit behavior.
163 |
164 | ::
165 |
166 | is_even_int = all_fn(isa(int), even)
167 |
168 |
169 |
170 | .. function:: some_fn(*fs)
171 |
172 | Constructs function calling ``fs`` one by one and returning first true result.
173 |
174 | Enables creating functions by short-circuiting several behaviours::
175 |
176 | get_amount = some_fn(
177 | lambda s: 4 if 'set of' in s else None,
178 | r'(\d+) wheels?',
179 | compose({'one': 1, 'two': 2, 'pair': 2}, r'(\w+) wheels?')
180 | )
181 |
182 | If you wonder how on Earth one can :func:`compose` dict and string see :ref:`extended_fns`.
183 |
184 |
185 | .. raw:: html
186 | :file: descriptions.html
187 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | Welcome to funcy documentation!
2 | =================================
3 |
4 | Funcy is designed to be a layer of functional tools over python.
5 |
6 | Special topics:
7 |
8 | .. toctree::
9 | :maxdepth: 1
10 |
11 | overview
12 | cheatsheet
13 | extended_fns
14 |
15 |
16 | Contents:
17 |
18 | .. toctree::
19 | :maxdepth: 1
20 |
21 | seqs
22 | colls
23 | funcs
24 | decorators
25 | flow
26 | strings
27 | calc
28 | types
29 | objects
30 | debug
31 | primitives
32 |
33 |
34 | Essays:
35 |
36 | - `Why Every Language Needs Its Underscore `_
37 | - `Functional Python Made Easy `_
38 | - `Abstracting Control Flow `_
39 | - `Painless Decorators `_
40 |
41 |
42 | You can also `look at the code `_ or `create an issue `_.
43 |
--------------------------------------------------------------------------------
/docs/objects.rst:
--------------------------------------------------------------------------------
1 | Objects
2 | =======
3 |
4 | .. decorator:: cached_property
5 |
6 | Creates a property caching its result. This is a great way to lazily attach some data to an object::
7 |
8 | class MyUser(AbstractBaseUser):
9 | @cached_property
10 | def public_phones(self):
11 | return list(self.phones.filter(confirmed=True, public=True))
12 |
13 | One can rewrite cached value simply by assigning and clear cache by deleting it::
14 |
15 | user.public_phones = [...]
16 | del user.public_phones # will be populated on next access
17 |
18 | Note that the last line will raise ``AttributeError`` if cache is not set, to clear cache safely one might use::
19 |
20 | user.__dict__.pop('public_phones')
21 |
22 | **CAVEAT:** only one cached value is stored for each property, so if you call ancestors cached property from outside of corresponding child property it will save ancestors value, which will prevent future evaluations from ever calling child function.
23 |
24 |
25 | .. decorator:: cached_readonly
26 |
27 | Creates a read-only property caching its result. Same as :func:`cached_property` but protected against rewrites.
28 |
29 |
30 | .. decorator:: wrap_prop(ctx)
31 |
32 | Wraps a property accessors with a context manager::
33 |
34 | class SomeConnector:
35 | # We want several threads share this session,
36 | # but only one of them initialize it.
37 | @wrap_prop(threading.Lock())
38 | @cached_property
39 | def session(self):
40 | # ... build a session
41 |
42 | Note that ``@wrap_prop()`` preserves descriptor type, i.e. wrapped cached property may still be rewritten and cleared the same way.
43 |
44 |
45 | .. decorator:: monkey(cls_or_module, name=None)
46 |
47 | Monkey-patches class or module by adding decorated function or property to it named ``name`` or the same as decorated function. Saves overwritten method to ``original`` attribute of decorated function for a kind of inheritance::
48 |
49 | # A simple caching of all get requests,
50 | # even for models for which you can't easily change Manager
51 | @monkey(QuerySet)
52 | def get(self, *args, **kwargs):
53 | if not args and list(kwargs) == ['pk']:
54 | cache_key = '%s:%d' % (self.model, kwargs['pk'])
55 | result = cache.get(cache_key)
56 | if result is None:
57 | result = get.original(self, *args, **kwargs)
58 | cache.set(cache_key, result)
59 | return result
60 | else:
61 | return get.original(self, *args, **kwargs)
62 |
63 |
64 | .. class:: LazyObject(init)
65 |
66 | Creates a object only really setting itself up on first attribute access. Since attribute access happens immediately before any method call, this permits delaying initialization until first call::
67 |
68 | @LazyObject
69 | def redis_client():
70 | if isinstance(settings.REDIS, str):
71 | return StrictRedis.from_url(settings.REDIS)
72 | else:
73 | return StrictRedis(**settings.REDIS)
74 |
75 | # Will be only created on first use
76 | redis_client.set(...)
77 |
78 |
79 | .. raw:: html
80 | :file: descriptions.html
81 |
--------------------------------------------------------------------------------
/docs/overview.rst:
--------------------------------------------------------------------------------
1 | .. _overview:
2 |
3 | Overview
4 | ========
5 |
6 | Start with:
7 |
8 | ::
9 |
10 | pip install funcy
11 |
12 |
13 | .. include:: ../README.rst
14 | :start-after: --------------
15 | :end-before: And `much more `_.
16 |
--------------------------------------------------------------------------------
/docs/primitives.rst:
--------------------------------------------------------------------------------
1 | Primitives
2 | ==========
3 |
4 | .. function:: isnone(x)
5 |
6 | Checks if ``x`` is ``None``. Handy with filtering functions::
7 |
8 | _, data = lsplit_by(isnone, dirty_data) # Skip leading nones
9 |
10 | Plays nice with :func:`silent`, which returns ``None`` on fail::
11 |
12 | remove(isnone, map(silent(int), strings_with_numbers))
13 |
14 | Note that it's usually simpler to use :func:`keep` or :func:`compact` if you don't need to distinguish between ``None`` and other falsy values.
15 |
16 |
17 | .. function:: notnone(x)
18 |
19 | Checks if ``x`` is not ``None``. A shortcut for ``complement(isnone)`` meant to be used when ``bool`` is not specific enough. Compare::
20 |
21 | select_values(notnone, data_dict) # removes None values
22 | compact(data_dict) # removes all falsy values
23 |
24 |
25 | .. function:: inc(x)
26 |
27 | Increments its argument by 1.
28 |
29 |
30 | .. function:: dec(x)
31 |
32 | Decrements its argument by 1.
33 |
34 |
35 | .. function:: even(x)
36 |
37 | Checks if ``x`` is even.
38 |
39 |
40 | .. function:: odd(x)
41 |
42 | Checks if ``x`` is odd.
43 |
44 |
45 | .. raw:: html
46 | :file: descriptions.html
47 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | Sphinx==7.3.7
2 | sphinx-rtd-theme==2.0.0
3 | sphinxcontrib-jquery==4.1
4 |
--------------------------------------------------------------------------------
/docs/strings.rst:
--------------------------------------------------------------------------------
1 | String utils
2 | ============
3 |
4 | .. Prevent text wrap in captures table
5 |
6 | .. raw:: html
7 |
8 |
11 |
12 |
13 | .. function:: re_find(regex, s, flags=0)
14 |
15 | Finds ``regex`` in ``s``, returning the match in the simplest possible form guessed by captures in given regular expression:
16 |
17 | ================================= ==================================
18 | Captures Return value
19 | ================================= ==================================
20 | no captures a matched string
21 | single positional capture a substring matched by capture
22 | only positional captures a tuple of substrings for captures
23 | only named captures a dict of substrings for captures
24 | mixed pos/named captures a match object
25 | ================================= ==================================
26 |
27 | Returns ``None`` on mismatch.
28 |
29 | ::
30 |
31 | # Find first number in a line
32 | silent(int)(re_find(r'\d+', line))
33 |
34 | # Find number of men in a line
35 | re_find(r'(\d+) m[ae]n', line)
36 |
37 | # Parse uri into nice dict
38 | re_find(r'^/post/(?P\d+)/(?P\w+)$', uri)
39 |
40 |
41 | .. function:: re_test(regex, s, flags=0)
42 |
43 | Tests whether ``regex`` can be found in ``s``.
44 |
45 |
46 | .. function:: re_all(regex, s, flags=0)
47 | re_iter(regex, s, flags=0)
48 |
49 | Returns a list or an iterator of all matches of ``regex`` in ``s``. Matches are presented in most simple form possible, see table in :func:`re_find` docs.
50 |
51 | ::
52 |
53 | # A fast and dirty way to parse ini section into dict
54 | dict(re_iter('(\w+)=(\w+)', ini_text))
55 |
56 |
57 | .. function:: re_finder(regex, flags=0)
58 |
59 | Returns a function that calls :func:`re_find` for its sole argument. Its main purpose is quickly constructing mapper functions for :func:`map` and friends.
60 |
61 | See also :ref:`extended_fns`.
62 |
63 |
64 | .. function:: re_tester(regex, flags=0)
65 |
66 | Returns a function that calls :func:`re_test` for it's sole argument. Aimed at quick construction of predicates for use in :func:`filter` and friends.
67 |
68 | See also :ref:`extended_fns`.
69 |
70 |
71 | .. function:: str_join([sep=""], seq)
72 |
73 | Joins sequence with ``sep``. Same as ``sep.join(seq)``, but forcefully converts all elements to separator type, ``str`` by default.
74 |
75 | See also :func:`joining`.
76 |
77 |
78 | .. function:: cut_prefix(s, prefix)
79 |
80 | Cuts prefix from given string if it's present.
81 |
82 |
83 | .. function:: cut_suffix(s, suffix)
84 |
85 | Cuts suffix from given string if it's present.
86 |
87 |
88 | .. raw:: html
89 | :file: descriptions.html
90 |
--------------------------------------------------------------------------------
/docs/types.rst:
--------------------------------------------------------------------------------
1 | Type testing
2 | ============
3 |
4 | .. function:: isa(*types)
5 |
6 | Returns function checking if its argument is of any of given ``types``.
7 |
8 | Split labels from ids::
9 |
10 | labels, ids = lsplit(isa(str), values)
11 |
12 |
13 | .. function:: is_mapping(value)
14 | is_set(value)
15 | is_list(value)
16 | is_tuple(value)
17 | is_seq(value)
18 | is_iter(value)
19 |
20 | These functions check if value is ``Mapping``, ``Set``, ``list``, ``tuple``, ``Sequence`` or iterator respectively.
21 |
22 |
23 | .. function:: is_seqcoll(value)
24 |
25 | Checks if ``value`` is a list or a tuple, which are both sequences and collections.
26 |
27 |
28 | .. function:: is_seqcont(value)
29 |
30 | Checks if ``value`` is a list, a tuple or an iterator, which are sequential containers. It can be used to distinguish between value and multiple values in dual-interface functions::
31 |
32 | def add_to_selection(view, region):
33 | if is_seqcont(region):
34 | # A sequence of regions
35 | view.sel().add_all(region)
36 | else:
37 | view.sel().add(region)
38 |
39 |
40 | .. function:: iterable(value)
41 |
42 | Tests if ``value`` is iterable.
43 |
44 |
45 | .. raw:: html
46 | :file: descriptions.html
47 |
--------------------------------------------------------------------------------
/funcy/__init__.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from .calc import *
4 | from .colls import *
5 | from .tree import *
6 | from .decorators import *
7 | from .funcolls import *
8 | from .funcs import *
9 | from .seqs import *
10 | from .types import *
11 | from .strings import *
12 | from .flow import *
13 | from .objects import *
14 | from .debug import *
15 | from .primitives import *
16 |
17 |
18 | # Setup __all__
19 | modules = ('calc', 'colls', 'tree', 'decorators', 'funcolls', 'funcs', 'seqs', 'types',
20 | 'strings', 'flow', 'objects', 'debug', 'primitives')
21 | __all__ = lcat(sys.modules['funcy.' + m].__all__ for m in modules)
22 |
--------------------------------------------------------------------------------
/funcy/_inspect.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 | from inspect import CO_VARARGS, CO_VARKEYWORDS, signature
3 | from collections import namedtuple
4 | import platform
5 | import re
6 |
7 | from .decorators import unwrap
8 |
9 |
10 | IS_PYPY = platform.python_implementation() == "PyPy"
11 |
12 | # This provides sufficient introspection for *curry() functions.
13 | #
14 | # We only really need a number of required positional arguments.
15 | # If arguments can be specified by name (not true for many builtin functions),
16 | # then we need to now their names to ignore anything else passed by name.
17 | #
18 | # Stars mean some positional argument which can't be passed by name.
19 | # Functions not mentioned here get one star "spec".
20 | ARGS = {}
21 |
22 |
23 | ARGS['builtins'] = {
24 | 'bool': '*',
25 | 'complex': 'real,imag',
26 | 'enumerate': 'iterable,start',
27 | 'file': 'file-**',
28 | 'float': 'x',
29 | 'int': 'x-*',
30 | 'long': 'x-*',
31 | 'open': 'file-**',
32 | 'round': 'number-*',
33 | 'setattr': '***',
34 | 'str': 'object-*',
35 | 'unicode': 'string-**',
36 | '__import__': 'name-****',
37 | '__buildclass__': '***',
38 | # Complex functions with different set of arguments
39 | 'iter': '*-*',
40 | 'format': '*-*',
41 | 'type': '*-**',
42 | }
43 | # Add two argument functions
44 | two_arg_funcs = '''cmp coerce delattr divmod filter getattr hasattr isinstance issubclass
45 | map pow reduce'''
46 | ARGS['builtins'].update(dict.fromkeys(two_arg_funcs.split(), '**'))
47 |
48 |
49 | ARGS['functools'] = {'reduce': '**'}
50 |
51 |
52 | ARGS['itertools'] = {
53 | 'accumulate': 'iterable-*',
54 | 'combinations': 'iterable,r',
55 | 'combinations_with_replacement': 'iterable,r',
56 | 'compress': 'data,selectors',
57 | 'groupby': 'iterable-*',
58 | 'permutations': 'iterable-*',
59 | 'repeat': 'object-*',
60 | }
61 | two_arg_funcs = 'dropwhile filterfalse ifilter ifilterfalse starmap takewhile'
62 | ARGS['itertools'].update(dict.fromkeys(two_arg_funcs.split(), '**'))
63 |
64 |
65 | ARGS['operator'] = {
66 | 'delslice': '***',
67 | 'getslice': '***',
68 | 'setitem': '***',
69 | 'setslice': '****',
70 | }
71 | two_arg_funcs = """
72 | _compare_digest add and_ concat contains countOf delitem div eq floordiv ge getitem
73 | gt iadd iand iconcat idiv ifloordiv ilshift imatmul imod imul indexOf ior ipow irepeat
74 | irshift is_ is_not isub itruediv ixor le lshift lt matmul mod mul ne or_ pow repeat rshift
75 | sequenceIncludes sub truediv xor
76 | """
77 | ARGS['operator'].update(dict.fromkeys(two_arg_funcs.split(), '**'))
78 | ARGS['operator'].update([
79 | ('__%s__' % op.strip('_'), args) for op, args in ARGS['operator'].items()])
80 | ARGS['_operator'] = ARGS['operator']
81 |
82 |
83 | # Fixate this
84 | STD_MODULES = set(ARGS)
85 |
86 |
87 | # Describe some funcy functions, mostly for r?curry()
88 | ARGS['funcy.seqs'] = {
89 | 'map': 'f*', 'lmap': 'f*', 'xmap': 'f*',
90 | 'mapcat': 'f*', 'lmapcat': 'f*',
91 | }
92 | ARGS['funcy.colls'] = {
93 | 'merge_with': 'f*',
94 | }
95 |
96 |
97 | Spec = namedtuple("Spec", "max_n names req_n req_names varkw")
98 |
99 |
100 | def get_spec(func, _cache={}):
101 | func = getattr(func, '__original__', None) or unwrap(func)
102 | try:
103 | return _cache[func]
104 | except (KeyError, TypeError):
105 | pass
106 |
107 | mod = getattr(func, '__module__', None)
108 | if mod in STD_MODULES or mod in ARGS and func.__name__ in ARGS[mod]:
109 | _spec = ARGS[mod].get(func.__name__, '*')
110 | required, _, optional = _spec.partition('-')
111 | req_names = re.findall(r'\w+|\*', required) # a list with dups of *
112 | max_n = len(req_names) + len(optional)
113 | req_n = len(req_names)
114 | spec = Spec(max_n=max_n, names=set(), req_n=req_n, req_names=set(req_names), varkw=False)
115 | _cache[func] = spec
116 | return spec
117 | elif isinstance(func, type):
118 | # __init__ inherited from builtin classes
119 | objclass = getattr(func.__init__, '__objclass__', None)
120 | if objclass and objclass is not func:
121 | return get_spec(objclass)
122 | # Introspect constructor and remove self
123 | spec = get_spec(func.__init__)
124 | self_set = {func.__init__.__code__.co_varnames[0]}
125 | return spec._replace(max_n=spec.max_n - 1, names=spec.names - self_set,
126 | req_n=spec.req_n - 1, req_names=spec.req_names - self_set)
127 | # In PyPy some builtins might have __code__ but no __defaults__, so we fall back to signature
128 | elif not IS_PYPY and hasattr(func, '__code__'):
129 | return _code_to_spec(func)
130 | else:
131 | # We use signature last to be fully backwards compatible. Also it's slower
132 | try:
133 | sig = signature(func)
134 | # import ipdb; ipdb.set_trace()
135 | except (ValueError, TypeError):
136 | raise ValueError('Unable to introspect %s() arguments'
137 | % (getattr(func, '__qualname__', None) or getattr(func, '__name__', func)))
138 | else:
139 | spec = _cache[func] = _sig_to_spec(sig)
140 | return spec
141 |
142 |
143 | def _code_to_spec(func):
144 | code = func.__code__
145 |
146 | # Weird function like objects
147 | defaults = getattr(func, '__defaults__', None)
148 | defaults_n = len(defaults) if isinstance(defaults, tuple) else 0
149 |
150 | kwdefaults = getattr(func, '__kwdefaults__', None)
151 | if not isinstance(kwdefaults, dict):
152 | kwdefaults = {}
153 |
154 | # Python 3.7 and earlier does not have this
155 | posonly_n = getattr(code, 'co_posonlyargcount', 0)
156 |
157 | varnames = code.co_varnames
158 | pos_n = code.co_argcount
159 | n = pos_n + code.co_kwonlyargcount
160 | names = set(varnames[posonly_n:n])
161 | req_n = n - defaults_n - len(kwdefaults)
162 | req_names = set(varnames[posonly_n:pos_n - defaults_n] + varnames[pos_n:n]) - set(kwdefaults)
163 | varkw = bool(code.co_flags & CO_VARKEYWORDS)
164 | # If there are varargs they could be required
165 | max_n = n + 1 if code.co_flags & CO_VARARGS else n
166 | return Spec(max_n=max_n, names=names, req_n=req_n, req_names=req_names, varkw=varkw)
167 |
168 |
169 | def _sig_to_spec(sig):
170 | max_n, names, req_n, req_names, varkw = 0, set(), 0, set(), False
171 | for name, param in sig.parameters.items():
172 | max_n += 1
173 | if param.kind == param.VAR_KEYWORD:
174 | max_n -= 1
175 | varkw = True
176 | elif param.kind == param.VAR_POSITIONAL:
177 | req_n += 1
178 | elif param.kind == param.POSITIONAL_ONLY:
179 | if param.default is param.empty:
180 | req_n += 1
181 | else:
182 | names.add(name)
183 | if param.default is param.empty:
184 | req_n += 1
185 | req_names.add(name)
186 | return Spec(max_n=max_n, names=names, req_n=req_n, req_names=req_names, varkw=varkw)
187 |
--------------------------------------------------------------------------------
/funcy/calc.py:
--------------------------------------------------------------------------------
1 | from datetime import timedelta
2 | import time
3 | import inspect
4 | from collections import deque
5 | from bisect import bisect
6 |
7 | from .decorators import wraps
8 |
9 |
10 | __all__ = ['memoize', 'make_lookuper', 'silent_lookuper', 'cache']
11 |
12 |
13 |
14 | class SkipMemory(Exception):
15 | pass
16 |
17 |
18 | # TODO: use pos-only arg once in Python 3.8+ only
19 | def memoize(_func=None, *, key_func=None):
20 | """@memoize(key_func=None). Makes decorated function memoize its results.
21 |
22 | If key_func is specified uses key_func(*func_args, **func_kwargs) as memory key.
23 | Otherwise uses args + tuple(sorted(kwargs.items()))
24 |
25 | Exposes its memory via .memory attribute.
26 | """
27 | if _func is not None:
28 | return memoize()(_func)
29 | return _memory_decorator({}, key_func)
30 |
31 | memoize.skip = SkipMemory
32 |
33 |
34 | def cache(timeout, *, key_func=None):
35 | """Caches a function results for timeout seconds."""
36 | if isinstance(timeout, timedelta):
37 | timeout = timeout.total_seconds()
38 |
39 | return _memory_decorator(CacheMemory(timeout), key_func)
40 |
41 | cache.skip = SkipMemory
42 |
43 |
44 | def _memory_decorator(memory, key_func):
45 | def decorator(func):
46 | @wraps(func)
47 | def wrapper(*args, **kwargs):
48 | # We inline this here since @memoize also targets microoptimizations
49 | key = key_func(*args, **kwargs) if key_func else \
50 | args + tuple(sorted(kwargs.items())) if kwargs else args
51 | try:
52 | return memory[key]
53 | except KeyError:
54 | try:
55 | value = memory[key] = func(*args, **kwargs)
56 | return value
57 | except SkipMemory as e:
58 | return e.args[0] if e.args else None
59 |
60 | def invalidate(*args, **kwargs):
61 | key = key_func(*args, **kwargs) if key_func else \
62 | args + tuple(sorted(kwargs.items())) if kwargs else args
63 | memory.pop(key, None)
64 | wrapper.invalidate = invalidate
65 |
66 | def invalidate_all():
67 | memory.clear()
68 | wrapper.invalidate_all = invalidate_all
69 |
70 | wrapper.memory = memory
71 | return wrapper
72 | return decorator
73 |
74 | class CacheMemory(dict):
75 | def __init__(self, timeout):
76 | self.timeout = timeout
77 | self.clear()
78 |
79 | def __setitem__(self, key, value):
80 | expires_at = time.time() + self.timeout
81 | dict.__setitem__(self, key, (value, expires_at))
82 | self._keys.append(key)
83 | self._expires.append(expires_at)
84 |
85 | def __getitem__(self, key):
86 | value, expires_at = dict.__getitem__(self, key)
87 | if expires_at <= time.time():
88 | self.expire()
89 | raise KeyError(key)
90 | return value
91 |
92 | def expire(self):
93 | i = bisect(self._expires, time.time())
94 | for _ in range(i):
95 | self._expires.popleft()
96 | self.pop(self._keys.popleft(), None)
97 |
98 | def clear(self):
99 | dict.clear(self)
100 | self._keys = deque()
101 | self._expires = deque()
102 |
103 |
104 | def _make_lookuper(silent):
105 | def make_lookuper(func):
106 | """
107 | Creates a single argument function looking up result in a memory.
108 |
109 | Decorated function is called once on first lookup and should return all available
110 | arg-value pairs.
111 |
112 | Resulting function will raise LookupError when using @make_lookuper
113 | or simply return None when using @silent_lookuper.
114 | """
115 | has_args, has_keys = has_arg_types(func)
116 | assert not has_keys, \
117 | 'Lookup table building function should not have keyword arguments'
118 |
119 | if has_args:
120 | @memoize
121 | def wrapper(*args):
122 | f = lambda: func(*args)
123 | f.__name__ = '%s(%s)' % (func.__name__, ', '.join(map(str, args)))
124 | return make_lookuper(f)
125 | else:
126 | memory = {}
127 |
128 | def wrapper(arg):
129 | if not memory:
130 | memory[object()] = None # prevent continuos memory refilling
131 | memory.update(func())
132 |
133 | if silent:
134 | return memory.get(arg)
135 | elif arg in memory:
136 | return memory[arg]
137 | else:
138 | raise LookupError("Failed to look up %s(%s)" % (func.__name__, arg))
139 |
140 | return wraps(func)(wrapper)
141 | return make_lookuper
142 |
143 | make_lookuper = _make_lookuper(False)
144 | silent_lookuper = _make_lookuper(True)
145 | silent_lookuper.__name__ = 'silent_lookuper'
146 |
147 |
148 | def has_arg_types(func):
149 | params = inspect.signature(func).parameters.values()
150 | return any(p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD, p.VAR_POSITIONAL)
151 | for p in params), \
152 | any(p.kind in (p.KEYWORD_ONLY, p.VAR_KEYWORD) for p in params)
153 |
--------------------------------------------------------------------------------
/funcy/colls.py:
--------------------------------------------------------------------------------
1 | from builtins import all as _all, any as _any
2 | from copy import copy
3 | from operator import itemgetter, methodcaller, attrgetter
4 | from itertools import chain, tee
5 | from collections import defaultdict
6 | from collections.abc import Mapping, Set, Iterable, Iterator
7 |
8 | from .primitives import EMPTY
9 | from .funcs import partial, compose
10 | from .funcmakers import make_func, make_pred
11 | from .seqs import take, map as xmap, filter as xfilter
12 |
13 |
14 | __all__ = ['empty', 'iteritems', 'itervalues',
15 | 'join', 'merge', 'join_with', 'merge_with',
16 | 'walk', 'walk_keys', 'walk_values', 'select', 'select_keys', 'select_values', 'compact',
17 | 'is_distinct', 'all', 'any', 'none', 'one', 'some',
18 | 'zipdict', 'flip', 'project', 'omit', 'zip_values', 'zip_dicts',
19 | 'where', 'pluck', 'pluck_attr', 'invoke', 'lwhere', 'lpluck', 'lpluck_attr', 'linvoke',
20 | 'get_in', 'get_lax', 'set_in', 'update_in', 'del_in', 'has_path']
21 |
22 |
23 | ### Generic ops
24 | FACTORY_REPLACE = {
25 | type(object.__dict__): dict,
26 | type({}.keys()): list,
27 | type({}.values()): list,
28 | type({}.items()): list,
29 | }
30 |
31 | def _factory(coll, mapper=None):
32 | coll_type = type(coll)
33 | # Hack for defaultdicts overridden constructor
34 | if isinstance(coll, defaultdict):
35 | item_factory = compose(mapper, coll.default_factory) if mapper and coll.default_factory \
36 | else coll.default_factory
37 | return partial(defaultdict, item_factory)
38 | elif isinstance(coll, Iterator):
39 | return iter
40 | elif isinstance(coll, (bytes, str)):
41 | return coll_type().join
42 | elif coll_type in FACTORY_REPLACE:
43 | return FACTORY_REPLACE[coll_type]
44 | else:
45 | return coll_type
46 |
47 | def empty(coll):
48 | """Creates an empty collection of the same type."""
49 | if isinstance(coll, Iterator):
50 | return iter([])
51 | return _factory(coll)()
52 |
53 | def iteritems(coll):
54 | return coll.items() if hasattr(coll, 'items') else coll
55 |
56 | def itervalues(coll):
57 | return coll.values() if hasattr(coll, 'values') else coll
58 |
59 | iteritems.__doc__ = "Yields (key, value) pairs of the given collection."
60 | itervalues.__doc__ = "Yields values of the given collection."
61 |
62 |
63 | def join(colls):
64 | """Joins several collections of same type into one."""
65 | colls, colls_copy = tee(colls)
66 | it = iter(colls_copy)
67 | try:
68 | dest = next(it)
69 | except StopIteration:
70 | return None
71 | cls = dest.__class__
72 |
73 | if isinstance(dest, (bytes, str)):
74 | return ''.join(colls)
75 | elif isinstance(dest, Mapping):
76 | result = dest.copy()
77 | for d in it:
78 | result.update(d)
79 | return result
80 | elif isinstance(dest, Set):
81 | return dest.union(*it)
82 | elif isinstance(dest, (Iterator, range)):
83 | return chain.from_iterable(colls)
84 | elif isinstance(dest, Iterable):
85 | # NOTE: this could be reduce(concat, ...),
86 | # more effective for low count
87 | return cls(chain.from_iterable(colls))
88 | else:
89 | raise TypeError("Don't know how to join %s" % cls.__name__)
90 |
91 | def merge(*colls):
92 | """Merges several collections of same type into one.
93 |
94 | Works with dicts, sets, lists, tuples, iterators and strings.
95 | For dicts later values take precedence."""
96 | return join(colls)
97 |
98 |
99 | def join_with(f, dicts, strict=False):
100 | """Joins several dicts, combining values with given function."""
101 | dicts = list(dicts)
102 | if not dicts:
103 | return {}
104 | elif not strict and len(dicts) == 1:
105 | return dicts[0]
106 |
107 | lists = {}
108 | for c in dicts:
109 | for k, v in iteritems(c):
110 | if k in lists:
111 | lists[k].append(v)
112 | else:
113 | lists[k] = [v]
114 |
115 | if f is not list:
116 | # kind of walk_values() inplace
117 | for k, v in iteritems(lists):
118 | lists[k] = f(v)
119 |
120 | return lists
121 |
122 | def merge_with(f, *dicts):
123 | """Merges several dicts, combining values with given function."""
124 | return join_with(f, dicts)
125 |
126 |
127 | def walk(f, coll):
128 | """Walks the collection transforming its elements with f.
129 | Same as map, but preserves coll type."""
130 | return _factory(coll)(xmap(f, iteritems(coll)))
131 |
132 | def walk_keys(f, coll):
133 | """Walks keys of the collection, mapping them with f."""
134 | f = make_func(f)
135 | # NOTE: we use this awkward construct instead of lambda to be Python 3 compatible
136 | def pair_f(pair):
137 | k, v = pair
138 | return f(k), v
139 |
140 | return walk(pair_f, coll)
141 |
142 | def walk_values(f, coll):
143 | """Walks values of the collection, mapping them with f."""
144 | f = make_func(f)
145 | # NOTE: we use this awkward construct instead of lambda to be Python 3 compatible
146 | def pair_f(pair):
147 | k, v = pair
148 | return k, f(v)
149 |
150 | return _factory(coll, mapper=f)(xmap(pair_f, iteritems(coll)))
151 |
152 | # TODO: prewalk, postwalk and friends
153 |
154 | def select(pred, coll):
155 | """Same as filter but preserves coll type."""
156 | return _factory(coll)(xfilter(pred, iteritems(coll)))
157 |
158 | def select_keys(pred, coll):
159 | """Select part of the collection with keys passing pred."""
160 | pred = make_pred(pred)
161 | return select(lambda pair: pred(pair[0]), coll)
162 |
163 | def select_values(pred, coll):
164 | """Select part of the collection with values passing pred."""
165 | pred = make_pred(pred)
166 | return select(lambda pair: pred(pair[1]), coll)
167 |
168 |
169 | def compact(coll):
170 | """Removes falsy values from the collection."""
171 | if isinstance(coll, Mapping):
172 | return select_values(bool, coll)
173 | else:
174 | return select(bool, coll)
175 |
176 |
177 | ### Content tests
178 |
179 | def is_distinct(coll, key=EMPTY):
180 | """Checks if all elements in the collection are different."""
181 | if key is EMPTY:
182 | return len(coll) == len(set(coll))
183 | else:
184 | return len(coll) == len(set(xmap(key, coll)))
185 |
186 |
187 | def all(pred, seq=EMPTY):
188 | """Checks if all items in seq pass pred (or are truthy)."""
189 | if seq is EMPTY:
190 | return _all(pred)
191 | return _all(xmap(pred, seq))
192 |
193 | def any(pred, seq=EMPTY):
194 | """Checks if any item in seq passes pred (or is truthy)."""
195 | if seq is EMPTY:
196 | return _any(pred)
197 | return _any(xmap(pred, seq))
198 |
199 | def none(pred, seq=EMPTY):
200 | """"Checks if none of the items in seq pass pred (or are truthy)."""
201 | return not any(pred, seq)
202 |
203 | def one(pred, seq=EMPTY):
204 | """Checks whether exactly one item in seq passes pred (or is truthy)."""
205 | if seq is EMPTY:
206 | return one(bool, pred)
207 | return len(take(2, xfilter(pred, seq))) == 1
208 |
209 | # Not same as in clojure! returns value found not pred(value)
210 | def some(pred, seq=EMPTY):
211 | """Finds first item in seq passing pred or first that is truthy."""
212 | if seq is EMPTY:
213 | return some(bool, pred)
214 | return next(xfilter(pred, seq), None)
215 |
216 | # TODO: a variant of some that returns mapped value,
217 | # one can use some(map(f, seq)) or first(keep(f, seq)) for now.
218 |
219 | # TODO: vector comparison tests - ascending, descending and such
220 | # def chain_test(compare, seq):
221 | # return all(compare, zip(seq, rest(seq))
222 |
223 | def zipdict(keys, vals):
224 | """Creates a dict with keys mapped to the corresponding vals."""
225 | return dict(zip(keys, vals))
226 |
227 | def flip(mapping):
228 | """Flip passed dict or collection of pairs swapping its keys and values."""
229 | def flip_pair(pair):
230 | k, v = pair
231 | return v, k
232 | return walk(flip_pair, mapping)
233 |
234 | def project(mapping, keys):
235 | """Leaves only given keys in mapping."""
236 | return _factory(mapping)((k, mapping[k]) for k in keys if k in mapping)
237 |
238 | def omit(mapping, keys):
239 | """Removes given keys from mapping."""
240 | return _factory(mapping)((k, v) for k, v in iteritems(mapping) if k not in keys)
241 |
242 | def zip_values(*dicts):
243 | """Yields tuples of corresponding values of several dicts."""
244 | if len(dicts) < 1:
245 | raise TypeError('zip_values expects at least one argument')
246 | keys = set.intersection(*map(set, dicts))
247 | for key in keys:
248 | yield tuple(d[key] for d in dicts)
249 |
250 | def zip_dicts(*dicts):
251 | """Yields tuples like (key, (val1, val2, ...))
252 | for each common key in all given dicts."""
253 | if len(dicts) < 1:
254 | raise TypeError('zip_dicts expects at least one argument')
255 | keys = set.intersection(*map(set, dicts))
256 | for key in keys:
257 | yield key, tuple(d[key] for d in dicts)
258 |
259 | def get_in(coll, path, default=None):
260 | """Returns a value at path in the given nested collection."""
261 | for key in path:
262 | try:
263 | coll = coll[key]
264 | except (KeyError, IndexError):
265 | return default
266 | return coll
267 |
268 | def get_lax(coll, path, default=None):
269 | """Returns a value at path in the given nested collection.
270 | Does not raise on a wrong collection type along the way, but removes default.
271 | """
272 | for key in path:
273 | try:
274 | coll = coll[key]
275 | except (KeyError, IndexError, TypeError):
276 | return default
277 | return coll
278 |
279 | def set_in(coll, path, value):
280 | """Creates a copy of coll with the value set at path."""
281 | return update_in(coll, path, lambda _: value)
282 |
283 | def update_in(coll, path, update, default=None):
284 | """Creates a copy of coll with a value updated at path."""
285 | if not path:
286 | return update(coll)
287 | elif isinstance(coll, list):
288 | copy = coll[:]
289 | # NOTE: there is no auto-vivication for lists
290 | copy[path[0]] = update_in(copy[path[0]], path[1:], update, default)
291 | return copy
292 | else:
293 | copy = coll.copy()
294 | current_default = {} if len(path) > 1 else default
295 | copy[path[0]] = update_in(copy.get(path[0], current_default), path[1:], update, default)
296 | return copy
297 |
298 |
299 | def del_in(coll, path):
300 | """Creates a copy of coll with a nested key or index deleted."""
301 | if not path:
302 | return coll
303 | try:
304 | next_coll = coll[path[0]]
305 | except (KeyError, IndexError):
306 | return coll
307 |
308 | coll_copy = copy(coll)
309 | if len(path) == 1:
310 | del coll_copy[path[0]]
311 | else:
312 | coll_copy[path[0]] = del_in(next_coll, path[1:])
313 | return coll_copy
314 |
315 |
316 | def has_path(coll, path):
317 | """Checks if path exists in the given nested collection."""
318 | for p in path:
319 | try:
320 | coll = coll[p]
321 | except (KeyError, IndexError):
322 | return False
323 | return True
324 |
325 | def lwhere(mappings, **cond):
326 | """Selects mappings containing all pairs in cond."""
327 | return list(where(mappings, **cond))
328 |
329 | def lpluck(key, mappings):
330 | """Lists values for key in each mapping."""
331 | return list(pluck(key, mappings))
332 |
333 | def lpluck_attr(attr, objects):
334 | """Lists values of given attribute of each object."""
335 | return list(pluck_attr(attr, objects))
336 |
337 | def linvoke(objects, name, *args, **kwargs):
338 | """Makes a list of results of the obj.name(*args, **kwargs)
339 | for each object in objects."""
340 | return list(invoke(objects, name, *args, **kwargs))
341 |
342 |
343 | # Iterator versions for python 3 interface
344 |
345 | def where(mappings, **cond):
346 | """Iterates over mappings containing all pairs in cond."""
347 | items = cond.items()
348 | match = lambda m: all(k in m and m[k] == v for k, v in items)
349 | return filter(match, mappings)
350 |
351 | def pluck(key, mappings):
352 | """Iterates over values for key in mappings."""
353 | return map(itemgetter(key), mappings)
354 |
355 | def pluck_attr(attr, objects):
356 | """Iterates over values of given attribute of given objects."""
357 | return map(attrgetter(attr), objects)
358 |
359 | def invoke(objects, name, *args, **kwargs):
360 | """Yields results of the obj.name(*args, **kwargs)
361 | for each object in objects."""
362 | return map(methodcaller(name, *args, **kwargs), objects)
363 |
--------------------------------------------------------------------------------
/funcy/debug.py:
--------------------------------------------------------------------------------
1 | import re
2 | import traceback
3 | from itertools import chain
4 | from functools import partial
5 | from timeit import default_timer as timer
6 |
7 | from .decorators import decorator, wraps, Call
8 |
9 |
10 | __all__ = [
11 | 'tap',
12 | 'log_calls', 'print_calls',
13 | 'log_enters', 'print_enters',
14 | 'log_exits', 'print_exits',
15 | 'log_errors', 'print_errors',
16 | 'log_durations', 'print_durations',
17 | 'log_iter_durations', 'print_iter_durations',
18 | ]
19 |
20 | REPR_LEN = 25
21 |
22 |
23 | def tap(x, label=None):
24 | """Prints x and then returns it."""
25 | if label:
26 | print('%s: %s' % (label, x))
27 | else:
28 | print(x)
29 | return x
30 |
31 |
32 | @decorator
33 | def log_calls(call, print_func, errors=True, stack=True, repr_len=REPR_LEN):
34 | """Logs or prints all function calls,
35 | including arguments, results and raised exceptions."""
36 | signature = signature_repr(call, repr_len)
37 | try:
38 | print_func('Call %s' % signature)
39 | result = call()
40 | # NOTE: using full repr of result
41 | print_func('-> %s from %s' % (smart_repr(result, max_len=None), signature))
42 | return result
43 | except BaseException as e:
44 | if errors:
45 | print_func('-> ' + _format_error(signature, e, stack))
46 | raise
47 |
48 | def print_calls(errors=True, stack=True, repr_len=REPR_LEN):
49 | if callable(errors):
50 | return log_calls(print)(errors)
51 | else:
52 | return log_calls(print, errors, stack, repr_len)
53 | print_calls.__doc__ = log_calls.__doc__
54 |
55 |
56 | @decorator
57 | def log_enters(call, print_func, repr_len=REPR_LEN):
58 | """Logs each entrance to a function."""
59 | print_func('Call %s' % signature_repr(call, repr_len))
60 | return call()
61 |
62 |
63 | def print_enters(repr_len=REPR_LEN):
64 | """Prints on each entrance to a function."""
65 | if callable(repr_len):
66 | return log_enters(print)(repr_len)
67 | else:
68 | return log_enters(print, repr_len)
69 |
70 |
71 | @decorator
72 | def log_exits(call, print_func, errors=True, stack=True, repr_len=REPR_LEN):
73 | """Logs exits from a function."""
74 | signature = signature_repr(call, repr_len)
75 | try:
76 | result = call()
77 | # NOTE: using full repr of result
78 | print_func('-> %s from %s' % (smart_repr(result, max_len=None), signature))
79 | return result
80 | except BaseException as e:
81 | if errors:
82 | print_func('-> ' + _format_error(signature, e, stack))
83 | raise
84 |
85 | def print_exits(errors=True, stack=True, repr_len=REPR_LEN):
86 | """Prints on exits from a function."""
87 | if callable(errors):
88 | return log_exits(print)(errors)
89 | else:
90 | return log_exits(print, errors, stack, repr_len)
91 |
92 |
93 | class LabeledContextDecorator(object):
94 | """
95 | A context manager which also works as decorator, passing call signature as its label.
96 | """
97 | def __init__(self, print_func, label=None, repr_len=REPR_LEN):
98 | self.print_func = print_func
99 | self.label = label
100 | self.repr_len = repr_len
101 |
102 | def __call__(self, label=None, **kwargs):
103 | if callable(label):
104 | return self.decorator(label)
105 | else:
106 | return self.__class__(self.print_func, label, **kwargs)
107 |
108 | def decorator(self, func):
109 | @wraps(func)
110 | def inner(*args, **kwargs):
111 | # Recreate self with a new label so that nested and recursive calls will work
112 | cm = self.__class__.__new__(self.__class__)
113 | cm.__dict__.update(self.__dict__)
114 | cm.label = signature_repr(Call(func, args, kwargs), self.repr_len)
115 | with cm:
116 | return func(*args, **kwargs)
117 | return inner
118 |
119 |
120 | class log_errors(LabeledContextDecorator):
121 | """Logs or prints all errors within a function or block."""
122 | def __init__(self, print_func, label=None, stack=True, repr_len=REPR_LEN):
123 | LabeledContextDecorator.__init__(self, print_func, label=label, repr_len=repr_len)
124 | self.stack = stack
125 |
126 | def __enter__(self):
127 | return self
128 |
129 | def __exit__(self, exc_type, exc_value, tb):
130 | if exc_type:
131 | if self.stack:
132 | exc_message = ''.join(traceback.format_exception(exc_type, exc_value, tb))
133 | else:
134 | exc_message = '%s: %s' % (exc_type.__name__, exc_value)
135 | self.print_func(_format_error(self.label, exc_message, self.stack))
136 |
137 | print_errors = log_errors(print)
138 |
139 |
140 | # Duration utils
141 |
142 | def format_time(sec):
143 | if sec < 1e-6:
144 | return '%8.2f ns' % (sec * 1e9)
145 | elif sec < 1e-3:
146 | return '%8.2f mks' % (sec * 1e6)
147 | elif sec < 1:
148 | return '%8.2f ms' % (sec * 1e3)
149 | else:
150 | return '%8.2f s' % sec
151 |
152 | time_formatters = {
153 | 'auto': format_time,
154 | 'ns': lambda sec: '%8.2f ns' % (sec * 1e9),
155 | 'mks': lambda sec: '%8.2f mks' % (sec * 1e6),
156 | 'ms': lambda sec: '%8.2f ms' % (sec * 1e3),
157 | 's': lambda sec: '%8.2f s' % sec,
158 | }
159 |
160 |
161 | class log_durations(LabeledContextDecorator):
162 | """Times each function call or block execution."""
163 | def __init__(self, print_func, label=None, unit='auto', threshold=-1, repr_len=REPR_LEN):
164 | LabeledContextDecorator.__init__(self, print_func, label=label, repr_len=repr_len)
165 | if unit not in time_formatters:
166 | raise ValueError('Unknown time unit: %s. It should be ns, mks, ms, s or auto.' % unit)
167 | self.format_time = time_formatters[unit]
168 | self.threshold = threshold
169 |
170 | def __enter__(self):
171 | self.start = timer()
172 | return self
173 |
174 | def __exit__(self, *exc):
175 | duration = timer() - self.start
176 | if duration >= self.threshold:
177 | duration_str = self.format_time(duration)
178 | self.print_func("%s in %s" % (duration_str, self.label) if self.label else duration_str)
179 |
180 | print_durations = log_durations(print)
181 |
182 |
183 | def log_iter_durations(seq, print_func, label=None, unit='auto'):
184 | """Times processing of each item in seq."""
185 | if unit not in time_formatters:
186 | raise ValueError('Unknown time unit: %s. It should be ns, mks, ms, s or auto.' % unit)
187 | _format_time = time_formatters[unit]
188 | suffix = " of %s" % label if label else ""
189 | it = iter(seq)
190 | for i, item in enumerate(it):
191 | start = timer()
192 | yield item
193 | duration = _format_time(timer() - start)
194 | print_func("%s in iteration %d%s" % (duration, i, suffix))
195 |
196 | def print_iter_durations(seq, label=None, unit='auto'):
197 | """Times processing of each item in seq."""
198 | return log_iter_durations(seq, print, label, unit=unit)
199 |
200 |
201 | ### Formatting utils
202 |
203 | def _format_error(label, e, stack=True):
204 | if isinstance(e, Exception):
205 | if stack:
206 | e_message = traceback.format_exc()
207 | else:
208 | e_message = '%s: %s' % (e.__class__.__name__, e)
209 | else:
210 | e_message = e
211 |
212 | if label:
213 | template = '%s raised in %s' if stack else '%s raised in %s'
214 | return template % (e_message, label)
215 | else:
216 | return e_message
217 |
218 |
219 | ### Call signature stringification utils
220 |
221 | def signature_repr(call, repr_len=REPR_LEN):
222 | if isinstance(call._func, partial):
223 | if hasattr(call._func.func, '__name__'):
224 | name = '<%s partial>' % call._func.func.__name__
225 | else:
226 | name = ''
227 | else:
228 | name = getattr(call._func, '__name__', '')
229 | args_repr = (smart_repr(arg, repr_len) for arg in call._args)
230 | kwargs_repr = ('%s=%s' % (key, smart_repr(value, repr_len))
231 | for key, value in call._kwargs.items())
232 | return '%s(%s)' % (name, ', '.join(chain(args_repr, kwargs_repr)))
233 |
234 | def smart_repr(value, max_len=REPR_LEN):
235 | if isinstance(value, (bytes, str)):
236 | res = repr(value)
237 | else:
238 | res = str(value)
239 |
240 | res = re.sub(r'\s+', ' ', res)
241 | if max_len and len(res) > max_len:
242 | res = res[:max_len-3] + '...'
243 | return res
244 |
--------------------------------------------------------------------------------
/funcy/decorators.py:
--------------------------------------------------------------------------------
1 | from contextlib import ContextDecorator, contextmanager # reexport these for backwards compat
2 | import functools
3 | from inspect import unwrap # reexport this for backwards compat
4 | import inspect
5 |
6 | from . colls import omit
7 |
8 | __all__ = ['decorator', 'wraps', 'unwrap', 'ContextDecorator', 'contextmanager']
9 |
10 |
11 | def decorator(deco):
12 | """
13 | Transforms a flat wrapper into decorator::
14 |
15 | @decorator
16 | def func(call, methods, content_type=DEFAULT): # These are decorator params
17 | # Access call arg by name
18 | if call.request.method not in methods:
19 | # ...
20 | # Decorated functions and all the arguments are accesible as:
21 | print(call._func, call_args, call._kwargs)
22 | # Finally make a call:
23 | return call()
24 | """
25 | if has_single_arg(deco):
26 | return make_decorator(deco)
27 | elif has_1pos_and_kwonly(deco):
28 | # Any arguments after first become decorator arguments
29 | # And a decorator with arguments is essentially a decorator fab
30 | # TODO: use pos-only arg once in Python 3.8+ only
31 | def decorator_fab(_func=None, **dkwargs):
32 | if _func is not None:
33 | return make_decorator(deco, (), dkwargs)(_func)
34 | return make_decorator(deco, (), dkwargs)
35 | else:
36 | def decorator_fab(*dargs, **dkwargs):
37 | return make_decorator(deco, dargs, dkwargs)
38 |
39 | return wraps(deco)(decorator_fab)
40 |
41 |
42 | def make_decorator(deco, dargs=(), dkwargs={}):
43 | @wraps(deco)
44 | def _decorator(func):
45 | def wrapper(*args, **kwargs):
46 | call = Call(func, args, kwargs)
47 | return deco(call, *dargs, **dkwargs)
48 | return wraps(func)(wrapper)
49 |
50 | # NOTE: should I update name to show args?
51 | # Save these for introspection
52 | _decorator._func, _decorator._args, _decorator._kwargs = deco, dargs, dkwargs
53 | return _decorator
54 |
55 |
56 | class Call(object):
57 | """
58 | A call object to pass as first argument to decorator.
59 |
60 | Call object is just a proxy for decorated function
61 | with call arguments saved in its attributes.
62 | """
63 | def __init__(self, func, args, kwargs):
64 | self._func, self._args, self._kwargs = func, args, kwargs
65 |
66 | def __call__(self, *a, **kw):
67 | if not a and not kw:
68 | return self._func(*self._args, **self._kwargs)
69 | else:
70 | return self._func(*(self._args + a), **dict(self._kwargs, **kw))
71 |
72 | def __getattr__(self, name):
73 | try:
74 | res = self.__dict__[name] = arggetter(self._func)(name, self._args, self._kwargs)
75 | return res
76 | except TypeError as e:
77 | raise AttributeError(*e.args)
78 |
79 | def __str__(self):
80 | func = getattr(self._func, '__qualname__', str(self._func))
81 | args = ", ".join(list(map(str, self._args)) + ["%s=%s" % t for t in self._kwargs.items()])
82 | return "%s(%s)" % (func, args)
83 |
84 | def __repr__(self):
85 | return "" % self
86 |
87 |
88 | def has_single_arg(func):
89 | sig = inspect.signature(func)
90 | if len(sig.parameters) != 1:
91 | return False
92 | arg = next(iter(sig.parameters.values()))
93 | return arg.kind not in (arg.VAR_POSITIONAL, arg.VAR_KEYWORD)
94 |
95 | def has_1pos_and_kwonly(func):
96 | from collections import Counter
97 | from inspect import Parameter as P
98 |
99 | sig = inspect.signature(func)
100 | kinds = Counter(p.kind for p in sig.parameters.values())
101 | return kinds[P.POSITIONAL_ONLY] + kinds[P.POSITIONAL_OR_KEYWORD] == 1 \
102 | and kinds[P.VAR_POSITIONAL] == 0
103 |
104 | def get_argnames(func):
105 | func = getattr(func, '__original__', None) or unwrap(func)
106 | return func.__code__.co_varnames[:func.__code__.co_argcount]
107 |
108 | def arggetter(func, _cache={}):
109 | if func in _cache:
110 | return _cache[func]
111 |
112 | original = getattr(func, '__original__', None) or unwrap(func)
113 | code = original.__code__
114 |
115 | # Instrospect pos and kw names
116 | posnames = code.co_varnames[:code.co_argcount]
117 | n = code.co_argcount
118 | kwonlynames = code.co_varnames[n:n + code.co_kwonlyargcount]
119 | n += code.co_kwonlyargcount
120 | # TODO: remove this check once we drop Python 3.7
121 | if hasattr(code, 'co_posonlyargcount'):
122 | kwnames = posnames[code.co_posonlyargcount:] + kwonlynames
123 | else:
124 | kwnames = posnames + kwonlynames
125 |
126 | varposname = varkwname = None
127 | if code.co_flags & inspect.CO_VARARGS:
128 | varposname = code.co_varnames[n]
129 | n += 1
130 | if code.co_flags & inspect.CO_VARKEYWORDS:
131 | varkwname = code.co_varnames[n]
132 |
133 | allnames = set(code.co_varnames)
134 | indexes = {name: i for i, name in enumerate(posnames)}
135 | defaults = {}
136 | if original.__defaults__:
137 | defaults.update(zip(posnames[-len(original.__defaults__):], original.__defaults__))
138 | if original.__kwdefaults__:
139 | defaults.update(original.__kwdefaults__)
140 |
141 | def get_arg(name, args, kwargs):
142 | if name not in allnames:
143 | raise TypeError("%s() doesn't have argument named %s" % (func.__name__, name))
144 |
145 | index = indexes.get(name)
146 | if index is not None and index < len(args):
147 | return args[index]
148 | elif name in kwargs and name in kwnames:
149 | return kwargs[name]
150 | elif name == varposname:
151 | return args[len(posnames):]
152 | elif name == varkwname:
153 | return omit(kwargs, kwnames)
154 | elif name in defaults:
155 | return defaults[name]
156 | else:
157 | raise TypeError("%s() missing required argument: '%s'" % (func.__name__, name))
158 |
159 | _cache[func] = get_arg
160 | return get_arg
161 |
162 |
163 | ### Add __original__ to update_wrapper and @wraps
164 |
165 | def update_wrapper(wrapper,
166 | wrapped,
167 | assigned = functools.WRAPPER_ASSIGNMENTS,
168 | updated = functools.WRAPPER_UPDATES):
169 | functools.update_wrapper(wrapper, wrapped, assigned, updated)
170 |
171 | # Set an original ref for faster and more convenient access
172 | wrapper.__original__ = getattr(wrapped, '__original__', None) or unwrap(wrapped)
173 |
174 | return wrapper
175 |
176 | update_wrapper.__doc__ = functools.update_wrapper.__doc__
177 |
178 |
179 | def wraps(wrapped,
180 | assigned = functools.WRAPPER_ASSIGNMENTS,
181 | updated = functools.WRAPPER_UPDATES):
182 | return functools.partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)
183 |
184 | wraps.__doc__ = functools.wraps.__doc__
185 |
--------------------------------------------------------------------------------
/funcy/flow.py:
--------------------------------------------------------------------------------
1 | from collections.abc import Hashable
2 | from datetime import datetime, timedelta
3 | import time
4 | import threading
5 | from contextlib import suppress # reexport
6 |
7 | from .decorators import decorator, wraps, get_argnames, arggetter, contextmanager
8 |
9 |
10 | __all__ = ['raiser', 'ignore', 'silent', 'suppress', 'nullcontext', 'reraise', 'retry', 'fallback',
11 | 'limit_error_rate', 'ErrorRateExceeded', 'throttle',
12 | 'post_processing', 'collecting', 'joining',
13 | 'once', 'once_per', 'once_per_args',
14 | 'wrap_with']
15 |
16 |
17 | ### Error handling utilities
18 |
19 | def raiser(exception_or_class=Exception, *args, **kwargs):
20 | """Constructs function that raises the given exception
21 | with given arguments on any invocation."""
22 | if isinstance(exception_or_class, str):
23 | exception_or_class = Exception(exception_or_class)
24 |
25 | def _raiser(*a, **kw):
26 | if args or kwargs:
27 | raise exception_or_class(*args, **kwargs)
28 | else:
29 | raise exception_or_class
30 | return _raiser
31 |
32 |
33 | # Not using @decorator here for speed,
34 | # since @ignore and @silent should be used for very simple and fast functions
35 | def ignore(errors, default=None):
36 | """Alters function to ignore given errors, returning default instead."""
37 | errors = _ensure_exceptable(errors)
38 |
39 | def decorator(func):
40 | @wraps(func)
41 | def wrapper(*args, **kwargs):
42 | try:
43 | return func(*args, **kwargs)
44 | except errors:
45 | return default
46 | return wrapper
47 | return decorator
48 |
49 | def silent(func):
50 | """Alters function to ignore all exceptions."""
51 | return ignore(Exception)(func)
52 |
53 |
54 | ### Backport of Python 3.7 nullcontext
55 | try:
56 | from contextlib import nullcontext
57 | except ImportError:
58 | class nullcontext(object):
59 | """Context manager that does no additional processing.
60 |
61 | Used as a stand-in for a normal context manager, when a particular
62 | block of code is only sometimes used with a normal context manager:
63 |
64 | cm = optional_cm if condition else nullcontext()
65 | with cm:
66 | # Perform operation, using optional_cm if condition is True
67 | """
68 |
69 | def __init__(self, enter_result=None):
70 | self.enter_result = enter_result
71 |
72 | def __enter__(self):
73 | return self.enter_result
74 |
75 | def __exit__(self, *excinfo):
76 | pass
77 |
78 |
79 | @contextmanager
80 | def reraise(errors, into):
81 | """Reraises errors as other exception."""
82 | errors = _ensure_exceptable(errors)
83 | try:
84 | yield
85 | except errors as e:
86 | if callable(into) and not _is_exception_type(into):
87 | into = into(e)
88 | raise into from e
89 |
90 |
91 | @decorator
92 | def retry(call, tries, errors=Exception, timeout=0, filter_errors=None):
93 | """Makes decorated function retry up to tries times.
94 | Retries only on specified errors.
95 | Sleeps timeout or timeout(attempt) seconds between tries."""
96 | errors = _ensure_exceptable(errors)
97 | for attempt in range(tries):
98 | try:
99 | return call()
100 | except errors as e:
101 | if not (filter_errors is None or filter_errors(e)):
102 | raise
103 |
104 | # Reraise error on last attempt
105 | if attempt + 1 == tries:
106 | raise
107 | else:
108 | timeout_value = timeout(attempt) if callable(timeout) else timeout
109 | if timeout_value > 0:
110 | time.sleep(timeout_value)
111 |
112 |
113 | def fallback(*approaches):
114 | """Tries several approaches until one works.
115 | Each approach has a form of (callable, expected_errors)."""
116 | for approach in approaches:
117 | func, catch = (approach, Exception) if callable(approach) else approach
118 | catch = _ensure_exceptable(catch)
119 | try:
120 | return func()
121 | except catch:
122 | pass
123 |
124 | def _ensure_exceptable(errors):
125 | """Ensures that errors are passable to except clause.
126 | I.e. should be BaseException subclass or a tuple."""
127 | return errors if _is_exception_type(errors) else tuple(errors)
128 |
129 |
130 | def _is_exception_type(value):
131 | return isinstance(value, type) and issubclass(value, BaseException)
132 |
133 |
134 | class ErrorRateExceeded(Exception):
135 | pass
136 |
137 | def limit_error_rate(fails, timeout, exception=ErrorRateExceeded):
138 | """If function fails to complete fails times in a row,
139 | calls to it will be intercepted for timeout with exception raised instead."""
140 | if isinstance(timeout, int):
141 | timeout = timedelta(seconds=timeout)
142 |
143 | def decorator(func):
144 | @wraps(func)
145 | def wrapper(*args, **kwargs):
146 | if wrapper.blocked:
147 | if datetime.now() - wrapper.blocked < timeout:
148 | raise exception
149 | else:
150 | wrapper.blocked = None
151 |
152 | try:
153 | result = func(*args, **kwargs)
154 | except: # noqa
155 | wrapper.fails += 1
156 | if wrapper.fails >= fails:
157 | wrapper.blocked = datetime.now()
158 | raise
159 | else:
160 | wrapper.fails = 0
161 | return result
162 |
163 | wrapper.fails = 0
164 | wrapper.blocked = None
165 | return wrapper
166 | return decorator
167 |
168 |
169 | def throttle(period):
170 | """Allows only one run in a period, the rest is skipped"""
171 | if isinstance(period, timedelta):
172 | period = period.total_seconds()
173 |
174 | def decorator(func):
175 |
176 | @wraps(func)
177 | def wrapper(*args, **kwargs):
178 | now = time.time()
179 | if wrapper.blocked_until and wrapper.blocked_until > now:
180 | return
181 | wrapper.blocked_until = now + period
182 |
183 | return func(*args, **kwargs)
184 |
185 | wrapper.blocked_until = None
186 | return wrapper
187 |
188 | return decorator
189 |
190 |
191 | ### Post processing decorators
192 |
193 | @decorator
194 | def post_processing(call, func):
195 | """Post processes decorated function result with func."""
196 | return func(call())
197 |
198 | collecting = post_processing(list)
199 | collecting.__name__ = 'collecting'
200 | collecting.__doc__ = "Transforms a generator into list returning function."
201 |
202 | @decorator
203 | def joining(call, sep):
204 | """Joins decorated function results with sep."""
205 | return sep.join(map(sep.__class__, call()))
206 |
207 |
208 | ### Initialization helpers
209 |
210 | def once_per(*argnames):
211 | """Call function only once for every combination of the given arguments."""
212 | def once(func):
213 | lock = threading.Lock()
214 | done_set = set()
215 | done_list = list()
216 |
217 | get_arg = arggetter(func)
218 |
219 | @wraps(func)
220 | def wrapper(*args, **kwargs):
221 | with lock:
222 | values = tuple(get_arg(name, args, kwargs) for name in argnames)
223 | if isinstance(values, Hashable):
224 | done, add = done_set, done_set.add
225 | else:
226 | done, add = done_list, done_list.append
227 |
228 | if values not in done:
229 | add(values)
230 | return func(*args, **kwargs)
231 | return wrapper
232 | return once
233 |
234 | once = once_per()
235 | once.__doc__ = "Let function execute once, noop all subsequent calls."
236 |
237 | def once_per_args(func):
238 | """Call function once for every combination of values of its arguments."""
239 | return once_per(*get_argnames(func))(func)
240 |
241 |
242 | @decorator
243 | def wrap_with(call, ctx):
244 | """Turn context manager into a decorator"""
245 | with ctx:
246 | return call()
247 |
--------------------------------------------------------------------------------
/funcy/funcmakers.py:
--------------------------------------------------------------------------------
1 | from collections.abc import Mapping, Set
2 | from operator import itemgetter
3 |
4 | from .strings import re_tester, re_finder, _re_type
5 |
6 |
7 | __all__ = ('make_func', 'make_pred')
8 |
9 |
10 | def make_func(f, test=False):
11 | if callable(f):
12 | return f
13 | elif f is None:
14 | # pass None to builtin as predicate or mapping function for speed
15 | return bool if test else lambda x: x
16 | elif isinstance(f, (bytes, str, _re_type)):
17 | return re_tester(f) if test else re_finder(f)
18 | elif isinstance(f, (int, slice)):
19 | return itemgetter(f)
20 | elif isinstance(f, Mapping):
21 | return f.__getitem__
22 | elif isinstance(f, Set):
23 | return f.__contains__
24 | else:
25 | raise TypeError("Can't make a func from %s" % f.__class__.__name__)
26 |
27 | def make_pred(pred):
28 | return make_func(pred, test=True)
29 |
--------------------------------------------------------------------------------
/funcy/funcolls.py:
--------------------------------------------------------------------------------
1 | from .funcs import compose, juxt
2 | from .colls import some, none, one
3 |
4 |
5 | __all__ = ['all_fn', 'any_fn', 'none_fn', 'one_fn', 'some_fn']
6 |
7 |
8 | def all_fn(*fs):
9 | """Constructs a predicate, which holds when all fs hold."""
10 | return compose(all, juxt(*fs))
11 |
12 | def any_fn(*fs):
13 | """Constructs a predicate, which holds when any fs holds."""
14 | return compose(any, juxt(*fs))
15 |
16 | def none_fn(*fs):
17 | """Constructs a predicate, which holds when none of fs hold."""
18 | return compose(none, juxt(*fs))
19 |
20 | def one_fn(*fs):
21 | """Constructs a predicate, which holds when exactly one of fs holds."""
22 | return compose(one, juxt(*fs))
23 |
24 | def some_fn(*fs):
25 | """Constructs a function, which calls fs one by one
26 | and returns first truthy result."""
27 | return compose(some, juxt(*fs))
28 |
--------------------------------------------------------------------------------
/funcy/funcs.py:
--------------------------------------------------------------------------------
1 | from operator import __not__
2 | from functools import partial, reduce, wraps
3 |
4 | from ._inspect import get_spec, Spec
5 | from .primitives import EMPTY
6 | from .funcmakers import make_func, make_pred
7 |
8 |
9 | __all__ = ['identity', 'constantly', 'caller',
10 | # reexport functools for convenience
11 | 'reduce', 'partial',
12 | 'rpartial', 'func_partial',
13 | 'curry', 'rcurry', 'autocurry',
14 | 'iffy',
15 | 'compose', 'rcompose', 'complement', 'juxt', 'ljuxt']
16 |
17 |
18 | def identity(x):
19 | """Returns its argument."""
20 | return x
21 |
22 | def constantly(x):
23 | """Creates a function accepting any args, but always returning x."""
24 | return lambda *a, **kw: x
25 |
26 | # an operator.methodcaller() brother
27 | def caller(*a, **kw):
28 | """Creates a function calling its sole argument with given *a, **kw."""
29 | return lambda f: f(*a, **kw)
30 |
31 | def func_partial(func, *args, **kwargs):
32 | """A functools.partial alternative, which returns a real function.
33 | Can be used to construct methods."""
34 | return lambda *a, **kw: func(*(args + a), **dict(kwargs, **kw))
35 |
36 | def rpartial(func, *args, **kwargs):
37 | """Partially applies last arguments.
38 | New keyworded arguments extend and override kwargs."""
39 | return lambda *a, **kw: func(*(a + args), **dict(kwargs, **kw))
40 |
41 |
42 | def curry(func, n=EMPTY):
43 | """Curries func into a chain of one argument functions."""
44 | if n is EMPTY:
45 | n = get_spec(func).max_n
46 |
47 | if n <= 1:
48 | return func
49 | elif n == 2:
50 | return lambda x: lambda y: func(x, y)
51 | else:
52 | return lambda x: curry(partial(func, x), n - 1)
53 |
54 |
55 | def rcurry(func, n=EMPTY):
56 | """Curries func into a chain of one argument functions.
57 | Arguments are passed from right to left."""
58 | if n is EMPTY:
59 | n = get_spec(func).max_n
60 |
61 | if n <= 1:
62 | return func
63 | elif n == 2:
64 | return lambda x: lambda y: func(y, x)
65 | else:
66 | return lambda x: rcurry(rpartial(func, x), n - 1)
67 |
68 |
69 | def autocurry(func, n=EMPTY, _spec=None, _args=(), _kwargs={}):
70 | """Creates a version of func returning its partial applications
71 | until sufficient arguments are passed."""
72 | spec = _spec or (get_spec(func) if n is EMPTY else Spec(n, set(), n, set(), False))
73 |
74 | @wraps(func)
75 | def autocurried(*a, **kw):
76 | args = _args + a
77 | kwargs = _kwargs.copy()
78 | kwargs.update(kw)
79 |
80 | if not spec.varkw and len(args) + len(kwargs) >= spec.max_n:
81 | return func(*args, **kwargs)
82 | elif len(args) + len(set(kwargs) & spec.names) >= spec.max_n:
83 | return func(*args, **kwargs)
84 | elif len(args) + len(set(kwargs) & spec.req_names) >= spec.req_n:
85 | try:
86 | return func(*args, **kwargs)
87 | except TypeError:
88 | return autocurry(func, _spec=spec, _args=args, _kwargs=kwargs)
89 | else:
90 | return autocurry(func, _spec=spec, _args=args, _kwargs=kwargs)
91 |
92 | return autocurried
93 |
94 |
95 | def iffy(pred, action=EMPTY, default=identity):
96 | """Creates a function, which conditionally applies action or default."""
97 | if action is EMPTY:
98 | return iffy(bool, pred, default)
99 | else:
100 | pred = make_pred(pred)
101 | action = make_func(action)
102 | return lambda v: action(v) if pred(v) else \
103 | default(v) if callable(default) else \
104 | default
105 |
106 |
107 | def compose(*fs):
108 | """Composes passed functions."""
109 | if fs:
110 | pair = lambda f, g: lambda *a, **kw: f(g(*a, **kw))
111 | return reduce(pair, map(make_func, fs))
112 | else:
113 | return identity
114 |
115 | def rcompose(*fs):
116 | """Composes functions, calling them from left to right."""
117 | return compose(*reversed(fs))
118 |
119 | def complement(pred):
120 | """Constructs a complementary predicate."""
121 | return compose(__not__, pred)
122 |
123 |
124 | # NOTE: using lazy map in these two will result in empty list/iterator
125 | # from all calls to i?juxt result since map iterator will be depleted
126 |
127 | def ljuxt(*fs):
128 | """Constructs a juxtaposition of the given functions.
129 | Result returns a list of results of fs."""
130 | extended_fs = list(map(make_func, fs))
131 | return lambda *a, **kw: [f(*a, **kw) for f in extended_fs]
132 |
133 | def juxt(*fs):
134 | """Constructs a lazy juxtaposition of the given functions.
135 | Result returns an iterator of results of fs."""
136 | extended_fs = list(map(make_func, fs))
137 | return lambda *a, **kw: (f(*a, **kw) for f in extended_fs)
138 |
--------------------------------------------------------------------------------
/funcy/objects.py:
--------------------------------------------------------------------------------
1 | from inspect import isclass, ismodule
2 |
3 | from .strings import cut_prefix
4 |
5 |
6 | __all__ = ['cached_property', 'cached_readonly', 'wrap_prop', 'monkey', 'LazyObject']
7 |
8 |
9 | class cached_property(object):
10 | """
11 | Decorator that converts a method with a single self argument into
12 | a property cached on the instance.
13 | """
14 | # NOTE: implementation borrowed from Django.
15 | # NOTE: we use fget, fset and fdel attributes to mimic @property.
16 | fset = fdel = None
17 |
18 | def __init__(self, fget):
19 | self.fget = fget
20 | self.__doc__ = getattr(fget, '__doc__')
21 |
22 | def __get__(self, instance, type=None):
23 | if instance is None:
24 | return self
25 | res = instance.__dict__[self.fget.__name__] = self.fget(instance)
26 | return res
27 |
28 |
29 | class cached_readonly(cached_property):
30 | """Same as @cached_property, but protected against rewrites."""
31 | def __set__(self, instance, value):
32 | raise AttributeError("property is read-only")
33 |
34 |
35 | def wrap_prop(ctx):
36 | """Wrap a property accessors with a context manager"""
37 | def decorator(prop):
38 | class WrapperProp(object):
39 | def __repr__(self):
40 | return repr(prop)
41 |
42 | def __get__(self, instance, type=None):
43 | if instance is None:
44 | return self
45 |
46 | with ctx:
47 | return prop.__get__(instance, type)
48 |
49 | if hasattr(prop, '__set__'):
50 | def __set__(self, name, value):
51 | with ctx:
52 | return prop.__set__(name, value)
53 |
54 | if hasattr(prop, '__del__'):
55 | def __del__(self, name):
56 | with ctx:
57 | return prop.__del__(name)
58 |
59 | return WrapperProp()
60 | return decorator
61 |
62 |
63 | def monkey(cls, name=None):
64 | """
65 | Monkey patches class or module by adding to it decorated function.
66 |
67 | Anything overwritten could be accessed via .original attribute of decorated object.
68 | """
69 | assert isclass(cls) or ismodule(cls), "Attempting to monkey patch non-class and non-module"
70 |
71 | def decorator(value):
72 | func = getattr(value, 'fget', value) # Support properties
73 | func_name = name or cut_prefix(func.__name__, '%s__' % cls.__name__)
74 |
75 | func.__name__ = func_name
76 | func.original = getattr(cls, func_name, None)
77 |
78 | setattr(cls, func_name, value)
79 | return value
80 | return decorator
81 |
82 |
83 | # TODO: monkey_mix()?
84 |
85 |
86 | class LazyObject(object):
87 | """
88 | A simplistic lazy init object.
89 | Rewrites itself when any attribute is accessed.
90 | """
91 | # NOTE: we can add lots of magic methods here to intercept on more events,
92 | # this is postponed. As well as metaclass to support isinstance() check.
93 | def __init__(self, init):
94 | self.__dict__['_init'] = init
95 |
96 | def _setup(self):
97 | obj = self._init()
98 | object.__setattr__(self, '__class__', obj.__class__)
99 | object.__setattr__(self, '__dict__', obj.__dict__)
100 |
101 | def __getattr__(self, name):
102 | self._setup()
103 | return getattr(self, name)
104 |
105 | def __setattr__(self, name, value):
106 | self._setup()
107 | return setattr(self, name, value)
108 |
--------------------------------------------------------------------------------
/funcy/primitives.py:
--------------------------------------------------------------------------------
1 | __all__ = ['isnone', 'notnone', 'inc', 'dec', 'even', 'odd']
2 |
3 |
4 | class EmptyType:
5 | def __repr__(self):
6 | return 'EMPTY'
7 |
8 | EMPTY = EmptyType() # Used as unique default for optional arguments
9 |
10 |
11 | def isnone(x):
12 | return x is None
13 |
14 | def notnone(x):
15 | return x is not None
16 |
17 |
18 | def inc(x):
19 | return x + 1
20 |
21 | def dec(x):
22 | return x - 1
23 |
24 | def even(x):
25 | return x % 2 == 0
26 |
27 | def odd(x):
28 | return x % 2 == 1
29 |
--------------------------------------------------------------------------------
/funcy/strings.py:
--------------------------------------------------------------------------------
1 | import re
2 | from operator import methodcaller
3 |
4 | from .primitives import EMPTY
5 |
6 |
7 | __all__ = ['re_iter', 're_all', 're_find', 're_finder', 're_test', 're_tester',
8 | 'str_join',
9 | 'cut_prefix', 'cut_suffix']
10 |
11 |
12 | def _make_getter(regex):
13 | if regex.groups == 0:
14 | return methodcaller('group')
15 | elif regex.groups == 1 and regex.groupindex == {}:
16 | return methodcaller('group', 1)
17 | elif regex.groupindex == {}:
18 | return methodcaller('groups')
19 | elif regex.groups == len(regex.groupindex):
20 | return methodcaller('groupdict')
21 | else:
22 | return lambda m: m
23 |
24 | _re_type = type(re.compile(r'')) # re.Pattern was added in Python 3.7
25 |
26 | def _prepare(regex, flags):
27 | if not isinstance(regex, _re_type):
28 | regex = re.compile(regex, flags)
29 | return regex, _make_getter(regex)
30 |
31 |
32 | def re_iter(regex, s, flags=0):
33 | """Iterates over matches of regex in s, presents them in simplest possible form"""
34 | regex, getter = _prepare(regex, flags)
35 | return map(getter, regex.finditer(s))
36 |
37 | def re_all(regex, s, flags=0):
38 | """Lists all matches of regex in s, presents them in simplest possible form"""
39 | return list(re_iter(regex, s, flags))
40 |
41 | def re_find(regex, s, flags=0):
42 | """Matches regex against the given string,
43 | returns the match in the simplest possible form."""
44 | return re_finder(regex, flags)(s)
45 |
46 | def re_test(regex, s, flags=0):
47 | """Tests whether regex matches against s."""
48 | return re_tester(regex, flags)(s)
49 |
50 |
51 | def re_finder(regex, flags=0):
52 | """Creates a function finding regex in passed string."""
53 | regex, _getter = _prepare(regex, flags)
54 | getter = lambda m: _getter(m) if m else None
55 | return lambda s: getter(regex.search(s))
56 |
57 | def re_tester(regex, flags=0):
58 | """Creates a predicate testing passed string with regex."""
59 | if not isinstance(regex, _re_type):
60 | regex = re.compile(regex, flags)
61 | return lambda s: bool(regex.search(s))
62 |
63 |
64 | def str_join(sep, seq=EMPTY):
65 | """Joins the given sequence with sep.
66 | Forces stringification of seq items."""
67 | if seq is EMPTY:
68 | return str_join('', sep)
69 | else:
70 | return sep.join(map(sep.__class__, seq))
71 |
72 | def cut_prefix(s, prefix):
73 | """Cuts prefix from given string if it's present."""
74 | return s[len(prefix):] if s.startswith(prefix) else s
75 |
76 | def cut_suffix(s, suffix):
77 | """Cuts suffix from given string if it's present."""
78 | return s[:-len(suffix)] if s.endswith(suffix) else s
79 |
--------------------------------------------------------------------------------
/funcy/tree.py:
--------------------------------------------------------------------------------
1 | from collections import deque
2 | from .types import is_seqcont
3 |
4 |
5 | __all__ = ['tree_leaves', 'ltree_leaves', 'tree_nodes', 'ltree_nodes']
6 |
7 |
8 | def tree_leaves(root, follow=is_seqcont, children=iter):
9 | """Iterates over tree leaves."""
10 | q = deque([[root]])
11 | while q:
12 | node_iter = iter(q.pop())
13 | for sub in node_iter:
14 | if follow(sub):
15 | q.append(node_iter)
16 | q.append(children(sub))
17 | break
18 | else:
19 | yield sub
20 |
21 | def ltree_leaves(root, follow=is_seqcont, children=iter):
22 | """Lists tree leaves."""
23 | return list(tree_leaves(root, follow, children))
24 |
25 |
26 | def tree_nodes(root, follow=is_seqcont, children=iter):
27 | """Iterates over all tree nodes."""
28 | q = deque([[root]])
29 | while q:
30 | node_iter = iter(q.pop())
31 | for sub in node_iter:
32 | yield sub
33 | if follow(sub):
34 | q.append(node_iter)
35 | q.append(children(sub))
36 | break
37 |
38 | def ltree_nodes(root, follow=is_seqcont, children=iter):
39 | """Lists all tree nodes."""
40 | return list(tree_nodes(root, follow, children))
41 |
--------------------------------------------------------------------------------
/funcy/types.py:
--------------------------------------------------------------------------------
1 | from collections.abc import Mapping, Set, Sequence, Iterator, Iterable
2 |
3 |
4 | __all__ = ('isa', 'is_mapping', 'is_set', 'is_seq', 'is_list', 'is_tuple',
5 | 'is_seqcoll', 'is_seqcont',
6 | 'iterable', 'is_iter')
7 |
8 |
9 | def isa(*types):
10 | """
11 | Creates a function checking if its argument
12 | is of any of given types.
13 | """
14 | return lambda x: isinstance(x, types)
15 |
16 | is_mapping = isa(Mapping)
17 | is_set = isa(Set)
18 | is_seq = isa(Sequence)
19 | is_list = isa(list)
20 | is_tuple = isa(tuple)
21 |
22 | is_seqcoll = isa(list, tuple)
23 | is_seqcont = isa(list, tuple, Iterator, range)
24 |
25 | iterable = isa(Iterable)
26 | is_iter = isa(Iterator)
27 |
--------------------------------------------------------------------------------
/publish.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/bash
2 |
3 | set -ex
4 |
5 | NAME=funcy
6 | VERSION=`cat VERSION`
7 |
8 | python setup.py sdist bdist_wheel
9 | twine check dist/$NAME-$VERSION*
10 | twine upload --skip-existing -uSuor dist/$NAME-$VERSION*
11 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bdist_wheel]
2 | universal = 1
3 |
4 | [metadata]
5 | license_file = LICENSE
6 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 |
4 | # Remove build status
5 | README = open('README.rst').read().replace('|Build Status|', '', 1)
6 |
7 |
8 | setup(
9 | name='funcy',
10 | version=open('VERSION').read().strip(),
11 | author='Alexander Schepanovski',
12 | author_email='suor.web@gmail.com',
13 |
14 | description='A fancy and practical functional tools',
15 | long_description=README,
16 | long_description_content_type="text/x-rst",
17 | url='http://github.com/Suor/funcy',
18 | license='BSD',
19 |
20 | packages=['funcy'],
21 |
22 | classifiers=[
23 | 'Development Status :: 5 - Production/Stable',
24 | 'License :: OSI Approved :: BSD License',
25 | 'Operating System :: OS Independent',
26 | 'Programming Language :: Python',
27 | 'Programming Language :: Python :: 3',
28 | 'Programming Language :: Python :: 3.4',
29 | 'Programming Language :: Python :: 3.5',
30 | 'Programming Language :: Python :: 3.6',
31 | 'Programming Language :: Python :: 3.7',
32 | 'Programming Language :: Python :: 3.8',
33 | 'Programming Language :: Python :: 3.9',
34 | 'Programming Language :: Python :: 3.10',
35 | 'Programming Language :: Python :: 3.11',
36 | 'Programming Language :: Python :: 3.12',
37 | 'Programming Language :: Python :: Implementation :: CPython',
38 | 'Programming Language :: Python :: Implementation :: PyPy',
39 |
40 | 'Topic :: Software Development :: Libraries :: Python Modules',
41 | 'Intended Audience :: Developers',
42 | ]
43 | )
44 |
--------------------------------------------------------------------------------
/test_requirements.txt:
--------------------------------------------------------------------------------
1 | pytest==7.4.3; python_version>='3.7'
2 | pytest==6.2.5; python_version=='3.6'
3 | pytest==3.9.3; python_version<='3.5'
4 | more-itertools==4.0.0; python_version=='3.5'
5 | whatever==0.7
6 | typing; python_version=='3.4'
7 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Suor/funcy/207a7810c216c7408596d463d3f429686e83b871/tests/__init__.py
--------------------------------------------------------------------------------
/tests/py38_decorators.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from funcy.decorators import decorator
3 |
4 |
5 | def test_decorator_access_args():
6 | @decorator
7 | def return_x(call):
8 | return call.x
9 |
10 | # no arg
11 | with pytest.raises(AttributeError): return_x(lambda y: None)(10)
12 |
13 | # pos arg
14 | assert return_x(lambda x: None)(10) == 10
15 | with pytest.raises(AttributeError): return_x(lambda x: None)()
16 | assert return_x(lambda x=11: None)(10) == 10
17 | assert return_x(lambda x=11: None)() == 11
18 |
19 | # pos-only
20 | assert return_x(lambda x, /: None)(10) == 10
21 | with pytest.raises(AttributeError): return_x(lambda x, /: None)()
22 | assert return_x(lambda x=11, /: None)(10) == 10
23 | assert return_x(lambda x=11, /: None)() == 11
24 | # try to pass by name
25 | with pytest.raises(AttributeError): return_x(lambda x, /: None)(x=10)
26 | assert return_x(lambda x=11, /: None)(x=10) == 11
27 |
28 | # kw-only
29 | assert return_x(lambda _, /, *, x: None)(x=10) == 10
30 | with pytest.raises(AttributeError): return_x(lambda _, /, *, x: None)()
31 | assert return_x(lambda _, /, *, x=11: None)(x=10) == 10
32 | assert return_x(lambda _, /, *, x=11: None)() == 11
33 |
34 | # varargs
35 | assert return_x(lambda *x: None)(1, 2) == (1, 2)
36 | assert return_x(lambda _, *x: None)(1, 2) == (2,)
37 |
38 | # varkeywords
39 | assert return_x(lambda **x: None)(a=1, b=2) == {'a': 1, 'b': 2}
40 | assert return_x(lambda **x: None)(a=1, x=3) == {'a': 1, 'x': 3} # Not just 3
41 | assert return_x(lambda a, **x: None)(a=1, b=2) == {'b': 2}
42 | assert return_x(lambda a, /, **x: None)(a=1, b=2) == {'a': 1, 'b': 2}
43 |
--------------------------------------------------------------------------------
/tests/py38_funcs.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from funcy.funcs import autocurry
3 |
4 |
5 | def test_autocurry_posonly():
6 | at = autocurry(lambda a, /, b: (a, b))
7 | assert at(1)(b=2) == (1, 2)
8 | assert at(b=2)(1) == (1, 2)
9 | with pytest.raises(TypeError): at(a=1)(b=2)
10 |
11 | at = autocurry(lambda a, /, **kw: (a, kw))
12 | assert at(a=2)(1) == (1, {'a': 2})
13 |
14 | at = autocurry(lambda a=1, /, *, b: (a, b))
15 | assert at(b=2) == (1, 2)
16 | assert at(0)(b=3) == (0, 3)
17 |
--------------------------------------------------------------------------------
/tests/test_calc.py:
--------------------------------------------------------------------------------
1 | from math import sin, cos
2 | from datetime import timedelta
3 | import pytest
4 |
5 | from funcy.calc import *
6 |
7 |
8 | def test_memoize():
9 | @memoize
10 | def inc(x):
11 | calls.append(x)
12 | return x + 1
13 |
14 | calls = []
15 | assert inc(0) == 1
16 | assert inc(1) == 2
17 | assert inc(0) == 1
18 | assert calls == [0, 1]
19 |
20 | # using kwargs
21 | assert inc(x=0) == 1
22 | assert inc(x=1) == 2
23 | assert inc(x=0) == 1
24 | assert calls == [0, 1, 0, 1]
25 |
26 |
27 | def test_memoize_args_kwargs():
28 | @memoize
29 | def mul(x, by=1):
30 | calls.append((x, by))
31 | return x * by
32 |
33 | calls = []
34 | assert mul(0) == 0
35 | assert mul(1) == 1
36 | assert mul(0) == 0
37 | assert calls == [(0, 1), (1, 1)]
38 |
39 | # more with kwargs
40 | assert mul(0, 1) == 0
41 | assert mul(1, 1) == 1
42 | assert mul(0, 1) == 0
43 | assert calls == [(0, 1), (1, 1), (0, 1), (1, 1)]
44 |
45 |
46 | def test_memoize_skip():
47 | @memoize
48 | def inc(x):
49 | calls.append(x)
50 | if x == 2:
51 | raise memoize.skip
52 | if x == 3:
53 | raise memoize.skip(42)
54 | return x + 1
55 |
56 | calls = []
57 | assert inc(1) == 2
58 | assert inc(2) is None
59 | assert inc(2) is None
60 | assert inc(3) == 42
61 | assert inc(3) == 42
62 | assert calls == [1, 2, 2, 3, 3]
63 |
64 |
65 | def test_memoize_memory():
66 | @memoize
67 | def inc(x):
68 | calls.append(x)
69 | return x + 1
70 |
71 | calls = []
72 | inc(0)
73 | inc.memory.clear()
74 | inc(0)
75 | assert calls == [0, 0]
76 |
77 |
78 | def test_memoize_key_func():
79 | @memoize(key_func=len)
80 | def inc(s):
81 | calls.append(s)
82 | return s * 2
83 |
84 | calls = []
85 | assert inc('a') == 'aa'
86 | assert inc('b') == 'aa'
87 | inc('ab')
88 | assert calls == ['a', 'ab']
89 |
90 |
91 | def test_make_lookuper():
92 | @make_lookuper
93 | def letter_index():
94 | return ((c, i) for i, c in enumerate('abcdefghij'))
95 |
96 | assert letter_index('c') == 2
97 | with pytest.raises(LookupError): letter_index('_')
98 |
99 |
100 | def test_make_lookuper_nested():
101 | tables_built = [0]
102 |
103 | @make_lookuper
104 | def function_table(f):
105 | tables_built[0] += 1
106 | return ((x, f(x)) for x in range(10))
107 |
108 | assert function_table(sin)(5) == sin(5)
109 | assert function_table(cos)(3) == cos(3)
110 | assert function_table(sin)(3) == sin(3)
111 | assert tables_built[0] == 2
112 |
113 | with pytest.raises(LookupError): function_table(cos)(-1)
114 |
115 |
116 | def test_silent_lookuper():
117 | @silent_lookuper
118 | def letter_index():
119 | return ((c, i) for i, c in enumerate('abcdefghij'))
120 |
121 | assert letter_index('c') == 2
122 | assert letter_index('_') is None
123 |
124 |
125 | def test_silnent_lookuper_nested():
126 | @silent_lookuper
127 | def function_table(f):
128 | return ((x, f(x)) for x in range(10))
129 |
130 | assert function_table(sin)(5) == sin(5)
131 | assert function_table(cos)(-1) is None
132 |
133 |
134 | @pytest.mark.parametrize('typ',
135 | [pytest.param(int, id='int'), pytest.param(lambda s: timedelta(seconds=s), id='timedelta')])
136 | def test_cache(typ):
137 | calls = []
138 |
139 | @cache(timeout=typ(60))
140 | def inc(x):
141 | calls.append(x)
142 | return x + 1
143 |
144 | assert inc(0) == 1
145 | assert inc(1) == 2
146 | assert inc(0) == 1
147 | assert calls == [0, 1]
148 |
149 |
150 | def test_cache_mixed_args():
151 | @cache(timeout=60)
152 | def add(x, y):
153 | return x + y
154 |
155 | assert add(1, y=2) == 3
156 |
157 |
158 | def test_cache_timedout():
159 | calls = []
160 |
161 | @cache(timeout=0)
162 | def inc(x):
163 | calls.append(x)
164 | return x + 1
165 |
166 | assert inc(0) == 1
167 | assert inc(1) == 2
168 | assert inc(0) == 1
169 | assert calls == [0, 1, 0]
170 | assert len(inc.memory) == 1 # Both call should be erased then one added
171 |
172 |
173 | def test_cache_invalidate():
174 | calls = []
175 |
176 | @cache(timeout=60)
177 | def inc(x):
178 | calls.append(x)
179 | return x + 1
180 |
181 | assert inc(0) == 1
182 | assert inc(1) == 2
183 | assert inc(0) == 1
184 | assert calls == [0, 1]
185 |
186 | inc.invalidate_all()
187 | assert inc(0) == 1
188 | assert inc(1) == 2
189 | assert inc(0) == 1
190 | assert calls == [0, 1, 0, 1]
191 |
192 | inc.invalidate(1)
193 | assert inc(0) == 1
194 | assert inc(1) == 2
195 | assert inc(0) == 1
196 | assert calls == [0, 1, 0, 1, 1]
197 |
198 | # ensure invalidate() is idempotent (doesn't raise KeyError on the 2nd call)
199 | inc.invalidate(0)
200 | inc.invalidate(0)
201 |
--------------------------------------------------------------------------------
/tests/test_colls.py:
--------------------------------------------------------------------------------
1 | from collections.abc import Iterator
2 | from collections import defaultdict, namedtuple
3 | from itertools import chain, count
4 | import pytest
5 | from whatever import _
6 |
7 | from funcy.colls import *
8 |
9 |
10 | # Utilities
11 | def eq(a, b):
12 | return type(a) is type(b) and a == b \
13 | and (a.default_factory == b.default_factory if isinstance(a, defaultdict) else True)
14 |
15 | def inc(x):
16 | return x + 1
17 |
18 | def hinc(xs):
19 | return map(inc, xs)
20 |
21 |
22 | def test_empty():
23 | assert eq(empty({'a': 1}), {})
24 | assert eq(empty(defaultdict(int)), defaultdict(int))
25 | assert empty(defaultdict(int)).default_factory == defaultdict(int).default_factory
26 |
27 | def test_empty_iter():
28 | it = empty(iter([]))
29 | assert isinstance(it, Iterator)
30 | assert list(it) == []
31 |
32 | def test_empty_quirks():
33 | class A:
34 | FLAG = 1
35 | assert empty(A.__dict__) == {}
36 | assert empty({}.keys()) == []
37 | assert empty({}.values()) == []
38 | assert empty({}.items()) == []
39 |
40 |
41 | def test_iteritems():
42 | assert list(iteritems([1,2])) == [1,2]
43 | assert list(iteritems((1,2))) == [1,2]
44 | assert list(iteritems({'a': 1})) == [('a', 1)]
45 |
46 | def test_itervalues():
47 | assert list(itervalues([1,2])) == [1,2]
48 | assert list(itervalues((1,2))) == [1,2]
49 | assert list(itervalues({'a': 1})) == [1]
50 |
51 |
52 | def test_merge():
53 | assert eq(merge({1: 2}, {3: 4}), {1: 2, 3: 4})
54 |
55 | def test_join():
56 | assert join([]) is None
57 | with pytest.raises(TypeError): join([1])
58 | assert eq(join(['ab', '', 'cd']), 'abcd')
59 | assert eq(join([['a', 'b'], 'c']), list('abc'))
60 | assert eq(join([('a', 'b'), ('c',)]), tuple('abc'))
61 | assert eq(join([{'a': 1}, {'b': 2}]), {'a': 1, 'b': 2})
62 | assert eq(join([{'a': 1}, {'a': 2}]), {'a': 2})
63 | assert eq(join([{1,2}, {3}]), {1,2,3})
64 |
65 | it1 = (x for x in range(2))
66 | it2 = (x for x in range(5, 7))
67 | joined = join([it1, it2])
68 | assert isinstance(joined, Iterator) and list(joined) == [0,1,5,6]
69 |
70 | dd1 = defaultdict(int, a=1)
71 | dd2 = defaultdict(int, b=2)
72 | assert eq(join([dd1, dd2]), defaultdict(int, a=1, b=2))
73 |
74 | def test_join_iter():
75 | assert join(iter('abc')) == 'abc'
76 | assert join(iter([[1], [2]])) == [1, 2]
77 | assert eq(join(iter([{'a': 1}, {'b': 2}])), {'a': 1, 'b': 2})
78 | assert eq(join(iter([{1,2}, {3}])), {1,2,3})
79 |
80 | it1 = (x for x in range(2))
81 | it2 = (x for x in range(5, 7))
82 | chained = join(iter([it1, it2]))
83 | assert isinstance(chained, Iterator) and list(chained) == [0,1,5,6]
84 |
85 |
86 | def test_merge_with():
87 | assert merge_with(list, {1: 1}, {1: 10, 2: 2}) == {1: [1, 10], 2: [2]}
88 | assert merge_with(sum, {1: 1}, {1: 10, 2: 2}) == {1: 11, 2: 2}
89 | # Also works for collection of pairs
90 | assert merge_with(sum, {1: 1}, {1: 10, 2: 2}.items()) == {1: 11, 2: 2}
91 |
92 | def test_join_with():
93 | assert join_with(sum, ({n % 3: n} for n in range(5))) == {0: 3, 1: 5, 2: 2}
94 | assert join_with(list, [{1: 1}]) == {1: 1}
95 | assert join_with(list, [{1: 1}], strict=True) == {1: [1]}
96 |
97 |
98 | def test_walk():
99 | assert eq(walk(inc, [1,2,3]), [2,3,4])
100 | assert eq(walk(inc, (1,2,3)), (2,3,4))
101 | assert eq(walk(inc, {1,2,3}), {2,3,4})
102 | assert eq(walk(hinc, {1:1,2:2,3:3}), {2:2,3:3,4:4})
103 |
104 | def test_walk_iter():
105 | it = walk(inc, chain([0], [1, 2]))
106 | assert isinstance(it, Iterator) and list(it) == [1,2,3]
107 |
108 | it = walk(inc, (i for i in [0,1,2]))
109 | assert isinstance(it, Iterator) and list(it) == [1,2,3]
110 |
111 | def test_walk_extended():
112 | assert walk(None, {2, 3}) == {2, 3}
113 | assert walk(r'\d+', {'a2', '13b'}) == {'2', '13'}
114 | assert walk({'a': '1', 'b': '2'}, 'ab') == '12'
115 | assert walk({1, 2, 3}, (0, 1, 2)) == (False, True, True)
116 |
117 | def test_walk_keys():
118 | assert walk_keys(str.upper, {'a': 1, 'b':2}) == {'A': 1, 'B': 2}
119 | assert walk_keys(r'\d', {'a1': 1, 'b2': 2}) == {'1': 1, '2': 2}
120 |
121 | def test_walk_values():
122 | assert walk_values(_ * 2, {'a': 1, 'b': 2}) == {'a': 2, 'b': 4}
123 | assert walk_values(r'\d', {1: 'a1', 2: 'b2'}) == {1: '1', 2: '2'}
124 |
125 | def test_walk_values_defaultdict():
126 | dd = defaultdict(lambda: 'hey', {1: 'a', 2: 'ab'})
127 | walked_dd = walk_values(len, dd)
128 | assert walked_dd == {1: 1, 2: 2}
129 | # resulting default factory should be compose(len, lambda: 'hey')
130 | assert walked_dd[0] == 3
131 |
132 |
133 | def test_select():
134 | assert eq(select(_ > 1, [1,2,3]), [2,3])
135 | assert eq(select(_ > 1, (1,2,3)), (2,3))
136 | assert eq(select(_ > 1, {1,2,3}), {2,3})
137 | assert eq(select(_[1] > 1, {'a':1,'b':2,'c':3}), {'b':2,'c':3})
138 | assert select(_[1] > 1, defaultdict(int)) == {}
139 |
140 | def test_select_extended():
141 | assert select(None, [2, 3, 0]) == [2, 3]
142 | assert select(r'\d', 'a23bn45') == '2345'
143 | assert select({1,2,3}, (0, 1, 2, 4, 1)) == (1, 2, 1)
144 |
145 | def test_select_keys():
146 | assert select_keys(_[0] == 'a', {'a':1, 'b':2, 'ab':3}) == {'a': 1, 'ab':3}
147 | assert select_keys(r'^a', {'a':1, 'b':2, 'ab':3, 'ba': 4}) == {'a': 1, 'ab':3}
148 |
149 | def test_select_values():
150 | assert select_values(_ % 2, {'a': 1, 'b': 2}) == {'a': 1}
151 | assert select_values(r'a', {1: 'a', 2: 'b'}) == {1: 'a'}
152 |
153 |
154 | def test_compact():
155 | assert eq(compact([0, 1, None, 3]), [1, 3])
156 | assert eq(compact((0, 1, None, 3)), (1, 3))
157 | assert eq(compact({'a': None, 'b': 0, 'c': 1}), {'c': 1})
158 |
159 |
160 | def test_is_distinct():
161 | assert is_distinct('abc')
162 | assert not is_distinct('aba')
163 | assert is_distinct(['a', 'ab', 'abc'], key=len)
164 | assert not is_distinct(['ab', 'cb', 'ad'], key=0)
165 |
166 |
167 | def test_all():
168 | assert all([1,2,3])
169 | assert not all([1,2,''])
170 | assert all(callable, [abs, open, int])
171 | assert not all(_ < 3, [1,2,5])
172 |
173 | def test_all_extended():
174 | assert all(None, [1,2,3])
175 | assert not all(None, [1,2,''])
176 | assert all(r'\d', '125')
177 | assert not all(r'\d', '12.5')
178 |
179 | def test_any():
180 | assert any([0, False, 3, ''])
181 | assert not any([0, False, ''])
182 | assert any(_ > 0, [1,2,0])
183 | assert not any(_ < 0, [1,2,0])
184 |
185 | def test_one():
186 | assert one([0, False, 3, ''])
187 | assert not one([0, False, ''])
188 | assert not one([1, False, 'a'])
189 | assert one(_ > 0, [0,1])
190 | assert not one(_ < 0, [0,1,2])
191 | assert not one(_ > 0, [0,1,2])
192 |
193 | def test_none():
194 | assert none([0, False])
195 | assert not none(_ < 0, [0, -1])
196 |
197 | def test_some():
198 | assert some([0, '', 2, 3]) == 2
199 | assert some(_ > 3, range(10)) == 4
200 |
201 |
202 | def test_zipdict():
203 | assert zipdict([1, 2], 'ab') == {1: 'a', 2:'b'}
204 | assert zipdict('ab', count()) == {'a': 0, 'b': 1}
205 |
206 | def test_flip():
207 | assert flip({'a':1, 'b':2}) == {1:'a', 2:'b'}
208 |
209 | def test_project():
210 | assert project({'a':1, 'b':2, 'c': 3}, 'ac') == {'a':1, 'c': 3}
211 | dd = defaultdict(int, {'a':1, 'b':2, 'c': 3})
212 | assert eq(project(dd, 'ac'), defaultdict(int, {'a':1, 'c': 3}))
213 |
214 | def test_omit():
215 | assert omit({'a': 1, 'b': 2, 'c': 3}, 'ac') == {'b': 2}
216 | dd = defaultdict(int, {'a': 1, 'b': 2, 'c': 3})
217 | assert eq(omit(dd, 'ac'), defaultdict(int, {'b': 2}))
218 |
219 | def test_zip_values():
220 | assert list(zip_values({1: 10}, {1: 20, 2: 30})) == [(10, 20)]
221 | with pytest.raises(TypeError): list(zip_values())
222 |
223 | def test_zip_dicts():
224 | assert list(zip_dicts({1: 10}, {1: 20, 2: 30})) == [(1, (10, 20))]
225 | with pytest.raises(TypeError): list(zip_dicts())
226 |
227 |
228 | @pytest.mark.parametrize("get", [get_in, get_lax])
229 | def test_get(get):
230 | d = {
231 | "a": {
232 | "b": "c",
233 | "f": {"g": "h"}
234 | },
235 | "i": "j"
236 | }
237 | assert get(d, ["i"]) == "j"
238 | assert get(d, ["a", "b"]) == "c"
239 | assert get(d, ["a", "f", "g"]) == "h"
240 | assert get(d, ["m"]) is None
241 | assert get(d, ["a", "n"]) is None
242 | assert get(d, ["m", "n"], "foo") == "foo"
243 |
244 | @pytest.mark.parametrize("get", [get_in, get_lax])
245 | def test_get_list(get):
246 | assert get([1, 2], [0]) == 1
247 | assert get([1, 2], [3]) is None
248 | assert get({'x': [1, 2]}, ['x', 1]) == 2
249 |
250 | def test_get_error():
251 | with pytest.raises(TypeError): get_in([1, 2], ['a'])
252 | assert get_lax([1, 2], ['a']) is None
253 | assert get_lax([1, 2], ['a'], 'foo') == 'foo'
254 |
255 | with pytest.raises(TypeError): get_in('abc', [2, 'a'])
256 | assert get_lax('abc', [2, 'a']) is None
257 |
258 | with pytest.raises(TypeError): get_in(None, ['a', 'b'])
259 | assert get_lax({'a': None}, ['a', 'b']) is None
260 |
261 |
262 | def test_set_in():
263 | d = {
264 | 'a': {
265 | 'b': 1,
266 | 'c': 2,
267 | },
268 | 'd': 5
269 | }
270 |
271 | d2 = set_in(d, ['a', 'c'], 7)
272 | assert d['a']['c'] == 2
273 | assert d2['a']['c'] == 7
274 |
275 | d3 = set_in(d, ['e', 'f'], 42)
276 | assert d3['e'] == {'f': 42}
277 | assert d3['a'] is d['a']
278 |
279 | def test_set_in_list():
280 | l = [{}, 1]
281 | l2 = set_in(l, [1], 7)
282 | assert l2 == [{}, 7]
283 | assert l2[0] is l[0]
284 |
285 | def test_update_in():
286 | d = {'c': []}
287 |
288 | assert update_in(d, ['c'], len) == {'c': 0}
289 |
290 | d2 = update_in(d, ['a', 'b'], inc, default=0)
291 | assert d2['a']['b'] == 1
292 | assert d2['c'] is d['c']
293 |
294 | def test_del_in():
295 | d = {'c': [1, 2, 3]}
296 |
297 | assert del_in(d, []) is d
298 | assert del_in(d, ['a', 'b']) is d
299 | assert del_in(d, ['c', 1]) == {'c': [1, 3]}
300 | with pytest.raises(TypeError): del_in(d, ['c', 'b'])
301 |
302 | def test_has_path():
303 | d = {
304 | "a": {
305 | "b": "c",
306 | "d": "e",
307 | "f": {
308 | "g": "h"
309 | }
310 | },
311 | "i": "j"
312 | }
313 |
314 | assert has_path(d, [])
315 | assert not has_path(d, ["m"])
316 | assert not has_path(d, ["m", "n"])
317 | assert has_path(d, ("i",))
318 | assert has_path(d, ("a", "b"))
319 | assert has_path(d, ["a", "f", "g"])
320 |
321 | def test_has_path_list():
322 | assert has_path([1, 2], [0])
323 | assert not has_path([1, 2], [3])
324 | assert has_path({'x': [1, 2]}, ['x', 1])
325 |
326 | def test_where():
327 | data = [{'a': 1, 'b': 2}, {'a': 10, 'b': 2}]
328 | assert isinstance(where(data, a=1), Iterator)
329 | assert list(where(data, a=1)) == [{'a': 1, 'b': 2}]
330 |
331 | def test_lwhere():
332 | data = [{'a': 1, 'b': 2}, {'a': 10, 'b': 2}]
333 | assert lwhere(data, a=1, b=2) == [{'a': 1, 'b': 2}]
334 | assert lwhere(data, b=2) == data
335 |
336 | # Test non-existent key
337 | assert lwhere(data, c=1) == []
338 |
339 | def test_pluck():
340 | data = [{'a': 1, 'b': 2}, {'a': 10, 'b': 2}]
341 | assert lpluck('a', data) == [1, 10]
342 |
343 | def test_pluck_attr():
344 | TestObj = namedtuple('TestObj', ('id', 'name'))
345 | objs = [TestObj(1, 'test1'), TestObj(5, 'test2'), TestObj(10, 'test3')]
346 | assert lpluck_attr('id', objs) == [1, 5, 10]
347 |
348 | def test_invoke():
349 | assert linvoke(['abc', 'def', 'b'], 'find', 'b') == [1, -1, 0]
350 |
--------------------------------------------------------------------------------
/tests/test_debug.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from funcy.debug import *
4 | from funcy.flow import silent
5 | from funcy.seqs import lmap
6 |
7 |
8 | def test_tap():
9 | assert capture(tap, 42) == '42\n'
10 | assert capture(tap, 42, label='Life and ...') == 'Life and ...: 42\n'
11 |
12 |
13 | def test_log_calls():
14 | log = []
15 |
16 | @log_calls(log.append)
17 | def f(x, y):
18 | return x + y
19 |
20 | f(1, 2)
21 | f('a', 'b')
22 | assert log == [
23 | "Call f(1, 2)",
24 | "-> 3 from f(1, 2)",
25 | "Call f('a', 'b')",
26 | "-> 'ab' from f('a', 'b')",
27 | ]
28 |
29 | def test_print_calls():
30 | def f(x, y):
31 | return x + y
32 |
33 | capture(print_calls(f), 1, 2) == "Call f(1, 2)\n-> 3 from f(1, 2)\n",
34 | capture(print_calls()(f), 1, 2) == "Call f(1, 2)\n-> 3 from f(1, 2)\n",
35 |
36 |
37 | def test_log_calls_raise():
38 | log = []
39 |
40 | @log_calls(log.append, stack=False)
41 | def f():
42 | raise Exception('something bad')
43 |
44 | silent(f)()
45 | assert log == [
46 | "Call f()",
47 | "-> Exception: something bad raised in f()",
48 | ]
49 |
50 |
51 | def test_log_errors():
52 | log = []
53 |
54 | @log_errors(log.append)
55 | def f(x):
56 | return 1 / x
57 |
58 | silent(f)(1)
59 | silent(f)(0)
60 | assert len(log) == 1
61 | assert log[0].startswith('Traceback')
62 | assert re.search(r'ZeroDivisionError: .*\n raised in f\(0\)$', log[0])
63 |
64 |
65 | def test_log_errors_manager():
66 | log = []
67 | try:
68 | with log_errors(log.append):
69 | 1 / 0
70 | except ZeroDivisionError:
71 | pass
72 | try:
73 | with log_errors(log.append, 'name check', stack=False):
74 | hey
75 | except NameError:
76 | pass
77 | assert len(log) == 2
78 | print(log)
79 | assert log[0].startswith('Traceback')
80 | assert re.search(r'ZeroDivisionError: .* zero\s*$', log[0])
81 | assert not log[1].startswith('Traceback')
82 | assert re.search(r"NameError: (global )?name 'hey' is not defined raised in name check", log[1])
83 |
84 |
85 | def test_print_errors():
86 | def error():
87 | 1 / 0
88 |
89 | f = print_errors(error)
90 | assert f.__name__ == 'error'
91 | assert 'ZeroDivisionError' in capture(silent(f))
92 |
93 | g = print_errors(stack=False)(error)
94 | assert g.__name__ == 'error'
95 | assert capture(silent(g)).startswith('ZeroDivisionError')
96 |
97 |
98 | def test_print_errors_manager():
99 | @silent
100 | def f():
101 | with print_errors:
102 | 1 / 0
103 |
104 | assert 'ZeroDivisionError' in capture(f)
105 | assert capture(f).startswith('Traceback')
106 |
107 |
108 | def test_print_errors_recursion():
109 | @silent
110 | @print_errors(stack=False)
111 | def f(n):
112 | if n:
113 | f(0)
114 | 1 / 0
115 |
116 | assert 'f(1)' in capture(f, 1)
117 |
118 |
119 | def test_log_durations(monkeypatch):
120 | timestamps = iter([0, 0.01, 1, 1.000025])
121 | monkeypatch.setattr('funcy.debug.timer', lambda: next(timestamps))
122 | log = []
123 |
124 | f = log_durations(log.append)(lambda: None)
125 | f()
126 | with log_durations(log.append, 'hello'):
127 | pass
128 |
129 | assert lmap(r'^\s*(\d+\.\d+ mk?s) in (?:\(\)|hello)$', log) == ['10.00 ms', '25.00 mks']
130 |
131 |
132 | def test_log_durations_ex(monkeypatch):
133 | timestamps = [0, 0.01, 1, 1.001, 2, 2.02]
134 | timestamps_iter = iter(timestamps)
135 | monkeypatch.setattr('funcy.debug.timer', lambda: next(timestamps_iter))
136 | log = []
137 |
138 | f = log_durations(log.append, unit='ms', threshold=1.1e-3)(lambda: None)
139 | f(); f(); f()
140 |
141 | assert len(log) == 2
142 | assert lmap(r'^\s*(\d+\.\d+) ms in', log) == ['10.00', '20.00']
143 |
144 |
145 | def test_log_iter_dirations():
146 | log = []
147 | for item in log_iter_durations([1, 2], log.append):
148 | pass
149 | assert len(log) == 2
150 |
151 |
152 | ### An utility to capture stdout
153 |
154 | import sys
155 | try:
156 | from cStringIO import StringIO
157 | except ImportError:
158 | from io import StringIO
159 |
160 | def capture(command, *args, **kwargs):
161 | out, sys.stdout = sys.stdout, StringIO()
162 | try:
163 | command(*args, **kwargs)
164 | sys.stdout.seek(0)
165 | return sys.stdout.read()
166 | finally:
167 | sys.stdout = out
168 |
--------------------------------------------------------------------------------
/tests/test_decorators.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import pytest
3 | from funcy.decorators import *
4 |
5 |
6 | def test_decorator_no_args():
7 | @decorator
8 | def inc(call):
9 | return call() + 1
10 |
11 | @inc
12 | def ten():
13 | return 10
14 |
15 | assert ten() == 11
16 |
17 |
18 | def test_decorator_with_args():
19 | @decorator
20 | def add(call, n):
21 | return call() + n
22 |
23 | @add(2)
24 | def ten():
25 | return 10
26 |
27 | assert ten() == 12
28 |
29 |
30 | def test_decorator_kw_only_args():
31 | @decorator
32 | def add(call, *, n=1):
33 | return call() + n
34 |
35 | def ten(a, b):
36 | return 10
37 |
38 | # Should work with or without parentheses
39 | assert add(n=2)(ten)(1, 2) == 12
40 | assert add()(ten)(1, 2) == 11
41 | assert add(ten)(1, 2) == 11
42 |
43 |
44 | # TODO: replace this with a full version once we drop Python 3.7
45 | def test_decorator_access_args():
46 | @decorator
47 | def return_x(call):
48 | return call.x
49 |
50 | # no arg
51 | with pytest.raises(AttributeError): return_x(lambda y: None)(10)
52 |
53 | # pos arg
54 | assert return_x(lambda x: None)(10) == 10
55 | with pytest.raises(AttributeError): return_x(lambda x: None)()
56 | assert return_x(lambda x=11: None)(10) == 10
57 | assert return_x(lambda x=11: None)() == 11
58 |
59 | # varargs
60 | assert return_x(lambda *x: None)(1, 2) == (1, 2)
61 | assert return_x(lambda _, *x: None)(1, 2) == (2,)
62 |
63 | # varkeywords
64 | assert return_x(lambda **x: None)(a=1, b=2) == {'a': 1, 'b': 2}
65 | assert return_x(lambda **x: None)(a=1, x=3) == {'a': 1, 'x': 3} # Not just 3
66 | assert return_x(lambda a, **x: None)(a=1, b=2) == {'b': 2}
67 |
68 |
69 | if sys.version_info >= (3, 8):
70 | pytest.register_assert_rewrite("tests.py38_decorators")
71 | from .py38_decorators import test_decorator_access_args # noqa
72 |
73 |
74 | def test_double_decorator_defaults():
75 | @decorator
76 | def deco(call):
77 | return call.y
78 |
79 | @decorator
80 | def noop(call):
81 | return call()
82 |
83 | @deco
84 | @noop
85 | def f(x, y=1):
86 | pass
87 |
88 | assert f(42) == 1
89 |
90 |
91 | def test_decorator_with_method():
92 | @decorator
93 | def inc(call):
94 | return call() + 1
95 |
96 | class A(object):
97 | def ten(self):
98 | return 10
99 |
100 | @classmethod
101 | def ten_cls(cls):
102 | return 10
103 |
104 | @staticmethod
105 | def ten_static():
106 | return 10
107 |
108 | assert inc(A().ten)() == 11
109 | assert inc(A.ten_cls)() == 11
110 | assert inc(A.ten_static)() == 11
111 |
112 |
113 | def test_decorator_with_method_descriptor():
114 | @decorator
115 | def exclaim(call):
116 | return call() + '!'
117 |
118 | assert exclaim(str.upper)('hi') == 'HI!'
119 |
120 |
121 | def test_chain_arg_access():
122 | @decorator
123 | def decor(call):
124 | return call.x + call()
125 |
126 | @decor
127 | @decor
128 | def func(x):
129 | return x
130 |
131 | assert func(2) == 6
132 |
133 |
134 | def test_meta_attribtes():
135 | @decorator
136 | def decor(call):
137 | return call()
138 |
139 | def func(x):
140 | "Some doc"
141 | return x
142 |
143 | decorated = decor(func)
144 | double_decorated = decor(decorated)
145 |
146 | assert decorated.__name__ == 'func'
147 | assert decorated.__module__ == __name__
148 | assert decorated.__doc__ == "Some doc"
149 | assert decorated.__wrapped__ is func
150 | assert decorated.__original__ is func
151 |
152 | assert double_decorated.__wrapped__ is decorated
153 | assert double_decorated.__original__ is func
154 |
155 |
156 | def test_decorator_introspection():
157 | @decorator
158 | def decor(call, x):
159 | return call()
160 |
161 | assert decor.__name__ == 'decor'
162 |
163 | decor_x = decor(42)
164 | assert decor_x.__name__ == 'decor'
165 | assert decor_x._func is decor.__wrapped__
166 | assert decor_x._args == (42,)
167 | assert decor_x._kwargs == {}
168 |
--------------------------------------------------------------------------------
/tests/test_decorators.py.orig:
--------------------------------------------------------------------------------
1 | import pytest
2 | from funcy.decorators import *
3 |
4 |
5 | def test_decorator_no_args():
6 | @decorator
7 | def inc(call):
8 | return call() + 1
9 |
10 | @inc
11 | def ten():
12 | return 10
13 |
14 | assert ten() == 11
15 |
16 |
17 | def test_decorator_with_args():
18 | @decorator
19 | def add(call, n):
20 | return call() + n
21 |
22 | @add(2)
23 | def ten():
24 | return 10
25 |
26 | assert ten() == 12
27 |
28 |
29 | def test_decorator_kw_only_args():
30 | @decorator
31 | <<<<<<< Updated upstream
32 | def add(call, *, n=1):
33 | return call() + n
34 | =======
35 | def add(call, *, n=1): # TODO: use real kw-only args in Python 3
36 | return call() + call.n
37 | # @decorator
38 | # def add(call, **kwargs): # TODO: use real kw-only args in Python 3
39 | # return call() + kwargs.get("n", 1)
40 | >>>>>>> Stashed changes
41 |
42 | def ten(a, b):
43 | return 10
44 |
45 | # Should work with or without parentheses
46 | assert add(n=2)(ten)(1, 2) == 12
47 | assert add()(ten)(1, 2) == 11
48 | assert add(ten)(1, 2) == 11
49 |
50 |
51 | def test_decorator_access_arg():
52 | @decorator
53 | def multiply(call):
54 | return call() * call.n
55 |
56 | @multiply
57 | def square(n):
58 | return n
59 |
60 | assert square(5) == 25
61 |
62 |
63 | def test_decorator_access_nonexistent_arg():
64 | @decorator
65 | def return_x(call):
66 | return call.x
67 |
68 | @return_x
69 | def f():
70 | pass
71 |
72 | with pytest.raises(AttributeError): f()
73 |
74 |
75 | def test_decorator_required_arg():
76 | @decorator
77 | def deco(call):
78 | call.x
79 |
80 | @deco
81 | def f(x, y=42):
82 | pass
83 |
84 | with pytest.raises(AttributeError): f()
85 |
86 |
87 | def test_double_decorator_defaults():
88 | @decorator
89 | def deco(call):
90 | return call.y
91 |
92 | @decorator
93 | def noop(call):
94 | return call()
95 |
96 | @deco
97 | @noop
98 | def f(x, y=1):
99 | pass
100 |
101 | assert f(42) == 1
102 |
103 |
104 | def test_decorator_defaults():
105 | @decorator
106 | def deco(call):
107 | return call.y, call.z
108 |
109 | @deco
110 | def f(x, y=1, z=2):
111 | pass
112 |
113 | assert f(42) == (1, 2)
114 |
115 |
116 | def test_decorator_with_method():
117 | @decorator
118 | def inc(call):
119 | return call() + 1
120 |
121 | class A(object):
122 | def ten(self):
123 | return 10
124 |
125 | @classmethod
126 | def ten_cls(cls):
127 | return 10
128 |
129 | @staticmethod
130 | def ten_static():
131 | return 10
132 |
133 | assert inc(A().ten)() == 11
134 | assert inc(A.ten_cls)() == 11
135 | assert inc(A.ten_static)() == 11
136 |
137 |
138 | def test_decorator_with_method_descriptor():
139 | @decorator
140 | def exclaim(call):
141 | return call() + '!'
142 |
143 | assert exclaim(str.upper)('hi') == 'HI!'
144 |
145 |
146 | def test_chain_arg_access():
147 | @decorator
148 | def decor(call):
149 | return call.x + call()
150 |
151 | @decor
152 | @decor
153 | def func(x):
154 | return x
155 |
156 | assert func(2) == 6
157 |
158 |
159 | def test_meta_attribtes():
160 | @decorator
161 | def decor(call):
162 | return call()
163 |
164 | def func(x):
165 | "Some doc"
166 | return x
167 |
168 | decorated = decor(func)
169 | double_decorated = decor(decorated)
170 |
171 | assert decorated.__name__ == 'func'
172 | assert decorated.__module__ == __name__
173 | assert decorated.__doc__ == "Some doc"
174 | assert decorated.__wrapped__ is func
175 | assert decorated.__original__ is func
176 |
177 | assert double_decorated.__wrapped__ is decorated
178 | assert double_decorated.__original__ is func
179 |
180 |
181 | def test_decorator_introspection():
182 | @decorator
183 | def decor(call, x):
184 | return call()
185 |
186 | assert decor.__name__ == 'decor'
187 |
188 | decor_x = decor(42)
189 | assert decor_x.__name__ == 'decor'
190 | assert decor_x._func is decor.__wrapped__
191 | assert decor_x._args == (42,)
192 | assert decor_x._kwargs == {}
193 |
--------------------------------------------------------------------------------
/tests/test_flow.py:
--------------------------------------------------------------------------------
1 | from datetime import timedelta
2 | import pytest
3 | from funcy.flow import *
4 |
5 |
6 | def test_silent():
7 | assert silent(int)(1) == 1
8 | assert silent(int)('1') == 1
9 | assert silent(int)('hello') is None
10 |
11 | assert silent(str.upper)('hello') == 'HELLO'
12 |
13 |
14 | class MyError(Exception):
15 | pass
16 |
17 |
18 | def test_ignore():
19 | assert ignore(Exception)(raiser(Exception))() is None
20 | assert ignore(Exception)(raiser(MyError))() is None
21 | assert ignore((TypeError, MyError))(raiser(MyError))() is None
22 |
23 | with pytest.raises(TypeError):
24 | ignore(MyError)(raiser(TypeError))()
25 |
26 | assert ignore(MyError, default=42)(raiser(MyError))() == 42
27 |
28 |
29 | def test_raiser():
30 | with pytest.raises(Exception) as e: raiser()()
31 | assert e.type is Exception
32 |
33 | with pytest.raises(Exception, match="text") as e: raiser("text")()
34 | assert e.type is Exception
35 |
36 | with pytest.raises(MyError):
37 | raiser(MyError)()
38 | with pytest.raises(MyError, match="some message"):
39 | raiser(MyError('some message'))()
40 | with pytest.raises(MyError, match="some message") as e:
41 | raiser(MyError, 'some message')()
42 |
43 | with pytest.raises(MyError): raiser(MyError)('junk', keyword='junk')
44 |
45 |
46 | def test_suppress():
47 | with suppress(Exception):
48 | raise Exception
49 | with suppress(Exception):
50 | raise MyError
51 |
52 | with pytest.raises(TypeError):
53 | with suppress(MyError):
54 | raise TypeError
55 |
56 | with suppress(TypeError, MyError):
57 | raise MyError
58 |
59 |
60 | def test_reraise():
61 | @reraise((TypeError, ValueError), MyError)
62 | def erry(e):
63 | raise e
64 |
65 | with pytest.raises(MyError): erry(TypeError)
66 | with pytest.raises(MyError): erry(ValueError)
67 |
68 | with pytest.raises(MyError):
69 | with reraise(ValueError, MyError):
70 | raise ValueError
71 |
72 | with pytest.raises(TypeError):
73 | with reraise(ValueError, MyError):
74 | raise TypeError
75 |
76 | with pytest.raises(MyError, match="heyhey"):
77 | with reraise(ValueError, lambda e: MyError(str(e) * 2)):
78 | raise ValueError("hey")
79 |
80 |
81 | def test_retry():
82 | with pytest.raises(MyError):
83 | _make_failing()()
84 | assert retry(2, MyError)(_make_failing())() == 1
85 |
86 | with pytest.raises(MyError):
87 | retry(2, MyError)(_make_failing(n=2))()
88 |
89 |
90 | def test_retry_timeout(monkeypatch):
91 | timeouts = []
92 | monkeypatch.setattr('time.sleep', timeouts.append)
93 |
94 | def failing():
95 | raise MyError
96 |
97 | # sleep only between tries, so retry is 11, but sleep summary is ~0.1 sec
98 | del timeouts[:]
99 | with pytest.raises(MyError):
100 | retry(11, MyError, timeout=1)(failing)()
101 | assert timeouts == [1] * 10
102 |
103 | # exponential timeout
104 | del timeouts[:]
105 | with pytest.raises(MyError):
106 | retry(4, MyError, timeout=lambda a: 2 ** a)(failing)()
107 | assert timeouts == [1, 2, 4]
108 |
109 |
110 | def test_retry_many_errors():
111 | assert retry(2, (MyError, RuntimeError))(_make_failing())() == 1
112 | assert retry(2, [MyError, RuntimeError])(_make_failing())() == 1
113 |
114 |
115 | def test_retry_filter():
116 | error_pred = lambda e: 'x' in str(e)
117 | retry_deco = retry(2, MyError, filter_errors=error_pred)
118 |
119 | assert retry_deco(_make_failing(e=MyError('x')))() == 1
120 | with pytest.raises(MyError):
121 | retry_deco(_make_failing())()
122 |
123 |
124 | def _make_failing(n=1, e=MyError):
125 | calls = []
126 |
127 | def failing():
128 | if len(calls) < n:
129 | calls.append(1)
130 | raise e
131 | return 1
132 |
133 | return failing
134 |
135 |
136 | def test_fallback():
137 | assert fallback(raiser(), lambda: 1) == 1
138 | with pytest.raises(Exception): fallback((raiser(), MyError), lambda: 1)
139 | assert fallback((raiser(MyError), MyError), lambda: 1) == 1
140 |
141 |
142 | def test_limit_error_rate():
143 | calls = []
144 |
145 | @limit_error_rate(2, 60, MyError)
146 | def limited(x):
147 | calls.append(x)
148 | raise TypeError
149 |
150 | with pytest.raises(TypeError): limited(1)
151 | with pytest.raises(TypeError): limited(2)
152 | with pytest.raises(MyError): limited(3)
153 | assert calls == [1, 2]
154 |
155 |
156 | @pytest.mark.parametrize('typ',
157 | [pytest.param(int, id='int'), pytest.param(lambda s: timedelta(seconds=s), id='timedelta')])
158 | def test_throttle(monkeypatch, typ):
159 | timestamps = iter([0, 0.01, 1, 1.000025])
160 | monkeypatch.setattr('time.time', lambda: next(timestamps))
161 |
162 | calls = []
163 |
164 | @throttle(typ(1))
165 | def throttled(x):
166 | calls.append(x)
167 |
168 | throttled(1)
169 | throttled(2)
170 | throttled(3)
171 | throttled(4)
172 | assert calls == [1, 3]
173 |
174 |
175 | def test_throttle_class():
176 | class A:
177 | def foo(self):
178 | return 42
179 |
180 | a = A()
181 | assert throttle(1)(a.foo)() == 42
182 |
183 |
184 | def test_post_processing():
185 | @post_processing(max)
186 | def my_max(l):
187 | return l
188 |
189 | assert my_max([1, 3, 2]) == 3
190 |
191 |
192 | def test_collecting():
193 | @collecting
194 | def doubles(l):
195 | for i in l:
196 | yield i * 2
197 |
198 | assert doubles([1, 2]) == [2, 4]
199 |
200 |
201 | def test_once():
202 | calls = []
203 |
204 | @once
205 | def call(n):
206 | calls.append(n)
207 | return n
208 |
209 | call(1)
210 | call(2)
211 | assert calls == [1]
212 |
213 |
214 | def test_once_per():
215 | calls = []
216 |
217 | @once_per('n')
218 | def call(n, x=None):
219 | calls.append(n)
220 | return n
221 |
222 | call(1)
223 | call(2)
224 | call(1, 42)
225 | assert calls == [1, 2]
226 |
227 |
228 | def test_once_per_args():
229 | calls = []
230 |
231 | @once_per_args
232 | def call(n, x=None):
233 | calls.append(n)
234 | return n
235 |
236 | call(1)
237 | call(2)
238 | call(1, 42)
239 | assert calls == [1, 2, 1]
240 | call(1)
241 | assert calls == [1, 2, 1]
242 |
243 |
244 | def test_wrap_with():
245 | calls = []
246 |
247 | # Not using @contextmanager to not make this a decorator
248 | class Manager:
249 | def __enter__(self):
250 | calls.append(1)
251 | return self
252 |
253 | def __exit__(self, *args):
254 | pass
255 |
256 | @wrap_with(Manager())
257 | def calc():
258 | pass
259 |
260 | calc()
261 | assert calls == [1]
262 |
--------------------------------------------------------------------------------
/tests/test_funcmakers.py:
--------------------------------------------------------------------------------
1 | from collections import defaultdict
2 |
3 | import pytest
4 | from funcy.funcmakers import *
5 |
6 |
7 | def test_callable():
8 | assert make_func(lambda x: x + 42)(0) == 42
9 |
10 |
11 | def test_int():
12 | assert make_func(0)('abc') == 'a'
13 | assert make_func(2)([1,2,3]) == 3
14 | assert make_func(1)({1: 'a'}) == 'a'
15 | with pytest.raises(IndexError): make_func(1)('a')
16 | with pytest.raises(TypeError): make_func(1)(42)
17 |
18 |
19 | def test_slice():
20 | assert make_func(slice(1, None))('abc') == 'bc'
21 |
22 |
23 | def test_str():
24 | assert make_func(r'\d+')('ab42c') == '42'
25 | assert make_func(r'\d+')('abc') is None
26 | assert make_pred(r'\d+')('ab42c') is True
27 | assert make_pred(r'\d+')('abc') is False
28 |
29 |
30 | def test_dict():
31 | assert make_func({1: 'a'})(1) == 'a'
32 | with pytest.raises(KeyError): make_func({1: 'a'})(2)
33 |
34 | d = defaultdict(int, a=42)
35 | assert make_func(d)('a') == 42
36 | assert make_func(d)('b') == 0
37 |
38 |
39 | def test_set():
40 | s = set([1,2,3])
41 | assert make_func(s)(1) is True
42 | assert make_func(s)(4) is False
43 |
--------------------------------------------------------------------------------
/tests/test_funcolls.py:
--------------------------------------------------------------------------------
1 | from whatever import _
2 |
3 | from funcy import lfilter
4 | from funcy.funcolls import *
5 |
6 |
7 | def test_all_fn():
8 | assert lfilter(all_fn(_ > 3, _ % 2), range(10)) == [5, 7, 9]
9 |
10 | def test_any_fn():
11 | assert lfilter(any_fn(_ > 3, _ % 2), range(10)) == [1, 3, 4, 5, 6, 7, 8, 9]
12 |
13 | def test_none_fn():
14 | assert lfilter(none_fn(_ > 3, _ % 2), range(10)) == [0, 2]
15 |
16 | def test_one_fn():
17 | assert lfilter(one_fn(_ > 3, _ % 2), range(10)) == [1, 3, 4, 6, 8]
18 |
19 | def test_some_fn():
20 | assert some_fn(_-1, _*0, _+1, _*2)(1) == 2
21 |
22 |
23 | def test_extended_fns():
24 | f = any_fn(None, set([1,2,0]))
25 | assert f(1)
26 | assert f(0)
27 | assert f(10)
28 | assert not f('')
29 |
--------------------------------------------------------------------------------
/tests/test_funcs.py:
--------------------------------------------------------------------------------
1 | from operator import __add__, __sub__
2 | import sys
3 | import pytest
4 | from whatever import _
5 |
6 | from funcy import lmap, merge_with
7 | from funcy.funcs import *
8 | from funcy.seqs import keep
9 |
10 |
11 | def test_caller():
12 | assert caller([1, 2])(sum) == 3
13 |
14 | def test_constantly():
15 | assert constantly(42)() == 42
16 | assert constantly(42)('hi', 'there', volume='shout') == 42
17 |
18 | def test_partial():
19 | assert partial(__add__, 10)(1) == 11
20 | assert partial(__add__, 'abra')('cadabra') == 'abracadabra'
21 |
22 | merge = lambda a=None, b=None: a + b
23 | assert partial(merge, a='abra')(b='cadabra') == 'abracadabra'
24 | assert partial(merge, b='abra')(a='cadabra') == 'cadabraabra'
25 |
26 | def test_func_partial():
27 | class A(object):
28 | f = func_partial(lambda x, self: x + 1, 10)
29 |
30 | assert A().f() == 11
31 |
32 | def test_rpartial():
33 | assert rpartial(__sub__, 10)(1) == -9
34 | assert rpartial(pow, 2, 85)(10) == 15
35 |
36 | merge = lambda a, b, c='bra': a + b + c
37 | assert rpartial(merge, a='abra')(b='cada') == 'abracadabra'
38 | assert rpartial(merge, 'cada', c='fancy')('abra', c='funcy') == 'abracadafuncy'
39 |
40 | def test_curry():
41 | assert curry(lambda: 42)() == 42
42 | assert curry(lambda x: x * 2)(21) == 42
43 | assert curry(lambda x, y: x * y)(6)(7) == 42
44 | assert curry(__add__, 2)(10)(1) == 11
45 | assert curry(__add__)(10)(1) == 11 # Introspect builtin
46 | assert curry(lambda x,y,z: x+y+z)('a')('b')('c') == 'abc'
47 |
48 | def test_curry_funcy():
49 | # curry() doesn't handle required star args,
50 | # but we can code inspection for funcy utils.
51 | assert curry(lmap)(int)('123') == [1, 2, 3]
52 | assert curry(merge_with)(sum)({1: 1}) == {1: 1}
53 |
54 | def test_rcurry():
55 | assert rcurry(__sub__, 2)(10)(1) == -9
56 | assert rcurry(lambda x,y,z: x+y+z)('a')('b')('c') == 'cba'
57 | assert rcurry(str.endswith, 2)('c')('abc') is True
58 |
59 | def test_autocurry():
60 | at = autocurry(lambda a, b, c: (a, b, c))
61 |
62 | assert at(1)(2)(3) == (1, 2, 3)
63 | assert at(1, 2)(3) == (1, 2, 3)
64 | assert at(1)(2, 3) == (1, 2, 3)
65 | assert at(1, 2, 3) == (1, 2, 3)
66 | with pytest.raises(TypeError): at(1, 2, 3, 4)
67 | with pytest.raises(TypeError): at(1, 2)(3, 4)
68 |
69 | assert at(a=1, b=2, c=3) == (1, 2, 3)
70 | assert at(c=3)(1, 2) == (1, 2, 3)
71 | assert at(c=4)(c=3)(1, 2) == (1, 2, 3)
72 | with pytest.raises(TypeError): at(a=1)(1, 2, 3)
73 |
74 | def test_autocurry_named():
75 | at = autocurry(lambda a, b, c=9: (a, b, c))
76 |
77 | assert at(1)(2) == (1, 2, 9)
78 | assert at(1)(2, 3) == (1, 2, 3)
79 | assert at(a=1)(b=2) == (1, 2, 9)
80 | assert at(c=3)(1)(2) == (1, 2, 3)
81 | assert at(c=3, a=1, b=2) == (1, 2, 3)
82 |
83 | with pytest.raises(TypeError): at(b=2, c=9, d=42)(1)
84 |
85 | def test_autocurry_kwargs():
86 | at = autocurry(lambda a, b, **kw: (a, b, kw))
87 | assert at(1, 2) == (1, 2, {})
88 | assert at(1)(c=9)(2) == (1, 2, {'c': 9})
89 | assert at(c=9, d=5)(e=7)(1, 2) == (1, 2, {'c': 9, 'd': 5, 'e': 7})
90 |
91 | at = autocurry(lambda a, b=2, c=3: (a, b, c))
92 | assert at(1) == (1, 2, 3)
93 | assert at(a=1) == (1, 2, 3)
94 | assert at(c=9)(1) == (1, 2, 9)
95 | assert at(b=3, c=9)(1) == (1, 3, 9)
96 | with pytest.raises(TypeError): at(b=2, d=3, e=4)(a=1, c=1)
97 |
98 |
99 | def test_autocurry_kwonly():
100 | at = autocurry(lambda a, *, b: (a, b))
101 | assert at(1, b=2) == (1, 2)
102 | assert at(1)(b=2) == (1, 2)
103 | assert at(b=2)(1) == (1, 2)
104 |
105 | at = autocurry(lambda a, *, b=10: (a, b))
106 | assert at(1) == (1, 10)
107 | assert at(b=2)(1) == (1, 2)
108 |
109 | at = autocurry(lambda a=1, *, b: (a, b))
110 | assert at(b=2) == (1, 2)
111 | assert at(0)(b=2) == (0, 2)
112 |
113 | at = autocurry(lambda *, a=1, b: (a, b))
114 | assert at(b=2) == (1, 2)
115 | assert at(a=0)(b=2) == (0, 2)
116 |
117 | # TODO: move this here once we drop Python 3.7
118 | if sys.version_info >= (3, 8):
119 | pytest.register_assert_rewrite("tests.py38_funcs")
120 | from .py38_funcs import test_autocurry_posonly # noqa
121 |
122 |
123 | def test_autocurry_builtin():
124 | assert autocurry(complex)(imag=1)(0) == 1j
125 | assert autocurry(lmap)(_ + 1)([1, 2]) == [2, 3]
126 | assert autocurry(int)(base=12)('100') == 144
127 | if sys.version_info >= (3, 7):
128 | assert autocurry(str.split)(sep='_')('a_1') == ['a', '1']
129 |
130 | def test_autocurry_hard():
131 | def required_star(f, *seqs):
132 | return lmap(f, *seqs)
133 |
134 | assert autocurry(required_star)(__add__)('12', 'ab') == ['1a', '2b']
135 |
136 | _iter = autocurry(iter)
137 | assert list(_iter([1, 2])) == [1, 2]
138 | assert list(_iter([0, 1, 2].pop)(0)) == [2, 1]
139 |
140 | _keep = autocurry(keep)
141 | assert list(_keep('01')) == ['0', '1']
142 | assert list(_keep(int)('01')) == [1]
143 | with pytest.raises(TypeError): _keep(1, 2, 3)
144 |
145 | def test_autocurry_class():
146 | class A:
147 | def __init__(self, x, y=0):
148 | self.x, self.y = x, y
149 |
150 | assert autocurry(A)(1).__dict__ == {'x': 1, 'y': 0}
151 |
152 | class B: pass
153 | autocurry(B)()
154 |
155 | class I(int): pass
156 | assert autocurry(int)(base=12)('100') == 144
157 |
158 | def test_autocurry_docstring():
159 | @autocurry
160 | def f(a, b):
161 | 'docstring'
162 |
163 | assert f.__doc__ == 'docstring'
164 |
165 |
166 | def test_compose():
167 | double = _ * 2
168 | inc = _ + 1
169 | assert compose()(10) == 10
170 | assert compose(double)(10) == 20
171 | assert compose(inc, double)(10) == 21
172 | assert compose(str, inc, double)(10) == '21'
173 | assert compose(int, r'\d+')('abc1234xy') == 1234
174 |
175 | def test_rcompose():
176 | double = _ * 2
177 | inc = _ + 1
178 | assert rcompose()(10) == 10
179 | assert rcompose(double)(10) == 20
180 | assert rcompose(inc, double)(10) == 22
181 | assert rcompose(double, inc)(10) == 21
182 |
183 | def test_complement():
184 | assert complement(identity)(0) is True
185 | assert complement(identity)([1, 2]) is False
186 |
187 | def test_juxt():
188 | assert ljuxt(__add__, __sub__)(10, 2) == [12, 8]
189 | assert lmap(ljuxt(_ + 1, _ - 1), [2, 3]) == [[3, 1], [4, 2]]
190 |
191 | def test_iffy():
192 | assert lmap(iffy(_ % 2, _ * 2, _ / 2), [1,2,3,4]) == [2,1,6,2]
193 | assert lmap(iffy(_ % 2, _ * 2), [1,2,3,4]) == [2,2,6,4]
194 | assert lmap(iffy(_ * 2), [21, '', None]) == [42, '', None]
195 | assert lmap(iffy(_ % 2, _ * 2, None), [1,2,3,4]) == [2, None, 6, None]
196 | assert lmap(iffy(_ + 1, default=1), [1, None, 2]) == [2, 1, 3]
197 | assert lmap(iffy(set([1,4,5]), _ * 2), [1, 2, 3, 4]) == [2, 2, 3, 8]
198 | assert lmap(iffy(r'\d+', str.upper), ['a2', 'c']) == ['A2', 'c']
199 | assert lmap(iffy(set([1,4,5])), [False, 2, 4]) == [False, False, True]
200 | assert lmap(iffy(None), [False, 2, 3, 4]) == [False, 2, 3, 4]
201 |
--------------------------------------------------------------------------------
/tests/test_interface.py:
--------------------------------------------------------------------------------
1 | import funcy
2 |
3 |
4 | def test_docs():
5 | exports = [(name, getattr(funcy, name)) for name in funcy.__all__
6 | if name not in ('print_errors', 'print_durations', 'ErrorRateExceeded')
7 | and getattr(funcy, name).__module__ not in ('funcy.types', 'funcy.primitives')]
8 | # NOTE: we are testing this way and not with all() to immediately get a list of offenders
9 | assert [name for name, f in exports if f.__name__ in ('', '_decorator')] == []
10 | assert [name for name, f in exports if f.__doc__ is None] == []
11 |
--------------------------------------------------------------------------------
/tests/test_objects.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import pytest
3 | from funcy.objects import *
4 | from funcy import suppress
5 |
6 |
7 | ### @cached_property
8 |
9 | def test_cached_property():
10 | calls = [0]
11 |
12 | class A(object):
13 | @cached_property
14 | def prop(self):
15 | calls[0] += 1
16 | return 7
17 |
18 | a = A()
19 | assert a.prop == 7
20 | assert a.prop == 7
21 | assert calls == [1]
22 |
23 | a.prop = 42
24 | assert a.prop == 42
25 |
26 | del a.prop
27 | assert a.prop == 7
28 | assert calls == [2]
29 |
30 | def test_cached_property_doc():
31 | class A(object):
32 | @cached_property
33 | def prop(self):
34 | "prop doc"
35 | return 7
36 |
37 | assert A.prop.__doc__ == "prop doc"
38 |
39 |
40 | def test_cached_readonly():
41 | class A(object):
42 | @cached_readonly
43 | def prop(self):
44 | return 7
45 |
46 | a = A()
47 | assert a.prop == 7
48 | with pytest.raises(AttributeError):
49 | a.prop = 8
50 |
51 |
52 | def test_wrap_prop():
53 | calls = []
54 |
55 | # Not using @contextmanager to not make this a decorator
56 | class Manager:
57 | def __init__(self, name):
58 | self.name = name
59 |
60 | def __enter__(self):
61 | calls.append(self.name)
62 | return self
63 |
64 | def __exit__(self, *args):
65 | pass
66 |
67 | class A(object):
68 | @wrap_prop(Manager('p'))
69 | @property
70 | def prop(self):
71 | return 1
72 |
73 | @wrap_prop(Manager('cp'))
74 | @cached_property
75 | def cached_prop(self):
76 | return 1
77 |
78 | a = A()
79 | assert a.prop and calls == ['p']
80 | assert a.prop and calls == ['p', 'p']
81 | assert a.cached_prop and calls == ['p', 'p', 'cp']
82 | assert a.cached_prop and calls == ['p', 'p', 'cp']
83 |
84 | # Wrap __set__ for data props
85 | a = A()
86 | calls[:] = []
87 | with suppress(AttributeError):
88 | a.prop = 2
89 | assert calls == ['p']
90 |
91 | # Do not wrap __set__ for non-data props
92 | a.cached_property = 2
93 | assert calls == ['p']
94 |
95 |
96 | ### Monkey tests
97 |
98 | def test_monkey():
99 | class A(object):
100 | def f(self):
101 | return 7
102 |
103 | @monkey(A)
104 | def f(self):
105 | return f.original(self) * 6
106 |
107 | assert A().f() == 42
108 |
109 |
110 | def test_monkey_with_name():
111 | class A(object):
112 | def f(self):
113 | return 7
114 |
115 | @monkey(A, name='f')
116 | def g(self):
117 | return g.original(self) * 6
118 |
119 | assert A().f() == 42
120 |
121 |
122 | def test_monkey_property():
123 | class A(object):
124 | pass
125 |
126 | @monkey(A)
127 | @property
128 | def prop(self):
129 | return 42
130 |
131 | assert A().prop == 42
132 |
133 |
134 | def f(x):
135 | return x
136 |
137 | def test_monkey_module():
138 | this_module = sys.modules[__name__]
139 |
140 | @monkey(this_module)
141 | def f(x):
142 | return f.original(x) * 2
143 |
144 | assert f(21) == 42
145 |
146 |
147 | def test_lazy_object():
148 | class A(object):
149 | x = 42
150 | def __init__(self):
151 | log.append('init')
152 |
153 | log = []
154 | a = LazyObject(A)
155 | assert not log
156 | assert a.x == 42
157 |
--------------------------------------------------------------------------------
/tests/test_seqs.py:
--------------------------------------------------------------------------------
1 | from collections.abc import Iterator
2 | from operator import add
3 | import pytest
4 | from whatever import _
5 |
6 | from funcy import is_list
7 | from funcy.seqs import *
8 |
9 |
10 | def test_repeatedly():
11 | counter = count()
12 | c = lambda: next(counter)
13 | assert take(2, repeatedly(c)) == [0, 1]
14 |
15 | def test_iterate():
16 | assert take(4, iterate(_ * 2, 1)) == [1, 2, 4, 8]
17 |
18 |
19 | def test_take():
20 | assert take(2, [3, 2, 1]) == [3, 2]
21 | assert take(2, count(7)) == [7, 8]
22 |
23 | def test_drop():
24 | dropped = drop(2, [5, 4, 3, 2])
25 | assert isinstance(dropped, Iterator)
26 | assert list(dropped) == [3, 2]
27 |
28 | assert take(2, drop(2, count())) == [2, 3]
29 |
30 | def test_first():
31 | assert first('xyz') == 'x'
32 | assert first(count(7)) == 7
33 | assert first([]) is None
34 |
35 | def test_second():
36 | assert second('xyz') == 'y'
37 | assert second(count(7)) == 8
38 | assert second('x') is None
39 |
40 | def test_last():
41 | assert last('xyz') == 'z'
42 | assert last(range(1, 10)) == 9
43 | assert last([]) is None
44 | assert last(x for x in 'xyz') == 'z'
45 |
46 | def test_nth():
47 | assert nth(0, 'xyz') == 'x'
48 | assert nth(2, 'xyz') == 'z'
49 | assert nth(3, 'xyz') is None
50 | assert nth(3, count(7)) == 10
51 |
52 | def test_butlast():
53 | assert list(butlast('xyz')) == ['x', 'y']
54 | assert list(butlast([])) == []
55 |
56 | def test_ilen():
57 | assert ilen('xyz') == 3
58 | assert ilen(range(10)) == 10
59 |
60 |
61 | def test_lmap():
62 | assert lmap(_ * 2, [2, 3]) == [4, 6]
63 | assert lmap(None, [2, 3]) == [2, 3]
64 | assert lmap(_ + _, [1, 2], [4, 5]) == [5, 7]
65 |
66 | assert lmap(r'\d+', ['a2', '13b']) == ['2', '13']
67 | assert lmap({'a': 1, 'b': 2}, 'ab') == [1, 2]
68 | assert lmap(set([1,2,3]), [0, 1, 2]) == [False, True, True]
69 | assert lmap(1, ['abc', '123']) == ['b', '2']
70 | assert lmap(slice(2), ['abc', '123']) == ['ab', '12']
71 |
72 |
73 | def test_filter():
74 | assert lfilter(None, [2, 3, 0]) == [2, 3]
75 | assert lfilter(r'\d+', ['a2', '13b', 'c']) == ['a2', '13b']
76 | assert lfilter(set([1,2,3]), [0, 1, 2, 4, 1]) == [1, 2, 1]
77 |
78 | def test_remove():
79 | assert lremove(_ > 3, range(10)) == [0, 1, 2, 3]
80 | assert lremove('^a', ['a', 'b', 'ba']) == ['b', 'ba']
81 |
82 | def test_keep():
83 | assert lkeep(_ % 3, range(5)) == [1, 2, 1]
84 | assert lkeep(range(5)) == [1, 2, 3, 4]
85 | assert lkeep(mapcat(range, range(4))) == [1, 1, 2]
86 |
87 | def test_concat():
88 | assert lconcat('ab', 'cd') == list('abcd')
89 | assert lconcat() == []
90 |
91 | def test_cat():
92 | assert lcat('abcd') == list('abcd')
93 | assert lcat(range(x) for x in range(3)) == [0, 0, 1]
94 |
95 | def test_flatten():
96 | assert lflatten([1, [2, 3]]) == [1, 2, 3]
97 | assert lflatten([[1, 2], 3]) == [1, 2, 3]
98 | assert lflatten([(2, 3)]) == [2, 3]
99 | assert lflatten([iter([2, 3])]) == [2, 3]
100 |
101 | def test_flatten_follow():
102 | assert lflatten([1, [2, 3]], follow=is_list) == [1, 2, 3]
103 | assert lflatten([1, [(2, 3)]], follow=is_list) == [1, (2, 3)]
104 |
105 | def test_mapcat():
106 | assert lmapcat(lambda x: [x, x], 'abc') == list('aabbcc')
107 |
108 | def test_interleave():
109 | assert list(interleave('ab', 'cd')) == list('acbd')
110 | assert list(interleave('ab_', 'cd')) == list('acbd')
111 |
112 | def test_iterpose():
113 | assert list(interpose('.', 'abc')) == list('a.b.c')
114 |
115 |
116 | def test_takewhile():
117 | assert list(takewhile([1, 2, None, 3])) == [1, 2]
118 |
119 |
120 | def test_distinct():
121 | assert ldistinct('abcbad') == list('abcd')
122 | assert ldistinct([{}, {}, {'a': 1}, {'b': 2}], key=len) == [{}, {'a': 1}]
123 | assert ldistinct(['ab', 'cb', 'ad'], key=0) == ['ab', 'cb']
124 |
125 | # Separate test as lsplit() is not implemented via it.
126 | def test_split():
127 | assert lmap(list, split(_ % 2, range(5))) == [[1, 3], [0, 2, 4]]
128 |
129 | def test_lsplit():
130 | assert lsplit(_ % 2, range(5)) == ([1, 3], [0, 2, 4])
131 | # This behaviour moved to split_at()
132 | with pytest.raises(TypeError): lsplit(2, range(5))
133 |
134 | def test_split_at():
135 | assert lsplit_at(2, range(5)) == ([0, 1], [2, 3, 4])
136 |
137 | def test_split_by():
138 | assert lsplit_by(_ % 2, [1, 2, 3]) == ([1], [2, 3])
139 |
140 | def test_group_by():
141 | assert group_by(_ % 2, range(5)) == {0: [0, 2, 4], 1: [1, 3]}
142 | assert group_by(r'\d', ['a1', 'b2', 'c1']) == {'1': ['a1', 'c1'], '2': ['b2']}
143 |
144 | def test_group_by_keys():
145 | assert group_by_keys(r'(\d)(\d)', ['12', '23']) == {'1': ['12'], '2': ['12', '23'], '3': ['23']}
146 |
147 | def test_group_values():
148 | assert group_values(['ab', 'ac', 'ba']) == {'a': ['b', 'c'], 'b': ['a']}
149 |
150 | def test_count_by():
151 | assert count_by(_ % 2, range(5)) == {0: 3, 1: 2}
152 | assert count_by(r'\d', ['a1', 'b2', 'c1']) == {'1': 2, '2': 1}
153 |
154 | def test_count_by_is_defaultdict():
155 | cnts = count_by(len, [])
156 | assert cnts[1] == 0
157 |
158 | def test_count_reps():
159 | assert count_reps([0, 1, 0]) == {0: 2, 1: 1}
160 |
161 | def test_partition():
162 | assert lpartition(2, [0, 1, 2, 3, 4]) == [[0, 1], [2, 3]]
163 | assert lpartition(2, 1, [0, 1, 2, 3]) == [[0, 1], [1, 2], [2, 3]]
164 | # test iters
165 | assert lpartition(2, iter(range(5))) == [[0, 1], [2, 3]]
166 | assert lmap(list, lpartition(2, range(5))) == [[0, 1], [2, 3]]
167 |
168 | def test_chunks():
169 | assert lchunks(2, [0, 1, 2, 3, 4]) == [[0, 1], [2, 3], [4]]
170 | assert lchunks(2, 1, [0, 1, 2, 3]) == [[0, 1], [1, 2], [2, 3], [3]]
171 | assert lchunks(3, 1, iter(range(3))) == [[0, 1, 2], [1, 2], [2]]
172 |
173 | def test_partition_by():
174 | assert lpartition_by(lambda x: x == 3, [1,2,3,4,5]) == [[1,2], [3], [4,5]]
175 | assert lpartition_by('x', 'abxcd') == [['a', 'b'], ['x'], ['c', 'd']]
176 | assert lpartition_by(r'\d', '1211') == [['1'], ['2'], ['1','1']]
177 |
178 |
179 | def test_with_prev():
180 | assert list(with_prev(range(3))) == [(0, None), (1, 0), (2, 1)]
181 |
182 | def test_with_next():
183 | assert list(with_next(range(3))) == [(0, 1), (1, 2), (2, None)]
184 |
185 | def test_pairwise():
186 | assert list(pairwise(range(3))) == [(0, 1), (1, 2)]
187 |
188 | def test_lzip():
189 | assert lzip('12', 'xy') == [('1', 'x'), ('2', 'y')]
190 | assert lzip('123', 'xy') == [('1', 'x'), ('2', 'y')]
191 | assert lzip('12', 'xyz') == [('1', 'x'), ('2', 'y')]
192 | assert lzip('12', iter('xyz')) == [('1', 'x'), ('2', 'y')]
193 |
194 | def test_lzip_strict():
195 | assert lzip('123', 'xy', strict=False) == [('1', 'x'), ('2', 'y')]
196 | assert lzip('12', 'xy', strict=True) == [('1', 'x'), ('2', 'y')]
197 | assert lzip('12', iter('xy'), strict=True) == [('1', 'x'), ('2', 'y')]
198 | for wrap in (str, iter):
199 | with pytest.raises(ValueError): lzip(wrap('123'), wrap('xy'), strict=True)
200 | with pytest.raises(ValueError): lzip(wrap('12'), wrap('xyz'), wrap('abcd'), strict=True)
201 | with pytest.raises(ValueError): lzip(wrap('123'), wrap('xy'), wrap('abcd'), strict=True)
202 | with pytest.raises(ValueError): lzip(wrap('123'), wrap('xyz'), wrap('ab'), strict=True)
203 |
204 |
205 | def test_reductions():
206 | assert lreductions(add, []) == []
207 | assert lreductions(add, [None]) == [None]
208 | assert lreductions(add, [1, 2, 3, 4]) == [1, 3, 6, 10]
209 | assert lreductions(lambda x, y: x + [y], [1,2,3], []) == [[1], [1, 2], [1, 2, 3]]
210 |
211 | def test_sums():
212 | assert lsums([]) == []
213 | assert lsums([1, 2, 3, 4]) == [1, 3, 6, 10]
214 | assert lsums([[1],[2],[3]]) == [[1], [1, 2], [1, 2, 3]]
215 |
216 | def test_without():
217 | assert lwithout([]) == []
218 | assert lwithout([1, 2, 3, 4]) == [1, 2, 3, 4]
219 | assert lwithout([1, 2, 1, 0, 3, 1, 4], 0, 1) == [2, 3, 4]
220 |
--------------------------------------------------------------------------------
/tests/test_strings.py:
--------------------------------------------------------------------------------
1 | from funcy.strings import *
2 |
3 |
4 | def test_re_find():
5 | assert re_find(r'\d+', 'x34y12') == '34'
6 | assert re_find(r'y(\d+)', 'x34y12') == '12'
7 | assert re_find(r'([a-z]+)(\d+)', 'x34y12') == ('x', '34')
8 | assert re_find(r'(?P[a-z]+)(?P\d+)', 'x34y12') == {'l': 'x', 'd': '34'}
9 |
10 |
11 | def test_re_all():
12 | assert re_all(r'\d+', 'x34y12') == ['34', '12']
13 | assert re_all(r'([a-z]+)(\d+)', 'x34y12') == [('x', '34'), ('y', '12')]
14 | assert re_all(r'(?P[a-z]+)(?P\d+)', 'x34y12') \
15 | == [{'l': 'x', 'd': '34'}, {'l': 'y', 'd': '12'}]
16 |
17 | def test_str_join():
18 | assert str_join([1, 2, 3]) == '123'
19 | assert str_join('_', [1, 2, 3]) == '1_2_3'
20 | assert isinstance(str_join(u'_', [1, 2, 3]), type(u''))
21 |
22 |
23 | def test_cut_prefix():
24 | assert cut_prefix('name:alex', 'name:') == 'alex'
25 | assert cut_prefix('alex', 'name:') == 'alex'
26 |
27 | def test_cut_suffix():
28 | assert cut_suffix('name.py', '.py') == 'name'
29 | assert cut_suffix('name', '.py') == 'name'
30 |
--------------------------------------------------------------------------------
/tests/test_tree.py:
--------------------------------------------------------------------------------
1 | from whatever import _
2 |
3 | from funcy import rest
4 | from funcy.tree import *
5 |
6 |
7 | def test_tree_leaves():
8 | assert ltree_leaves([1, 2, [3, [4]], 5]) == [1, 2, 3, 4, 5]
9 | assert ltree_leaves(1) == [1]
10 |
11 | assert ltree_leaves(3, follow=_ > 1, children=range) == [0, 1, 0, 1]
12 | assert ltree_leaves([1, [2, [3, 4], 5], 6], children=rest) == [4, 5, 6]
13 |
14 |
15 | def test_tree_nodes():
16 | assert ltree_nodes([1, 2, [3, [4]], 5]) == [
17 | [1, 2, [3, [4]], 5],
18 | 1, 2,
19 | [3, [4]], 3, [4], 4,
20 | 5
21 | ]
22 | assert ltree_nodes(1) == [1]
23 | assert ltree_nodes(3, follow=_ > 1, children=range) == [3, 0, 1, 2, 0, 1]
24 |
--------------------------------------------------------------------------------
/tests/test_types.py:
--------------------------------------------------------------------------------
1 | from funcy.types import *
2 |
3 |
4 | def test_iterable():
5 | assert iterable([])
6 | assert iterable({})
7 | assert iterable('abc')
8 | assert iterable(iter([]))
9 | assert iterable(x for x in range(10))
10 | assert iterable(range(10))
11 |
12 | assert not iterable(1)
13 |
14 |
15 | def test_is_iter():
16 | assert is_iter(iter([]))
17 | assert is_iter(x for x in range(10))
18 |
19 | assert not is_iter([])
20 | assert not is_iter(range(10))
21 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py34, py35, py36, py37, py38, py39, py310, py311, py312, pypy3, docs, lint
3 |
4 | [testenv]
5 | deps = -r test_requirements.txt
6 | commands = py.test -W error {posargs}
7 |
8 |
9 | [testenv:docs]
10 | deps = -r docs/requirements.txt
11 | changedir = docs
12 | commands = sphinx-build -b html -W . _build/html
13 |
14 |
15 | ; TODO: get rid of flakes
16 | [flake8]
17 | max-line-length = 100
18 | ignore = E127,E128,E302,F403,E126,E272,E226,E301,E261,E265,E251,E303,E305,E306,E266,E731,E402,F405,W503
19 | exclude = docs/conf.py, .tox
20 |
21 | [testenv:lint]
22 | basepython = python3.10
23 | passenv = PYTHONPATH
24 | deps =
25 | flake8>=3.8.3
26 | commands =
27 | python --version
28 | flake8 funcy
29 | flake8 --select=F,E5,W tests
30 |
--------------------------------------------------------------------------------