├── .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 | --------------------------------------------------------------------------------