├── .github └── workflows │ ├── codeql-analysis.yml │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── conftest.py ├── docs ├── Makefile ├── conf.py ├── functionali.rst ├── higher-order-functions.rst ├── index.rst ├── make.bat ├── modules.rst ├── predicates.rst ├── seq-transform.rst └── seq-traverse.rst ├── functionali ├── __init__.py ├── higher_order_functions.py ├── predicates.py ├── seq_transform.py └── seq_traverse.py ├── poetry.lock ├── pyproject.toml └── tests ├── test_higher_order_functions.py ├── test_predicates.py ├── test_seq_transform.py └── test_seq_traverse.py /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '38 7 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the main branch 8 | push: 9 | branches: [ main ] 10 | pull_request: 11 | branches: [ main ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | - uses: actions/checkout@v2 27 | - uses: actions/setup-python@v2 28 | with: 29 | python-version: 3.8 30 | 31 | # Runs a single command using the runners shell 32 | - name: Install poetry 33 | run: pip install poetry 34 | 35 | - name: Install dependencies. 36 | run: poetry install 37 | 38 | - name: Generate coverage report 39 | run: poetry run pytest --cov functionali --cov-report=xml 40 | 41 | - name: Upload coverage to Codecov 42 | uses: codecov/codecov-action@v1 43 | with: 44 | token: ${{ secrets.CODECOV_TOKEN }} 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # file for me 2 | scratch_file.py 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | scratch.txt 133 | .vscode/settings.json 134 | .vscode/settings.json 135 | .vscode/* -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.3.0 4 | 5 | ### Added 6 | - `tuplize` 7 | - `map` 8 | - `lazymap` 9 | - `filter` 10 | - `lazyfilter` 11 | 12 | ## 0.2.0 13 | ### Added 14 | - `comp` 15 | - `count` 16 | - `count_` 17 | - `identity` 18 | - `reduce` 19 | - `reduced` 20 | - `threadf` 21 | - `threadl` 22 | - `trampoline` 23 | 24 | ### Changed 25 | - `is_nested` now works with dictionaries 26 | 27 | 28 | ### Fixed 29 | - `less_than` would return true for this `1 < 4 < 2` [issue #16](https://github.com/AbhinavOmprakash/functionali/issues/16) 30 | - `foldr` now works with iterators. [issues #17](https://github.com/AbhinavOmprakash/functionali/issues/17) 31 | - `curry` now works with 0-arg functions 32 | 33 | ## 0.1.1 Jul 27, 2021 34 | ### Fixed 35 | - `first`, `second`, now work with iterables. 36 | 37 | ## 0.1.0 Jul 26, 2021 38 | ### Added 39 | - `all_predicates` 40 | - `argmap` 41 | - `argzip` 42 | - `butlast` 43 | - `complement` 44 | - `concat` 45 | - `conj` 46 | - `cons` 47 | - `contains` 48 | - `curry` 49 | - `drop` 50 | - `drop_while` 51 | - `equals` 52 | - `ffirst` 53 | - `fifth` 54 | - `first` 55 | - `flatten` 56 | - `flip` 57 | - `foldr` 58 | - `fourth` 59 | - `greater_than` 60 | - `greater_than_eq` 61 | - `insert` 62 | - `interleave` 63 | - `is_` 64 | - `is_atom` 65 | - `is_divisible` 66 | - `is_divisible_by` 67 | - `is_empty` 68 | - `is_even` 69 | - `is_nested` 70 | - `is_numeric` 71 | - `is_odd` 72 | - `iter_` 73 | - `last` 74 | - `less_than` 75 | - `less_than_eq` 76 | - `remove` 77 | - `rest` 78 | - `reversed_second` 79 | - `some_predicates` 80 | - `split_with` 81 | - `take` 82 | - `take_while` 83 | - `third` 84 | - `unzip` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Abhinav Omprakash 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Functionali 2 | functional programming tools for python. Putting the fun in functional programming 😉 3 | 4 | [![codecov](https://codecov.io/gh/AbhinavOmprakash/functionali/branch/main/graph/badge.svg?token=75LLE4F7EY)](https://codecov.io/gh/AbhinavOmprakash/functionali) 5 | [![Documentation Status](https://readthedocs.org/projects/functionali/badge/?version=latest)](https://functionali.readthedocs.io/en/latest/?badge=latest) 6 | 7 | 8 | Functional programming is a fundamentally different way of solving problems, and once It clicks, it's pure joy after that. 9 | A lot of ideas in this library have been taken from Clojure and Haskell, so the credit goes to those languages. 10 | If you find your favorite function missing, or find ways to improve this project, I'd love to hear it. 11 | 12 | 13 | 14 | [Documentation](https://functionali.readthedocs.io/en/latest/) 15 | 16 | [wiki](https://functionali.readthedocs.io/en/latest/) 17 | 18 | ## Installation 19 | ```bash 20 | pip install functionali 21 | ``` 22 | 23 | ## Features 24 | - Polymorphic functions that operate on all sequences. Including dictionaries. 25 | - A bunch of commonly used predicates that enhance readability, especially when used with filters. 26 | 27 | ## A note on how functionali deals with dictionaries. 28 | As of now, all sequence traversing like `first` and `rest` functions treat dictionaries as a nested tuple of keys and values. 29 | Here's an example. 30 | 31 | ```pycon 32 | >>> d = {1:"a"} 33 | >>> first(d) 34 | ((1, "a"),) 35 | ``` 36 | 37 | while this is convenient for traversing dicts, it makes certain functions like `contains` rather awkward, since you as the user, would 38 | have to explicity convert the tuple back to a dict before passing it in. 39 | (This might change in future) 40 | 41 | 42 | 43 | ## functional programming is powerful 44 | 45 | One of my favorite Functional implementations of an algorithm is insertion sort, here's how simple it 46 | ```pycon 47 | >>> from functionali import foldr, insert 48 | >>> def insertion_sort(iterable): 49 | ... return foldr(insert, iterable, []) 50 | 51 | >>> insertion_sort([3,2,1]) 52 | (1, 2, 3) 53 | 54 | # even works with dictionaries 55 | >>> insertion_sort({3:"c", 1: "a", 2: "b"}) 56 | ((1, 'a'), (2, 'b'), (3, 'c')) 57 | 58 | # and strings 59 | >>> insertion_sort("cbdasdf") 60 | ('a', 'b', 'c', 'd', 'd', 'f', 's') 61 | >>> "".join(insertion_sort("cbdasdf")) 62 | 'abcddfs' # better? 63 | ``` 64 | 65 | ## functional programs are flexible 66 | 67 | Let's say you wanted to find a number that was divisible by The following numbers 68 | `1,2,3,4,5,6,7,8,9,10` 69 | One way to solve it is a bunch of nested `if` Statements and we all know how brittle those can be. 70 | 71 | Let's see the functional way. 72 | ```pycon 73 | >>> from functionali import is_divisible_by, all_predicates 74 | >>> my_filter = all_predicates(*map(is_divisible_by,[1,2,3,4,5,6,7,8,9,10])) 75 | >>> list(filter(my_filter,range(1,10000))) 76 | [2520, 5040, 7560] 77 | ``` 78 | 79 | Now let's say the boss decides that He wants the numbers that are not 80 | Divisible by all these numbers 81 | ```pycon 82 | >>> from functionali import complement 83 | >>> list(filter(complement(my_filter),range(1,10000))) 84 | [... 2515, 2516, 2517, 2518, 2519, 2521, 2522, 2523, 2524...] # Snipped for brevity 85 | # note that 2520 is not present 86 | ``` 87 | The boss comes in again and he says that he Doesn't want numbers that are divisible by ALL these numbers but instead, ANY of these numbers `[1,2,3,4,5,6,7,8,9,10]` 88 | ```pycon 89 | >>> from functionali import some_predicates 90 | >>> my_filter = some_predicates(*map(is_divisible_by,[1,2,3,4,5,6,7,8,9,10])) 91 | >>> list(filter(complement(my_filter),range(1,10000))) 92 | [] 93 | ``` 94 | The boss realizes that all numbers are divisible by 1 and he tells you to Remove 1 95 | 96 | ```pycon 97 | >>> my_filter = some_predicates(*map(is_divisible_by,[2,3,4,5,6,7,8,9,10])) 98 | >>> list(filter(complement(my_filter),range(1,10000))) 99 | [1, 11, 13, 17, 19, 23, 29, 31...] # ,Snipped for brevity 100 | ``` 101 | 102 | ## A few more examples 103 | 104 | Functionali provides functions to traverse sequences(Including dictionaries), Some of the most useful ones are `first`, `rest`, `last`,`butlast`, `take`, `drop` 105 | ```Python 106 | from functionali import first, rest, last, butlast, take, drop 107 | 108 | >>> first([1,2,3,4,5]) 109 | 1 110 | 111 | >>> first({1:"a", 2:"b"}) 112 | (1, "a") 113 | 114 | >>> last([1,2,3,4]) 115 | 4 116 | 117 | >>> list(rest([1,2,3,4,5])) 118 | [2, 3, 4, 5] 119 | 120 | >>> butlast([1,2,3]) # returns all elements except the last element 121 | (1,2) 122 | 123 | >>> take(3, [1,2,3,4,5]) 124 | (1, 2, 3) 125 | 126 | >>> drop(3, [1,2,3,4,5]) 127 | (4,5) 128 | ``` 129 | 130 | There are functions that construct new sequences like `cons`, `conj`, `concat`, `insert` 131 | 132 | ```Python 133 | from functionali import cons, conj, concat, insert 134 | >>> cons(5, [1,2,3,4]) 135 | deque([5, 1, 2, 3, 4]) # 5 is the 'head' of the new list. 136 | 137 | # adds element to the iterable, at the appropriate end. 138 | >>> conj([1,2,3,4],5) # similar to Clojure's conj 139 | [1, 2, 3, 4, 5] 140 | 141 | # Adds to the left of a deque. 142 | >>> conj(deque([1,2]), 3,4) 143 | deque([4, 3, 1, 2]) 144 | 145 | # Add items to the end of the iterable. 146 | >>> concat([1,2,3,4],5) 147 | [1, 2, 3, 4, 5] 148 | 149 | >>> concat(deque([1,2]), 3,4) 150 | deque([1, 2, 3, 4]) 151 | 152 | # Inserts 3 right before the first element 153 | # in the iterable (here:4) that is greater than 3 154 | >>> insert(3, [1,2,4,2]) 155 | (1,2,3,4,2) 156 | ``` 157 | 158 | Functionali also comes with a number of useful predicates 159 | (if You can't find something you're looking for, make a pull request.) 160 | These can be combined in various ways. 161 | for example. 162 | 163 | ```Python 164 | from functionali import is_even, is_prime, take_while 165 | >>> list(filter(is_even,[1,2,3,4]) 166 | [2,4] 167 | 168 | >>> take_while(is_prime, [2,3,5,4,6,5])) # Constructs a list while is_prime is true. 169 | [2,3,5] 170 | 171 | ``` 172 | 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhinavOmprakash/functionali/c9349c6b5d113456018e29f8a8bed97a262758a6/conftest.py -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | sys.path.insert(0, os.path.abspath("..")) 17 | 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = "Functionali" 22 | copyright = "2021, Abhinav Omprakash" 23 | author = "Abhinav Omprakash" 24 | 25 | # The full version, including alpha/beta/rc tags 26 | release = "0.1.0" 27 | 28 | 29 | # -- General configuration --------------------------------------------------- 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | import sphinx_rtd_theme 35 | 36 | extensions = [ 37 | "sphinx.ext.autodoc", 38 | "sphinx.ext.autosummary", 39 | "sphinx_rtd_theme", 40 | ] 41 | 42 | # source_suffix = ['.rst', '.md'] 43 | # Add any paths that contain templates here, relative to this directory. 44 | templates_path = ["_templates"] 45 | 46 | # List of patterns, relative to source directory, that match files and 47 | # directories to ignore when looking for source files. 48 | # This pattern also affects html_static_path and html_extra_path. 49 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 50 | 51 | 52 | # -- Options for HTML output ------------------------------------------------- 53 | 54 | # The theme to use for HTML and HTML Help pages. See the documentation for 55 | # a list of builtin themes. 56 | # 57 | html_theme = "sphinx_rtd_theme" 58 | 59 | # Add any paths that contain custom static files (such as style sheets) here, 60 | # relative to this directory. They are copied after the builtin static files, 61 | # so a file named "default.css" will overwrite the builtin "default.css". 62 | html_static_path = ["_static"] 63 | -------------------------------------------------------------------------------- /docs/functionali.rst: -------------------------------------------------------------------------------- 1 | functionali package 2 | =================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | functionali.higher\_order\_functions module 8 | ------------------------------------------- 9 | 10 | .. automodule:: functionali.higher_order_functions 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | functionali.predicates module 16 | ----------------------------- 17 | 18 | .. automodule:: functionali.predicates 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | functionali.seq\_transform module 24 | --------------------------------- 25 | 26 | .. automodule:: functionali.seq_transform 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | functionali.seq\_traverse module 32 | -------------------------------- 33 | 34 | .. automodule:: functionali.seq_traverse 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | functionali.statements module 40 | ----------------------------- 41 | 42 | .. automodule:: functionali.statements 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | Module contents 48 | --------------- 49 | 50 | .. automodule:: functionali 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | -------------------------------------------------------------------------------- /docs/higher-order-functions.rst: -------------------------------------------------------------------------------- 1 | Higher order functions 2 | ====================== 3 | 4 | .. module:: functionali 5 | 6 | .. autosummary:: 7 | reduce 8 | reduced 9 | flip 10 | foldr 11 | curry 12 | threadf 13 | threadl 14 | trampoline 15 | comp 16 | 17 | .. autofunction:: reduce 18 | 19 | .. autofunction:: reduced 20 | 21 | .. autofunction:: flip 22 | 23 | .. autofunction:: foldr 24 | 25 | .. autofunction:: curry 26 | 27 | .. autofunction:: threadf 28 | 29 | .. autofunction:: threadl 30 | 31 | .. autofunction:: trampoline 32 | 33 | .. autofunction:: comp 34 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | 2 | Welcome to Functionali's documentation! 3 | ======================================= 4 | Putting the fun in functional programming 😉 5 | 6 | .. image:: https://codecov.io/gh/AbhinavOmprakash/functionali/branch/main/graph/badge.svg?token=75LLE4F7EY 7 | :target: https://codecov.io/gh/AbhinavOmprakash/functionali 8 | 9 | .. image:: https://readthedocs.org/projects/functionali/badge/?version=latest 10 | :target: https://functionali.readthedocs.io/en/latest/?badge=latest 11 | :alt: Documentation Status 12 | 13 | Functional programming is a fundamentally different way of solving problems, and once It clicks, it's pure joy after that. 14 | A lot of ideas in this library have been taken from Clojure and Haskell, so the credit goes to those languages. 15 | If you find your favorite function missing, or find ways to improve this project, I'd love to hear it. 16 | 17 | There are quite a few functions in the library, And they can seem quite overwhelming at first. 18 | These functions can be divided into four major categories- 19 | 20 | #. Higher order functions. For example ``foldr``, ``curry``, ``flip``, 21 | #. Sequence traversing functions. For example ``first``, ``rest``, ``last``. 22 | #. Sequence transforming functions. For example ``cons``, ``concat``, ``flatten``. 23 | #. predicates. For example ``is_even``, ``is_prime``, ``is_nested``. 24 | 25 | .. toctree:: 26 | :maxdepth: 2 27 | 28 | seq-traverse 29 | seq-transform 30 | predicates 31 | higher-order-functions 32 | 33 | 34 | 35 | Indices and tables 36 | ================== 37 | 38 | * :ref:`genindex` 39 | * :ref:`modindex` 40 | * :ref:`search` 41 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | functionali 2 | =========== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | functionali 8 | -------------------------------------------------------------------------------- /docs/predicates.rst: -------------------------------------------------------------------------------- 1 | Predicates 2 | ========== 3 | 4 | .. module:: functionali 5 | 6 | .. autosummary:: 7 | 8 | identity 9 | equals 10 | is_ 11 | less_than 12 | greater_than 13 | less_than_eq 14 | greater_than_eq 15 | complement 16 | is_even 17 | is_odd 18 | is_divisible 19 | is_divisible_by 20 | is_numeric 21 | is_atom 22 | contains 23 | is_empty 24 | is_nested 25 | all_predicates 26 | some_predicates 27 | 28 | .. autofunction:: identity 29 | 30 | .. autofunction:: equals 31 | 32 | .. autofunction:: is_ 33 | 34 | .. autofunction:: less_than 35 | 36 | .. autofunction:: greater_than 37 | 38 | .. autofunction:: less_than_eq 39 | 40 | .. autofunction:: greater_than_eq 41 | 42 | .. autofunction:: complement 43 | 44 | .. autofunction:: is_even 45 | 46 | .. autofunction:: is_odd 47 | 48 | .. autofunction:: is_divisible 49 | 50 | .. autofunction:: is_divisible_by 51 | 52 | .. autofunction:: is_numeric 53 | 54 | .. autofunction:: is_atom 55 | 56 | .. autofunction:: contains 57 | 58 | .. autofunction:: is_empty 59 | 60 | .. autofunction:: is_nested 61 | 62 | .. autofunction:: all_predicates 63 | 64 | .. autofunction:: some_predicates -------------------------------------------------------------------------------- /docs/seq-transform.rst: -------------------------------------------------------------------------------- 1 | Sequence transforming functions 2 | =============================== 3 | 4 | .. module:: functionali 5 | 6 | .. autosummary:: 7 | cons 8 | conj 9 | concat 10 | argmap 11 | argzip 12 | unzip 13 | interleave 14 | flatten 15 | insert 16 | remove 17 | 18 | .. autofunction:: remove 19 | 20 | .. autofunction:: cons 21 | 22 | .. autofunction:: conj 23 | 24 | .. autofunction:: concat 25 | 26 | .. autofunction:: argmap 27 | 28 | .. autofunction:: argzip 29 | 30 | .. autofunction:: unzip 31 | 32 | .. autofunction:: interleave 33 | 34 | .. autofunction:: flatten 35 | 36 | .. autofunction:: insert -------------------------------------------------------------------------------- /docs/seq-traverse.rst: -------------------------------------------------------------------------------- 1 | Sequence traversing functions 2 | ======================================= 3 | 4 | .. module:: functionali 5 | 6 | .. autosummary:: 7 | first 8 | rest 9 | ffirst 10 | second 11 | third 12 | fourth 13 | fifth 14 | last 15 | butlast 16 | take 17 | drop 18 | take_while 19 | drop_while 20 | split_with 21 | iter_ 22 | reversed_ 23 | count 24 | count_ 25 | 26 | 27 | .. autofunction:: first 28 | 29 | .. autofunction:: rest 30 | 31 | .. autofunction:: ffirst 32 | 33 | .. autofunction:: second 34 | 35 | .. autofunction:: third 36 | 37 | .. autofunction:: fourth 38 | 39 | .. autofunction:: fifth 40 | 41 | .. autofunction:: last 42 | 43 | .. autofunction:: butlast 44 | 45 | .. autofunction:: take 46 | 47 | .. autofunction:: drop 48 | 49 | .. autofunction:: take_while 50 | 51 | .. autofunction:: drop_while 52 | 53 | .. autofunction:: split_with 54 | 55 | .. autofunction:: iter_ 56 | 57 | .. autofunction:: reversed_ 58 | 59 | .. autofunction:: count 60 | 61 | .. autofunction:: count_ 62 | 63 | -------------------------------------------------------------------------------- /functionali/__init__.py: -------------------------------------------------------------------------------- 1 | from .higher_order_functions import * 2 | from .predicates import * 3 | from .seq_transform import * 4 | from .seq_traverse import * 5 | 6 | __all__ = [ 7 | # higher order functions 8 | "lazymap", 9 | "lazyfilter", 10 | "map", 11 | "filter", 12 | "flip", 13 | "foldr", 14 | "comp", 15 | "curry", 16 | "reduce", 17 | "reduced", 18 | "trampoline", 19 | "threadf", 20 | "threadl", 21 | # sequence traversing functions 22 | "iter_", 23 | "reversed_", 24 | "first", 25 | "ffirst", 26 | "last", 27 | "rest", 28 | "second", 29 | "third", 30 | "fourth", 31 | "fifth", 32 | "butlast", 33 | "take", 34 | "drop", 35 | "take_while", 36 | "drop_while", 37 | "split_with", 38 | "count", 39 | "count_", 40 | 41 | # sequence transforming functions 42 | "cons", 43 | "conj", 44 | "concat", 45 | "argmap", 46 | "argzip", 47 | "unzip", 48 | "interleave", 49 | "flatten", 50 | "insert", 51 | "remove", 52 | "tuplize", 53 | 54 | # predicates 55 | "identity", 56 | "equals", 57 | "is_", 58 | "less_than", 59 | "less_than_eq", 60 | "greater_than", 61 | "greater_than_eq", 62 | "complement", 63 | "is_even", 64 | "is_odd", 65 | "is_prime", 66 | "is_divisible_by", 67 | "is_numeric", 68 | "is_atom", 69 | "contains", 70 | "is_empty", 71 | "is_nested", 72 | "all_predicates", 73 | "some_predicates", 74 | ] 75 | -------------------------------------------------------------------------------- /functionali/higher_order_functions.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Any, Iterable, Union, Tuple, Generator 2 | from inspect import signature 3 | from functools import partial 4 | from .seq_traverse import iter_, reversed_ 5 | 6 | 7 | class lazymap: 8 | """Similar to python's map but returns a new generator 9 | everytime ``__iter__`` is called on it. 10 | Prefer using `functionali.map` unless you know what you're doing. 11 | 12 | 13 | >>> res = lazymap(lambda x:x+1, range(100)) 14 | >>> take(5, res) 15 | (1, 2, 3, 4, 5) 16 | >>> take(5, res) 17 | (1, 2, 3, 4, 5) # same result 18 | 19 | Compare this to python's implementation 20 | 21 | >>> res = map(lambda x:x+1, range(100)) 22 | >>> take(5, res) 23 | (1, 2, 3, 4, 5) 24 | >>> take(5, res) 25 | (6, 7, 8, 9, 10) 26 | 27 | 28 | Also ``__repr__`` is implemented to make repl-driven development easy :) 29 | 30 | >>> lazymap(lambda x:x+1,range(10)) 31 | (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) 32 | """ 33 | 34 | def __init__(self, fn: Callable, *iterables: Iterable) -> None: 35 | self.fn = fn 36 | self.iterables = iterables 37 | 38 | def __iter__(self) -> Generator: 39 | return (self.fn(*i) for i in zip(*self.iterables)) 40 | 41 | def __repr__(self): # pragma: no cover 42 | return str(tuple(self)) 43 | 44 | 45 | def map(fn: Callable, *iterables: Iterable) -> Tuple: 46 | """Map with eager evaluation. 47 | Prefer this over ``lazymap``. 48 | 49 | 50 | >>> map(lambda x:x+1, range(10)) 51 | (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) 52 | """ 53 | return tuple([fn(*i) for i in zip(*iterables)]) 54 | 55 | 56 | class lazyfilter: 57 | """Similar to python's filter but returns a new generator 58 | everytime ``__iter__`` is called on it. 59 | Prefer using `functionali.filter` unless you know what you're doing. 60 | 61 | >>> res = lazyfilter(is_even, range(10)) 62 | >>> take(2, res) 63 | (0, 2) 64 | >>> take(2, res) 65 | (0, 2) # same result 66 | 67 | Compare this to python's implementation 68 | 69 | # you get a different result everytime 70 | >>> res = filter(is_even, range(10)) 71 | >>> take(2, res) 72 | (0, 2) 73 | >>> take(2, res) 74 | (4, 6) 75 | >>> take(2, res) 76 | (8,) 77 | 78 | Also ``__repr__`` is implemented to make repl-driven development easy :) 79 | 80 | >>> lazyfilter(is_even, range(10)) 81 | (0, 2, 4, 6, 8) 82 | 83 | """ 84 | 85 | def __init__(self, fn: Callable, iterable: Iterable) -> None: 86 | self.fn = fn 87 | self.iterable = iterable 88 | 89 | def __iter__(self) -> Generator: 90 | return (i for i in self.iterable if self.fn(i)) 91 | 92 | def __repr__(self): # pragma: no cover 93 | return str(tuple(self)) 94 | 95 | 96 | def filter(fn: Callable, iterable: Iterable) -> Tuple: 97 | """filter with eager evaluation. 98 | Prefer this over ``lazyfilter``. 99 | 100 | >>> filter(is_even, range(10)) 101 | (0, 2, 4, 6, 8) 102 | """ 103 | return tuple([i for i in iterable if fn(i)]) 104 | 105 | 106 | def flip(fn: Callable) -> Callable: 107 | """returns a function that takes in a flipped order of args. 108 | Usage: 109 | 110 | >>> f = lambda a,b : a-b 111 | >>> f(1,3) 112 | -2 113 | >>> f(3,1) 114 | 2 115 | >>> flipped_f = flip(f) 116 | >>> flipped_f(3,1) 117 | -2 118 | >>> flipped_f(1,3) 119 | 2 120 | 121 | Added in version: 0.1.0 122 | """ 123 | 124 | def flipped(*args, **kwargs): 125 | return fn(*reversed_(args), **kwargs) 126 | 127 | return flipped 128 | 129 | 130 | def reduce(fn, iterable, initial=None): 131 | """Similar to python's reduce, but can be prematurely terminated with ``reduced``. 132 | Works with dictionaries too. 133 | 134 | Usage: 135 | 136 | >>> # Reducing over dictionaries. 137 | >>> def inc_value(result, kv_pair): 138 | k = kv_pair[0] 139 | v = kv_pair[1] 140 | return result[k]= v+1 141 | >>> reduce(inc_value, {"a":1,"b":2}, {}) 142 | {'a': 2, 'b': 3} 143 | 144 | >>> #premature termination with reduced 145 | >>> def inc_while_odd(result, element): 146 | if element%2==0: 147 | return reduced(result) 148 | else: 149 | result.append(element+1) 150 | return result 151 | >>> reduce(inc_while_odd, [1,3,5,6,7,8],[]) 152 | [2, 4, 6] 153 | # increments uptil 5 (third element) and prematurely terminates. 154 | 155 | """ 156 | it = iter_(iterable) 157 | 158 | if initial is None: 159 | result = next(it) 160 | else: 161 | result = initial 162 | 163 | for element in it: 164 | result = fn(result, element) 165 | 166 | # check if reduce needs to be prematurely terminated. 167 | if isinstance(result, Reduced): 168 | return result() # call result to get reduced value. 169 | 170 | return result 171 | 172 | 173 | def reduced(x): 174 | """Use with ``functionali.reduce`` to prematurely terminate ``reduce`` with the value of ``x``. 175 | 176 | Usage: 177 | 178 | >>> reduce(lambda acc, el: reduced("!"), [1,3,4]) 179 | "!" 180 | # reduce is prematurely terminated and returns a value of "!" 181 | """ 182 | return Reduced(x) 183 | 184 | 185 | class Reduced: 186 | def __init__(self, x): 187 | self.x = x 188 | 189 | def __call__(self): 190 | return self.x 191 | 192 | 193 | def foldr(fn: Callable, iterable: Iterable, initial: Any = None) -> Any: 194 | """Fold right. Stack safe implementation 195 | 196 | Added in version: 0.1.0 197 | """ 198 | # Implement with reduce, for this we reverse the iterable 199 | # and then we flip fn since the Function signature for fn is fn(element, accumulator) 200 | # This is the standard function signature of any function passed to Foldright 201 | # the flipping is necessary because reduce expects the reducing function to have a function signature of 202 | # fn(accumulator, element) Which is the flipped signature of fn(element, accumulator) 203 | reversed_it = reversed_(iterable) 204 | 205 | if initial is None: 206 | initial = next(reversed_it) 207 | 208 | return reduce(flip(fn), reversed_it, initial) 209 | 210 | 211 | def comp(*fns: Callable): 212 | """ 213 | returns a composed function that takes a variable number of args, 214 | and applies it to ``fns`` passed from right to left. 215 | 216 | Added in version: 0.1.2 217 | """ 218 | 219 | def composed(*args, **kwargs): 220 | first_func = fns[-1] # since we are applying functions from right to left 221 | return foldr(lambda f, arg: f(arg), fns[:-1], first_func(*args, **kwargs)) 222 | 223 | return composed 224 | 225 | 226 | def curry(fn: Callable) -> Callable: 227 | """Returns a curried version of the function. 228 | 229 | >>> def fn(arg1, arg2, arg3): # test function 230 | return [arg1, arg2, arg3] 231 | >>> curried_fn = curry(fn) 232 | >>> curried_fn(1)(2)(3) 233 | [1, 2, 3] 234 | 235 | Added in version: 0.1.0 236 | """ 237 | num_args = len(signature(fn).parameters) 238 | 239 | def curried(arg, fn, num_args): 240 | if num_args == 1: 241 | # call fn if the final arg is passed 242 | return fn(arg) 243 | else: 244 | # call partial fn if not final arg 245 | f = partial(fn, arg) 246 | return lambda arg: curried(arg, f, num_args - 1) 247 | 248 | if num_args == 0: 249 | return fn 250 | 251 | return lambda arg: curried(arg, fn, num_args) 252 | 253 | 254 | def trampoline(fn: Callable, *args: Any): 255 | """takes a function ``fn`` and calls if with ``*args``. if ``fn`` returns a function, 256 | calls the function until a function is not returned i.e. the base case is reached. 257 | function ``fn`` must return a function in its recursive case. 258 | Useful for optimizing tail recursive functions or mutual recursions. 259 | 260 | 261 | >>> def fact(x, curr=1, acc=1): 262 | >>> if curr == x: 263 | >>> return curr*acc 264 | >>> else: 265 | >>> return lambda: fact(x, curr+1, acc*curr) 266 | >>> trampoline(fact, 3) == 6 267 | >>> trampoline(fact, 100000000000) # does not raise RecursionError 268 | """ 269 | recursive_fn = fn(*args) 270 | while isinstance(recursive_fn, Callable): 271 | recursive_fn = recursive_fn() 272 | return recursive_fn 273 | 274 | 275 | def threadf(arg: Any, forms: Iterable[Union[Callable, Iterable]]) -> Any: 276 | """Thread first, passes ``arg`` as the first argument to the first function in ``forms`` 277 | and passes the result as the first argument to the second form and so on. 278 | 279 | see also ``threadl``. 280 | 281 | >>> from functionali import identity 282 | >>> from operator import add, sub, mul 283 | >>> threadf(5, [identity]) 284 | >>> 5 285 | 286 | >>> threadf(5, [identity, [add, 2]]) 287 | >>> 7 288 | 289 | >>> threadf(5, [[sub, 2]]) 290 | >>> 3 # threadf(5, [[sub, 2]]) -> sub(5, 2) -> 5-2 -> 3 291 | 292 | 293 | >>> # combining multiple functions 294 | >>> threadf(5, [identity, (add, 1), (sub, 1), (mul, 3)]) 295 | 15 296 | """ 297 | 298 | def fn(result, form): 299 | if isinstance(form, Iterable): 300 | fn = form[0] 301 | args = form[1:] 302 | return fn(result, *args) 303 | else: 304 | return form(result) 305 | 306 | return reduce(fn, forms, arg) 307 | 308 | 309 | def threadl(arg: Any, forms: Iterable[Union[Callable, Iterable]]) -> Any: 310 | """Thread last, passes ``arg`` as the last argument to the first function in ``forms`` 311 | and passes the result as the last argument to the second form and so on. 312 | 313 | see also ``threadf``. 314 | 315 | >>> from functionali import identity 316 | >>> from operator import add, sub, mul 317 | >>> threadl(5, [identity]) 318 | >>> 5 319 | 320 | >>> threadl(5, [identity, [add, 2]]) 321 | >>> 7 322 | 323 | >>> threadl(5, [[sub, 2]]) 324 | >>> -3 # threadl(5, [[sub, 2]]) -> sub(2, 5) -> 2-5 -> -3 325 | 326 | >>> # combining multiple functions 327 | >>> threadl(5, [identity, (add, 1), (sub, 1), (mul, 3)]) 328 | -15 329 | """ 330 | 331 | def fn(result, form): 332 | if isinstance(form, Iterable): 333 | fn = form[0] 334 | args = form[1:] 335 | return fn(*args, result) 336 | else: 337 | return form(result) 338 | 339 | return reduce(fn, forms, arg) 340 | -------------------------------------------------------------------------------- /functionali/predicates.py: -------------------------------------------------------------------------------- 1 | """A file containing useful predicates.""" 2 | 3 | from typing import List, Iterable, Callable, Any, Sequence, Union, Tuple, Iterator 4 | 5 | from .higher_order_functions import partial, flip, reduce 6 | from operator import lt, le, ge, gt 7 | 8 | 9 | def identity(x): 10 | """Returns its argument as it is.""" 11 | return x 12 | 13 | 14 | def equals(a, b=None, *args): 15 | """ 16 | if only ``a`` is passed, a function is returned that returns True when the ``arg`` passed to it is equal 17 | to ``a``; else returns True when ``a``,``b`` and ``*args`` are equal. 18 | 19 | with one argument 20 | 21 | >>> equals_one = equals(1) 22 | >>> equals_one(1) 23 | True 24 | >>> equals_one(2) 25 | False 26 | 27 | with two or more arguments 28 | 29 | >>> equals(1,1,1) 30 | True 31 | >>> equals(1,1,2) 32 | False 33 | 34 | Added in version: 0.1.0 35 | """ 36 | if not b: 37 | return lambda x: a == x 38 | elif not args: 39 | return a == b 40 | else: 41 | # TODO ugly. refactor 42 | if a == b: 43 | for arg in args: 44 | if a != arg: 45 | return False 46 | else: 47 | return True 48 | else: 49 | return False 50 | 51 | 52 | def is_(a, b=None, *args): 53 | """ 54 | if only ``a`` is passed, a function is returned that returns True when the ``arg`` passed is the same object 55 | as ``a`` ; else returns True when ``a``,``b`` and ``*args`` are. 56 | 57 | with one argument 58 | 59 | >>> d1 = {1,2,3} 60 | >>> d2 = {1,2,3} 61 | >>> is_d1 = is_(d1) 62 | >>> is_d1(d2) 63 | >>> False 64 | >>> d1 == d2 65 | >>> True 66 | 67 | with two or more arguments 68 | 69 | >>> is_(d1,d1) 70 | >>> True 71 | >>> is_(d1,d1,d2) 72 | >>> False 73 | 74 | 75 | Added in version: 0.1.0 76 | """ 77 | if not b: 78 | return lambda x: a is x 79 | elif not args: 80 | return a is b 81 | else: 82 | # TODO ugly. refactor 83 | if a is b: 84 | for arg in args: 85 | if not a is arg: 86 | return False 87 | else: 88 | return True 89 | else: 90 | return False 91 | 92 | 93 | def less_than(a, b=None, *args): 94 | """ 95 | if only ``a`` is passed, a function is returned that returns True when the ``arg`` passed to is less than ``a``; 96 | else returns True when ``a`` is less than``b`` and ``*args``. 97 | 98 | with one argument 99 | 100 | >>> less_than_one = less_than(1) 101 | >>> less_than_one(2) 102 | False 103 | >>> less_than_one(0) 104 | True 105 | 106 | 107 | with two or more arguments 108 | 109 | >>> less_than(1,2) 110 | >>> True 111 | >>> less_than(1,2,3) 112 | True 113 | >>> less_than(1,2,3,1) 114 | False 115 | 116 | Useful to use with filter 117 | 118 | >>> list(filter(less_than(5),range(10))) 119 | [0,1,2,3,4] 120 | 121 | Added in version: 0.1.0 122 | """ 123 | if not b: 124 | return lambda x: x < a 125 | elif not args: 126 | return a < b 127 | else: 128 | 129 | def f(boolean, el, args): 130 | if not boolean: 131 | return False 132 | elif not args: 133 | return boolean 134 | else: 135 | return f(lt(el, args[0]), args[0], args[1:]) 136 | 137 | return f(lt(a, b), b, args) 138 | 139 | 140 | def less_than_eq(a, b=None, *args): 141 | """ 142 | if only ``a`` is passed, a function is returned that returns True when the ``arg`` less than or equal to ``a``; 143 | else returns True when ``a`` is less than or equal to ``b`` and ``*args``. 144 | 145 | with one argument 146 | 147 | >>> less_than_or_eq_to_one = less_than_eq(1) 148 | >>> less_than_or_eq_to_one(2) 149 | False 150 | >>> less_than_or_eq_to_one(1) 151 | True 152 | 153 | with two or more arguments 154 | 155 | >>> less_than_eq(1,2) 156 | >>> True 157 | >>> less_than_eq(1,2,3) 158 | True 159 | >>> less_than_eq(1,2,3,1) 160 | True 161 | 162 | Useful to use with filter 163 | 164 | >>> list(filter(less_than_eq(5),range(10))) 165 | [0,1,2,3,4,5] 166 | 167 | Added in version: 0.1.0 168 | """ 169 | if not b: 170 | return lambda x: x <= a 171 | elif not args: 172 | return a <= b 173 | else: 174 | # TODO ugly. refactor 175 | def f(boolean, el, args): 176 | if not boolean: 177 | return False 178 | elif not args: 179 | return boolean 180 | else: 181 | return f(le(el, args[0]), args[0], args[1:]) 182 | 183 | return f(le(a, b), b, args) 184 | 185 | 186 | def greater_than(a, b=None, *args): 187 | """ 188 | if only ``a`` is passed, a function is returned that returns True when the ``arg`` passed to is greater than ``a``; 189 | else returns True when ``a`` is greater than``b`` and ``*args``. 190 | 191 | with one argument 192 | 193 | >>> greater_than_one = greater_than(1) 194 | >>> greater_than_one(2) 195 | True 196 | >>> greater_than_one(0) 197 | False 198 | 199 | with two or more arguments 200 | 201 | >>> greater_than(2,1) 202 | >>> True 203 | >>> greater_than(3,2,1) 204 | True 205 | >>> greater_than(3,2,1,3) 206 | False 207 | 208 | Useful to use with filter 209 | 210 | >>> list(filter(greater_than(5),range(10))) 211 | [6,7,8,9] 212 | 213 | Added in version: 0.1.0 214 | """ 215 | if not b: 216 | return lambda x: x > a 217 | elif not args: 218 | return a > b 219 | else: 220 | # TODO ugly. refactor 221 | def f(boolean, el, args): 222 | if not boolean: 223 | return False 224 | elif not args: 225 | return boolean 226 | else: 227 | return f(gt(el, args[0]), args[0], args[1:]) 228 | 229 | return f(gt(a, b), b, args) 230 | 231 | 232 | def greater_than_eq(a, b=None, *args): 233 | """ 234 | if only ``a`` is passed, a function is returned that returns True when the ``arg`` greater than or equal to ``a``; 235 | else returns True when ``a`` is greater than or equal to ``b`` and ``*args``. 236 | 237 | with one argument 238 | 239 | >>> greater_than_eq_one = greater_than_eq(1) 240 | >>> greater_than_eq_one(2) 241 | True 242 | >>> greater_than_eq_one(1) 243 | True 244 | 245 | with two or more arguments 246 | 247 | >>> greater_than_eq(2,1) 248 | >>> True 249 | >>> greater_than_eq(3,2,1) 250 | True 251 | >>> greater_than_eq(3,2,1,3) 252 | True 253 | 254 | Useful to use with filter 255 | 256 | >>> list(filter(greater_than_eq(5),range(10))) 257 | [5,6,7,8,9] 258 | 259 | Added in version: 0.1.0 260 | """ 261 | if not b: 262 | return lambda x: x >= a 263 | elif not args: 264 | return a >= b 265 | else: 266 | # TODO ugly. refactor 267 | def f(boolean, el, args): 268 | if not boolean: 269 | return False 270 | elif not args: 271 | return boolean 272 | else: 273 | return f(ge(el, args[0]), args[0], args[1:]) 274 | 275 | return f(ge(a, b), b, args) 276 | 277 | 278 | def complement( 279 | expr: Union[bool, Callable[[Any], bool]] 280 | ) -> Union[bool, Callable[[Any], bool]]: 281 | """Takes in a predicate or a Boolean expression and 282 | returns a negated version of the predicate or expression. 283 | 284 | >>> complement(True) 285 | >>> False 286 | 287 | >>> def fn(el): # returns the Boolean of el 288 | return bool(el) 289 | >>> negated_fn = complement(fn) 290 | >>> fn(1) 291 | >>> True 292 | >>> negated_fn(1) 293 | >>> False 294 | 295 | Added in version: 0.1.0 296 | """ 297 | 298 | if not isinstance(expr, Callable): 299 | # Wrapped around bool to handle cases like not_(1). 300 | # where 1 returns a boolean value of true. 301 | return not bool(expr) 302 | 303 | else: 304 | 305 | def negated(*args, **kwargs): 306 | return not expr(*args, **kwargs) 307 | 308 | return negated 309 | 310 | 311 | def is_even(num: int) -> bool: 312 | """Returns true when num is even. 313 | 314 | Added in version: 0.1.0 315 | """ 316 | return num % 2 == 0 317 | 318 | 319 | def is_odd(num: int) -> bool: 320 | """Returns true when num is odd 321 | 322 | Added in version: 0.1.0 323 | """ 324 | return num % 2 != 0 325 | 326 | 327 | def is_prime(num: int) -> bool: 328 | """Returns true when num is prime 329 | 330 | Added in version: 0.1.0 331 | """ 332 | if is_even(num) and num != 2: 333 | # You don't need to compute the whole Sieve if num is even. 334 | return False 335 | 336 | primes = [True for i in range(num + 1)] 337 | p = 2 338 | primes[0] = False 339 | primes[1] = False 340 | 341 | while p * p <= num: 342 | if primes[p] == True: 343 | # Update all multiples of p 344 | for i in range(p * 2, num + 1, p): 345 | primes[i] = False 346 | 347 | p += 1 348 | 349 | return primes[num] 350 | 351 | 352 | def is_divisible(divident: Union[int, float], divisor: Union[int, float]) -> bool: 353 | """Returns true if dividend is divisible by divisor. 354 | 355 | Added in version: 0.1.0 356 | """ 357 | return divident % divisor == 0 358 | 359 | 360 | def is_divisible_by(divisor: Union[int, float]) -> Callable[[Union[int, float]], bool]: 361 | """Takes a ``divisor`` And returns a function (closure) That expects a dividend. 362 | returns true if it passes the divisibility test. 363 | for e.g. 364 | 365 | >>> f = is_divisible_by(5) 366 | >>> f(10) 367 | True 368 | >>> f(7) 369 | False 370 | 371 | This is particularly useful to use with a filter. 372 | 373 | >>> list(filter(is_divisible_by(5), [1,2,3,4,5,6,7,8,9,10])) 374 | [5, 10] 375 | 376 | Suppose you want to filter out numbers that are divisible by 2 or 3 377 | 378 | >>> list(filter(some_predicates([is_divisible_by(2), is_divisible_by(3)]), range(1, 10))) 379 | [2, 3, 4, 6, 8, 9, 10] 380 | 381 | Added in version: 0.1.0 382 | """ 383 | return lambda dividend: dividend % divisor == 0 384 | 385 | 386 | def is_numeric(entity: Any) -> bool: 387 | """Return True if ``entity`` Is an ``int``, ``float``, or a ``complex``. 388 | 389 | Added in version: 0.1.0 390 | """ 391 | return any(map(isinstance, [entity, entity, entity], [int, float, complex])) 392 | 393 | 394 | def is_atom(entity: Any) -> bool: 395 | """Everything that is NOT an iterable(except strings) are considered atoms. 396 | 397 | >>> is_atom("plain string") 398 | True 399 | >>> is_atom(1) 400 | True 401 | >>> is_atom([1, 2]) 402 | False 403 | 404 | Added in version: 0.1.0 405 | """ 406 | if isinstance(entity, str): 407 | return True 408 | else: 409 | return not isinstance(entity, Iterable) 410 | 411 | 412 | def contains(entity: Any, collection: Iterable) -> bool: 413 | """Checks whether collection contains the given entity. 414 | Note, won't automatically convert a tuple of keys and values to a ``dict``. 415 | 416 | Added in version: 0.1.0 417 | """ 418 | return entity in collection 419 | 420 | 421 | def is_empty(collection: Iterable) -> bool: 422 | """Returns true if the collection is empty. 423 | 424 | Added in version: 0.1.0 425 | """ 426 | return not bool(collection) 427 | 428 | 429 | def is_nested(collection: Iterable) -> bool: 430 | """returns true if a collection is nested. 431 | Added in version: 0.1.0 432 | """ 433 | if isinstance(collection, dict): 434 | return any(map(lambda x: isinstance(x, dict), collection.values())) 435 | else: 436 | return any(map(complement(is_atom), collection)) 437 | 438 | 439 | def all_predicates(*predicates: Callable[[Any], bool]) -> Callable[[Any], bool]: 440 | """Takes a set of predicates and returns a function that takes an entity 441 | and checks if it satisfies all the predicates. 442 | 443 | >>> even_and_prime = all_predicates(is_even, is_prime) 444 | >>> even_and_prime(2) 445 | True 446 | >>> even_and_prime(4) 447 | False 448 | >>> even_and_prime(3) 449 | False 450 | 451 | Added in version: 0.1.0 452 | """ 453 | return lambda entity: all((p(entity) for p in predicates)) 454 | 455 | 456 | def some_predicates(*predicates: Callable[[Any], bool]) -> Callable[[Any], bool]: 457 | """Takes a set of predicates and returns a function that takes an entity 458 | and checks if it satisfies some of the predicates. 459 | 460 | >>> even_or_prime = some_predicates(is_even, is_prime) 461 | >>> even_or_prime(2) 462 | True 463 | >>> even_and_prime(4) 464 | True 465 | >>> even_and_prime(3) 466 | True 467 | 468 | Added in version: 0.1.0 469 | """ 470 | 471 | return lambda entity: any((p(entity) for p in predicates)) 472 | -------------------------------------------------------------------------------- /functionali/seq_transform.py: -------------------------------------------------------------------------------- 1 | """Functions that transform sequences""" 2 | from collections import deque 3 | from .predicates import is_atom, is_nested, complement 4 | from .higher_order_functions import reduce 5 | 6 | from .seq_traverse import iter_ 7 | 8 | from typing import ( 9 | Callable, 10 | Iterable, 11 | Generator, 12 | Any, 13 | Tuple, 14 | Union, 15 | Dict, 16 | Tuple, 17 | List, 18 | ) 19 | 20 | 21 | def cons(arg: Any, iterable: Iterable) -> deque: 22 | """Returns a deque with arg as the first element. 23 | 24 | Adds to the left of a deque. 25 | 26 | >>> cons(5, [1,2,3,4]) 27 | deque([5, 1, 2, 3, 4]) 28 | 29 | >>> cons(3, deque([1,2])) 30 | deque([3, 1, 2]) 31 | 32 | >>> cons((3, "c"), {1:"a", 2: "b"}) 33 | deque([(3, "c"), (1, "a"), (2, "b")]) 34 | 35 | Added in version: 0.1.0 36 | """ 37 | 38 | if isinstance(iterable, dict): 39 | dq = deque(iterable.items()) 40 | else: 41 | dq = deque(iterable) 42 | 43 | dq.appendleft(arg) 44 | 45 | return dq 46 | 47 | 48 | def conj(iterable: Iterable, *args: Any) -> Iterable: 49 | """Short for conjoin, adds element to the iterable, at the appropriate end. 50 | Adds to the left of a deque. 51 | 52 | >>> conj([1,2,3,4],5) 53 | [1, 2, 3, 4, 5] 54 | 55 | >>> conj(deque([1,2]), 3,4) 56 | deque([4, 3, 1, 2]) 57 | 58 | >>> conj([1,2,3,4],5,6,7,8) 59 | [1, 2, 3, 4, 5, 6, 7, 8] 60 | 61 | >>> conj([1,2,3,4],[5,6,7]) 62 | [1, 2, 3, 4, [5, 6, 7]] 63 | 64 | >>> conj((1,2,3,4),5,6,7) 65 | (1, 2, 3, 4, 5, 6, 7) 66 | 67 | >>> conj(range(10), 11) 68 | (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11) 69 | 70 | >>> conj({1:"a", 2:"b"}, {3:"c"}) 71 | {1: 'a', 2: 'b', 3: 'c'} 72 | 73 | Added in version: 0.1.0 74 | """ 75 | if isinstance(iterable, list): 76 | 77 | return iterable + list(args) # since args is a tuple 78 | 79 | elif isinstance(iterable, tuple): 80 | return iterable + args 81 | 82 | elif isinstance(iterable, dict): 83 | # Make a copy of iterable, instead of mutating it. 84 | conjed_dict = {**iterable} 85 | 86 | for d in args: 87 | conjed_dict.update(d) 88 | 89 | return conjed_dict 90 | 91 | elif isinstance(iterable, set): 92 | return {*iterable, *args} 93 | 94 | elif isinstance(iterable, deque): 95 | dq = deque(iterable) 96 | for element in args: 97 | dq.appendleft(element) 98 | return dq 99 | 100 | else: 101 | return tuple(iterable) + args 102 | 103 | 104 | def concat(iterable, *args): 105 | """Add items to the end of the iterable. 106 | 107 | >>> concat([1,2,3,4],5) 108 | [1, 2, 3, 4, 5] 109 | >>> concat(deque([1,2]), 3,4) 110 | deque([1, 2, 3, 4]) 111 | 112 | Added in version: 0.1.0 113 | """ 114 | 115 | if isinstance(iterable, deque): 116 | dq = deque(iterable) 117 | 118 | for element in args: 119 | dq.append(element) 120 | 121 | return dq 122 | 123 | else: 124 | # Since conj Behavior for data Structures other than 125 | # deque are similar to concat behaviour. 126 | # call conj 127 | return conj(iterable, *args) 128 | 129 | 130 | def argmap(functions: Iterable[Callable], args: Iterable) -> Generator: 131 | """Maps the same argument(s) to multiple functions. 132 | 133 | >>> inc = lambda x:x+1 134 | >>> dec = lambda x:x-1 135 | >>> list(argmap([inc, dec],[1])) 136 | [2,0] 137 | 138 | you can even map multiple arguments 139 | 140 | >>> add = lambda a,b: a+b 141 | >>> sub = lambda a,b: a-b 142 | >>> list(argmap([add, sub], [2, 1])) # two arguments 143 | [3, 1] 144 | 145 | Added in version: 0.1.0 146 | """ 147 | return (f(*args) for f in functions) 148 | 149 | 150 | def argzip(sequence: Iterable[Callable], *args: Any) -> Generator: 151 | """ 152 | Similar to zip, but instead of zipping iterables, 153 | It zips an argument(s) with all the values of the iterable. 154 | for example. 155 | 156 | >>> list(argzip([1,2,3,4], "number")) 157 | [(1, 'number'), (2, 'number'), (3, 'number'), (4, 'number')] 158 | >>> list(argzip([1,2,3,4], "number", "int")) 159 | [(1, 'number', 'int'), (2, 'number', 'int'), (3, 'number', 'int'), (4, 'number', 'int')] 160 | 161 | Added in version: 0.1.0 162 | """ 163 | return ((a, *args) for a in sequence) 164 | 165 | 166 | def unzip(sequence: Iterable) -> Tuple[Any]: 167 | """Opposite of zip. Unzip is shallow. 168 | 169 | >>> unzip([[1,'a'], [2,'b'], [3,'c']]) 170 | ((1, 2, 3), ('a', 'b', 'c')) 171 | >>> unzip([ [1,'a','A'], [2, 'b','B'], [3,'c','C'] ]) 172 | ((1, 2, 3), ('a', 'b', 'c'), ('A', 'B', 'C')) 173 | 174 | shallow nature of unzip. 175 | 176 | >>> unzip([ [[1,'num'],['a','str']], [[2,'num'],['b','str']] ]) 177 | (([1, 'num'], [2, 'num']), (['a', 'str'], ['b', 'str'])) 178 | 179 | Added in version: 0.1.0 180 | """ 181 | 182 | # TODO find better name for split? 183 | def split(constructed, inner_lis): 184 | # constructed is a nested list like [[1,2,3], ['a','b','c']] 185 | return tuple(map(conj, constructed, inner_lis)) 186 | 187 | def create_nested_list(sequence): 188 | # to be passed as an initial value to reduce 189 | # the number of 2nd level lists corresponds 190 | # to the number of elements in the inner list 191 | # of sequence. for e.g 192 | # [ [1,'a'], [2,'b], [3,'c'] ] -> ( (), () ) 193 | 194 | return (() for i in range(len(sequence[0]))) 195 | 196 | return reduce(split, sequence, create_nested_list(sequence)) 197 | 198 | 199 | def interleave(*seqs: Iterable) -> Tuple: 200 | """Similar to clojure's interleave. returns a flat sequence with the contents of iterables interleaved. 201 | 202 | >>> interleave([1,2,3],["a","b","c"]) 203 | (1, 'a', 2, 'b', 3, 'c') 204 | >>> interleave([1,2,3],["int","int","int"], ["a","b","c"],["str","str","str" ]) 205 | (1, 'int', 'a', 'str', 2, 'int', 'b', 'str', 3, 'int', 'c', 'str') 206 | 207 | Added in version: 0.1.0 208 | """ 209 | return flatten(zip(*seqs)) 210 | 211 | 212 | def flatten(sequence: Iterable) -> Tuple: 213 | """Returns the contents of a nested sequence as a flat sequence. 214 | Flatten is recursive. 215 | 216 | >>> flatten([1,2,[3,[4],5],6,7]) 217 | (1, 2, 3, 4, 5, 6, 7) 218 | 219 | Added in version: 0.1.0 220 | """ 221 | 222 | def fn(initial_val, elem): 223 | if is_atom(elem): 224 | return conj(initial_val, elem) 225 | elif is_nested(elem): 226 | # recursively call flatten if elem is a nested sequence 227 | return conj(initial_val, *flatten(elem)) 228 | else: 229 | return conj(initial_val, *elem) 230 | 231 | return reduce(fn, sequence, ()) 232 | 233 | 234 | def insert(element: Any, iterable: Iterable, *, key: Callable = lambda x: x) -> Tuple: 235 | """Inserts ``element`` right before the first element 236 | in the iterable that is greater than ``element`` 237 | 238 | >>> insert(3, [1,2,4,2]) 239 | (1,2,3,4,2) 240 | 241 | >>> insert((2, "b"), {1:"a", 3:"c"}) 242 | ((1, "a"), (2, "b"), (3, "c")) 243 | 244 | Using the key Parameter 245 | 246 | >>> Person = namedtuple("Person", ("name", "age")) 247 | >>> person1 = Person("John", 18) 248 | >>> person2 = Person("Abe", 50) 249 | >>> person3 = Person("Cassy", 25) 250 | >>> insert(person3, (person1, person2), key=lambda p:p.age) 251 | (person1, person3, person2) 252 | >>> insert(person3, (person1, person2), key=lambda p:p.name) 253 | (person3, person1, person2) 254 | 255 | Added in version: 0.1.0 256 | 257 | """ 258 | # TODO refactor. 259 | if not iterable: 260 | return (element,) 261 | 262 | it = iter_(iterable) 263 | accumulator = [] 264 | elem = next(it) 265 | 266 | while key(elem) <= key(element): 267 | accumulator.append(elem) 268 | try: 269 | elem = next(it) 270 | except StopIteration: 271 | return tuple(accumulator) + (element,) 272 | 273 | return tuple(accumulator) + (element, elem) + tuple(it) 274 | 275 | 276 | def remove(predicate: Callable, iterable: Iterable) -> Tuple: 277 | """ 278 | Opposite of filter; Constructs an iterable of elements that falsify the predicate. 279 | 280 | >>> remove(lambda x: x==1, [1,1,9,1,1] 281 | [9] 282 | >>> remove(lambda x: x%2==0, range(10)) 283 | [1,3,5,7,9] # filter would return [2,4,6,8] 284 | 285 | Added in version: 0.1.0 286 | """ 287 | return tuple(filter(complement(predicate), iterable)) 288 | 289 | 290 | def tuplize(iterable: Iterable) -> Tuple: 291 | """ 292 | Recursively converts ``iterable`` to tuple. 293 | 294 | >>> tuplize([1,2,[3,4],5]) 295 | (1, 2, (3, 4), 5) 296 | """ 297 | 298 | def fn(el): 299 | if is_atom(el): 300 | return el 301 | 302 | elif is_nested(el): 303 | return tuplize(el) 304 | 305 | else: # el is not a nested iterable 306 | return tuple(el) 307 | 308 | return tuple([fn(el) for el in iterable]) 309 | -------------------------------------------------------------------------------- /functionali/seq_traverse.py: -------------------------------------------------------------------------------- 1 | """ Functions to traverse the sequence""" 2 | from collections import deque 3 | from typing import ( 4 | Iterable, 5 | Any, 6 | TypeVar, 7 | Iterator, 8 | Tuple, 9 | Mapping, 10 | Union, 11 | Callable, 12 | Iterator, 13 | ) 14 | 15 | import sys 16 | 17 | 18 | def iter_(iterable: Iterable) -> Iterator: 19 | """Returns appropriate iterator for the given iterable. 20 | This is mainly created because python's ``iter`` 21 | returns an iterable of keys instead of keys and values for ``dict``. 22 | 23 | >>> tuple(iter_({1: "a", 2: "b", 3: "c"})) 24 | ((1, "a"),(2, "b"), (3, "c")) 25 | 26 | Added in version: 0.1.0 27 | """ 28 | 29 | if isinstance(iterable, dict): 30 | # since iter(dict) returns a tuple of keys. 31 | # I want a tuple of key-value pairs 32 | return iter(iterable.items()) 33 | 34 | return iter(iterable) 35 | 36 | 37 | def reversed_(iterable: Iterable) -> Iterator: 38 | """Returns appropriate reversed iterator for the given iterable. 39 | This is mainly created because python's ``reversed`` 40 | returns an iterable of keys instead of keys and values for ``dict``. 41 | 42 | >>> tuple(reversed_({1: "a", 2: "b", 3: "c"})) 43 | ((3, 'c'), (2, 'b'), (1, 'a')) 44 | 45 | Added in version: 0.1.0 46 | """ 47 | # TODO drop support for python < 3.8 when support ends 48 | if isinstance(iterable, dict): 49 | # since iter(dict) returns a tuple of keys. 50 | # I want a tuple of key-value pairs 51 | 52 | if ( 53 | sys.version_info[1] < 8 54 | ): # since reversed for dicts was available only in 3.8 55 | return reversed([(k, v) for k, v in iterable.items()]) 56 | else: # pragma:no cover 57 | return reversed(iterable.items()) 58 | 59 | elif not isinstance(iterable, Iterator): 60 | return reversed(iterable) 61 | 62 | else: # if iterable is already a reversed iterator 63 | try: 64 | return reversed(iterable) 65 | except TypeError: 66 | return reversed(tuple(iterable)) 67 | 68 | 69 | def first(iterable: Iterable[Any]) -> Union[Any, None]: 70 | """ 71 | Returns the first item in an iterable or ``None`` if iterable is empty. 72 | If iterable is a dict, returns a tuple of the First key-value pair 73 | 74 | >>> first([1,2,3,4,5]) 75 | 1 76 | >>> first({1:"a", 2:"b"}) 77 | (1, "a") 78 | 79 | Added in version: 0.1.0 80 | """ 81 | try: 82 | return next(iter_(iterable)) 83 | except TypeError: # Nonetype object is not iterable 84 | return None 85 | except StopIteration: 86 | return None 87 | 88 | 89 | def ffirst(iterable: Iterable[Any]) -> Union[Any, None]: 90 | """same as ``first(first(iterable))`` 91 | expects a nested iterable, returns None if iterable is empty 92 | 93 | >>> ffirst([[1,2], [3,4], [5,6]]) 94 | 1 95 | 96 | Added in version: 0.1.0 97 | """ 98 | 99 | return first(first(iterable)) 100 | 101 | 102 | def last(iterable: Iterable[Any]) -> Union[Any, None]: 103 | """ 104 | returns the last element in the iterable. 105 | 106 | >>> last([1,2,3,4]) 107 | 4 108 | >>> last({1: 'a', 2: 'b', 3: 'c'}) 109 | (3, "c") 110 | 111 | Added in version: 0.1.0 112 | """ 113 | if not iterable: 114 | return None 115 | else: 116 | it = iter_(iterable) 117 | return list(it)[-1] 118 | 119 | 120 | def rest(iterable: Iterable) -> Iterator: 121 | """ 122 | Returns an iterator of all but the first element in the iterable. 123 | If iterable is empty it returns an empty iterator. 124 | 125 | >>> list(rest([1,2,3,4,5])) 126 | [2, 3, 4, 5] 127 | 128 | >>> tuple(rest({1:"a", 2:"b", 3:"c"})) 129 | ((2,"b"), (3, "c")) 130 | 131 | >>> tuple(rest([])) 132 | () 133 | 134 | Added in version: 0.1.0 135 | """ 136 | try: 137 | it = iter_(iterable) 138 | next(it) # discard value 139 | return it 140 | 141 | except StopIteration: # If iterable is empty 142 | return iter([]) 143 | 144 | 145 | def second(iterable: Iterable[Any]) -> Union[Any, None]: 146 | """Returns the second item in iterable, or ``None`` if length is less than 2 147 | 148 | >>> second([1,2,3,4,5]) 149 | 2 150 | 151 | Added in version: 0.1.0 152 | """ 153 | result = first(rest(iterable)) 154 | if not result: # if result is an empty iterable 155 | return None 156 | else: 157 | return result 158 | 159 | 160 | def third(iterable: Iterable[Any]) -> Union[Any, None]: 161 | """Returns the third item in iterable, or ``None`` if length is less than 3 162 | 163 | >>> third([1,2,3,4,5]) 164 | 3 165 | 166 | Added in version: 0.1.0 167 | """ 168 | result = first(rest(rest(iterable))) 169 | if not result: # if result is an empty iterable 170 | return None 171 | else: 172 | return result 173 | 174 | 175 | def fourth(iterable: Iterable[Any]) -> Union[Any, None]: 176 | """Returns the fourth item in iterable, or ``None`` if length is less than 4 177 | 178 | >>> fourth([1,2,3,4,5]) 179 | 4 180 | 181 | Added in version: 0.1.0 182 | """ 183 | result = first(rest(rest(rest(iterable)))) 184 | if not result: # if result is an empty iterable 185 | return None 186 | else: 187 | return result 188 | 189 | 190 | def fifth(iterable: Iterable[Any]) -> Union[Any, None]: 191 | """Returns the fifth item in iterable, or ``None`` if length is less than 5 192 | 193 | >>> fifth([1,2,3,4,5]) 194 | 5 195 | 196 | Added in version: 0.1.0 197 | """ 198 | result = first(rest(rest(rest(rest(iterable))))) 199 | if not result: # if result is an empty iterable 200 | return None 201 | else: 202 | return result 203 | 204 | 205 | def butlast(iterable: Iterable[Any]) -> Union[Tuple[Any], None]: 206 | """returns an iterable of all but the last element 207 | in the iterable 208 | 209 | >>> butlast([1, 2, 3]) 210 | (1, 2) 211 | 212 | Added in version: 0.1.0 213 | """ 214 | # TODO Check efficiency of the operation 215 | # since it's iterating through it twice. 216 | t = tuple(iter_(iterable))[:-1] 217 | if t: 218 | return t 219 | else: 220 | return None 221 | 222 | 223 | def take(n: int, iterable: Iterable) -> Tuple: 224 | """Returns the first n number of elements in iterable. 225 | Returns an empty tuple if iterable is empty 226 | 227 | >>> take(3, [1,2,3,4,5]) 228 | (1, 2, 3) 229 | >>> take(2, {1: "a", 2: "b", 3: "c"}) 230 | ((1, "a"), (2, "b")) 231 | 232 | Added in version: 0.1.0 233 | """ 234 | it = iter_(iterable) 235 | 236 | accumulator = [] 237 | i = 1 238 | while i <= n: 239 | try: 240 | accumulator.append(next(it)) 241 | i += 1 242 | except StopIteration: 243 | break 244 | 245 | return tuple(accumulator) 246 | 247 | 248 | def drop(n: int, iterable: Iterable) -> Tuple: 249 | """Returns All the Elements after the first 250 | n number of elements in iterable. 251 | Returns an empty tuple if iterable is empty 252 | 253 | >>> drop(3, [1,2,3,4,5]) 254 | (4,5) 255 | >>> drop(2, {1: "a", 2: "b", 3: "c"}) 256 | ((3, "c"),) 257 | 258 | Added in version: 0.1.0 259 | """ 260 | 261 | it = iter_(iterable) 262 | 263 | i = 1 264 | while i <= n: 265 | try: 266 | next(it) # discard values 267 | i += 1 268 | except StopIteration: 269 | break 270 | 271 | return tuple(it) 272 | 273 | 274 | def take_while(predicate: Callable, iterable: Iterable) -> Tuple: 275 | """ 276 | Constructs a iterable list by taking elements from ``iterable`` while ``predicate`` is true, 277 | Stop taking after the first element falsifies the predicate. 278 | 279 | 280 | >>> take_while(is_even, [2,4,6,7,8,9,10]) 281 | (2,4,6) # Notice that it does not include 8 and 10 282 | 283 | >>> def is_even_dict(d): 284 | #checks if the key of dict d is even 285 | return d[0]%2==0 286 | >>> take_while(is_even_dict, {2:"a", 4:"b",5:"c"}) 287 | ((2, "a"), (4, "b")) 288 | 289 | Added in version: 0.1.0 290 | """ 291 | 292 | it = iter_(iterable) 293 | 294 | accumulator = [] 295 | elem = next(it) 296 | while predicate(elem): 297 | accumulator.append(elem) 298 | elem = next(it) 299 | 300 | return tuple(accumulator) 301 | 302 | 303 | def drop_while(predicate: Callable, iterable: Iterable) -> Tuple: 304 | """ 305 | Drops elements from ``iterable`` while ``predicate`` is true, 306 | And returns a tuple of the remaining elements in ``iterable``. 307 | 308 | >>> drop_while(is_even, [2,4,6,7,8,9,10]) 309 | (7,8,9, 10) 310 | 311 | >>> def is_even_dict(d): 312 | #checks if the key of dict d is even 313 | return d[0]%2==0 314 | >>> drop_while(is_even_dict, {2:"a", 4:"b",5:"c"}) 315 | ((5, "c"),) 316 | 317 | Added in version: 0.1.0 318 | """ 319 | 320 | it = iter_(iterable) 321 | 322 | elem = next(it) 323 | while predicate(elem): 324 | # discard values 325 | elem = next(it) 326 | 327 | # Since elem is the first element 328 | # that fails the predicate. 329 | # and the iterator has already moved ahead. 330 | # we need to include elem 331 | return (elem,) + tuple(it) 332 | 333 | 334 | def split_with(predicate: Callable, iterable: Iterable) -> Tuple[Tuple, Tuple]: 335 | """Equivalent to ``(take_while(predicate, iterable), drop_while(predicate, iterable))`` 336 | 337 | >>> split_with(is_even, [2, 4, 6, 7, 8, 9, 10]) 338 | ((2, 4, 6), (7, 8, 9, 10)) 339 | 340 | Added in version: 0.1.0 341 | """ 342 | # consider implementing with reduce 343 | # since we are iterating through iterable twice. 344 | return (take_while(predicate, iterable), drop_while(predicate, iterable)) 345 | 346 | 347 | def count(iterable: Iterable) -> int: 348 | """ 349 | counts the number of elements in the iterable, works with map objects, filter objets, and iterators. 350 | ``count`` will consume iterators, use ``count_`` if you want access to the iterators. 351 | Added in version: 0.1.2 352 | 353 | >>> count(iter([1,2,3])) 354 | 3 355 | """ 356 | if hasattr(iterable, "__len__"): 357 | return len(iterable) 358 | else: 359 | return len(list(iterable)) 360 | 361 | 362 | def count_(iterable: Iterable) -> Tuple[int, Iterable]: 363 | """ 364 | returns a tuple of the number of elements in the iterable and the iterable itself. 365 | This can be used if you wish to find the length of iterators and want to consume the iterators 366 | later on. 367 | Added in version: 0.1.2 368 | >>> count(iter([1,2,3])) 369 | (3,[1,2,3]) 370 | """ 371 | if hasattr(iterable, "__len__"): 372 | return (len(iterable), iterable) 373 | else: 374 | l = list(iterable) 375 | return (len(l), l) 376 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "alabaster" 3 | version = "0.7.12" 4 | description = "A configurable sidebar-enabled Sphinx theme" 5 | category = "main" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "appdirs" 11 | version = "1.4.4" 12 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 13 | category = "dev" 14 | optional = false 15 | python-versions = "*" 16 | 17 | [[package]] 18 | name = "atomicwrites" 19 | version = "1.4.0" 20 | description = "Atomic file writes." 21 | category = "dev" 22 | optional = false 23 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 24 | 25 | [[package]] 26 | name = "attrs" 27 | version = "21.2.0" 28 | description = "Classes Without Boilerplate" 29 | category = "dev" 30 | optional = false 31 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 32 | 33 | [package.extras] 34 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] 35 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 36 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] 37 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] 38 | 39 | [[package]] 40 | name = "babel" 41 | version = "2.9.1" 42 | description = "Internationalization utilities" 43 | category = "main" 44 | optional = false 45 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 46 | 47 | [package.dependencies] 48 | pytz = ">=2015.7" 49 | 50 | [[package]] 51 | name = "black" 52 | version = "21.5b2" 53 | description = "The uncompromising code formatter." 54 | category = "dev" 55 | optional = false 56 | python-versions = ">=3.6.2" 57 | 58 | [package.dependencies] 59 | appdirs = "*" 60 | click = ">=7.1.2" 61 | mypy-extensions = ">=0.4.3" 62 | pathspec = ">=0.8.1,<1" 63 | regex = ">=2020.1.8" 64 | toml = ">=0.10.1" 65 | 66 | [package.extras] 67 | colorama = ["colorama (>=0.4.3)"] 68 | d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"] 69 | python2 = ["typed-ast (>=1.4.2)"] 70 | uvloop = ["uvloop (>=0.15.2)"] 71 | 72 | [[package]] 73 | name = "blacken-docs" 74 | version = "1.10.0" 75 | description = "Run `black` on python code blocks in documentation files" 76 | category = "dev" 77 | optional = false 78 | python-versions = ">=3.6.1" 79 | 80 | [package.dependencies] 81 | black = ">=19.3b0" 82 | 83 | [[package]] 84 | name = "certifi" 85 | version = "2021.5.30" 86 | description = "Python package for providing Mozilla's CA Bundle." 87 | category = "main" 88 | optional = false 89 | python-versions = "*" 90 | 91 | [[package]] 92 | name = "chardet" 93 | version = "4.0.0" 94 | description = "Universal encoding detector for Python 2 and 3" 95 | category = "main" 96 | optional = false 97 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 98 | 99 | [[package]] 100 | name = "click" 101 | version = "8.0.1" 102 | description = "Composable command line interface toolkit" 103 | category = "dev" 104 | optional = false 105 | python-versions = ">=3.6" 106 | 107 | [package.dependencies] 108 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 109 | 110 | [[package]] 111 | name = "colorama" 112 | version = "0.4.4" 113 | description = "Cross-platform colored terminal text." 114 | category = "main" 115 | optional = false 116 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 117 | 118 | [[package]] 119 | name = "coverage" 120 | version = "5.5" 121 | description = "Code coverage measurement for Python" 122 | category = "dev" 123 | optional = false 124 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 125 | 126 | [package.extras] 127 | toml = ["toml"] 128 | 129 | [[package]] 130 | name = "docutils" 131 | version = "0.16" 132 | description = "Docutils -- Python Documentation Utilities" 133 | category = "main" 134 | optional = false 135 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 136 | 137 | [[package]] 138 | name = "idna" 139 | version = "2.10" 140 | description = "Internationalized Domain Names in Applications (IDNA)" 141 | category = "main" 142 | optional = false 143 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 144 | 145 | [[package]] 146 | name = "imagesize" 147 | version = "1.2.0" 148 | description = "Getting image size from png/jpeg/jpeg2000/gif file" 149 | category = "main" 150 | optional = false 151 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 152 | 153 | [[package]] 154 | name = "iniconfig" 155 | version = "1.1.1" 156 | description = "iniconfig: brain-dead simple config-ini parsing" 157 | category = "dev" 158 | optional = false 159 | python-versions = "*" 160 | 161 | [[package]] 162 | name = "jinja2" 163 | version = "3.0.1" 164 | description = "A very fast and expressive template engine." 165 | category = "main" 166 | optional = false 167 | python-versions = ">=3.6" 168 | 169 | [package.dependencies] 170 | MarkupSafe = ">=2.0" 171 | 172 | [package.extras] 173 | i18n = ["Babel (>=2.7)"] 174 | 175 | [[package]] 176 | name = "m2r" 177 | version = "0.2.1" 178 | description = "Markdown and reStructuredText in a single file." 179 | category = "main" 180 | optional = false 181 | python-versions = "*" 182 | 183 | [package.dependencies] 184 | docutils = "*" 185 | mistune = "*" 186 | 187 | [[package]] 188 | name = "markupsafe" 189 | version = "2.0.1" 190 | description = "Safely add untrusted strings to HTML/XML markup." 191 | category = "main" 192 | optional = false 193 | python-versions = ">=3.6" 194 | 195 | [[package]] 196 | name = "mistune" 197 | version = "0.8.4" 198 | description = "The fastest markdown parser in pure Python" 199 | category = "main" 200 | optional = false 201 | python-versions = "*" 202 | 203 | [[package]] 204 | name = "mypy-extensions" 205 | version = "0.4.3" 206 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 207 | category = "dev" 208 | optional = false 209 | python-versions = "*" 210 | 211 | [[package]] 212 | name = "packaging" 213 | version = "20.9" 214 | description = "Core utilities for Python packages" 215 | category = "main" 216 | optional = false 217 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 218 | 219 | [package.dependencies] 220 | pyparsing = ">=2.0.2" 221 | 222 | [[package]] 223 | name = "pathspec" 224 | version = "0.8.1" 225 | description = "Utility library for gitignore style pattern matching of file paths." 226 | category = "dev" 227 | optional = false 228 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 229 | 230 | [[package]] 231 | name = "pluggy" 232 | version = "0.13.1" 233 | description = "plugin and hook calling mechanisms for python" 234 | category = "dev" 235 | optional = false 236 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 237 | 238 | [package.extras] 239 | dev = ["pre-commit", "tox"] 240 | 241 | [[package]] 242 | name = "pretty-timeit" 243 | version = "0.1.0" 244 | description = "timeit, but with the interface it should have had." 245 | category = "dev" 246 | optional = false 247 | python-versions = ">=3.6.2,<4.0.0" 248 | 249 | [[package]] 250 | name = "py" 251 | version = "1.10.0" 252 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 253 | category = "dev" 254 | optional = false 255 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 256 | 257 | [[package]] 258 | name = "pygments" 259 | version = "2.9.0" 260 | description = "Pygments is a syntax highlighting package written in Python." 261 | category = "main" 262 | optional = false 263 | python-versions = ">=3.5" 264 | 265 | [[package]] 266 | name = "pyparsing" 267 | version = "2.4.7" 268 | description = "Python parsing module" 269 | category = "main" 270 | optional = false 271 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 272 | 273 | [[package]] 274 | name = "pytest" 275 | version = "6.2.4" 276 | description = "pytest: simple powerful testing with Python" 277 | category = "dev" 278 | optional = false 279 | python-versions = ">=3.6" 280 | 281 | [package.dependencies] 282 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 283 | attrs = ">=19.2.0" 284 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 285 | iniconfig = "*" 286 | packaging = "*" 287 | pluggy = ">=0.12,<1.0.0a1" 288 | py = ">=1.8.2" 289 | toml = "*" 290 | 291 | [package.extras] 292 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 293 | 294 | [[package]] 295 | name = "pytest-cov" 296 | version = "2.12.1" 297 | description = "Pytest plugin for measuring coverage." 298 | category = "dev" 299 | optional = false 300 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 301 | 302 | [package.dependencies] 303 | coverage = ">=5.2.1" 304 | pytest = ">=4.6" 305 | toml = "*" 306 | 307 | [package.extras] 308 | testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] 309 | 310 | [[package]] 311 | name = "pytz" 312 | version = "2021.1" 313 | description = "World timezone definitions, modern and historical" 314 | category = "main" 315 | optional = false 316 | python-versions = "*" 317 | 318 | [[package]] 319 | name = "regex" 320 | version = "2021.4.4" 321 | description = "Alternative regular expression module, to replace re." 322 | category = "dev" 323 | optional = false 324 | python-versions = "*" 325 | 326 | [[package]] 327 | name = "requests" 328 | version = "2.25.1" 329 | description = "Python HTTP for Humans." 330 | category = "main" 331 | optional = false 332 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 333 | 334 | [package.dependencies] 335 | certifi = ">=2017.4.17" 336 | chardet = ">=3.0.2,<5" 337 | idna = ">=2.5,<3" 338 | urllib3 = ">=1.21.1,<1.27" 339 | 340 | [package.extras] 341 | security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] 342 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 343 | 344 | [[package]] 345 | name = "snowballstemmer" 346 | version = "2.1.0" 347 | description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." 348 | category = "main" 349 | optional = false 350 | python-versions = "*" 351 | 352 | [[package]] 353 | name = "sphinx" 354 | version = "4.0.2" 355 | description = "Python documentation generator" 356 | category = "main" 357 | optional = false 358 | python-versions = ">=3.6" 359 | 360 | [package.dependencies] 361 | alabaster = ">=0.7,<0.8" 362 | babel = ">=1.3" 363 | colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} 364 | docutils = ">=0.14,<0.18" 365 | imagesize = "*" 366 | Jinja2 = ">=2.3" 367 | packaging = "*" 368 | Pygments = ">=2.0" 369 | requests = ">=2.5.0" 370 | snowballstemmer = ">=1.1" 371 | sphinxcontrib-applehelp = "*" 372 | sphinxcontrib-devhelp = "*" 373 | sphinxcontrib-htmlhelp = "*" 374 | sphinxcontrib-jsmath = "*" 375 | sphinxcontrib-qthelp = "*" 376 | sphinxcontrib-serializinghtml = "*" 377 | 378 | [package.extras] 379 | docs = ["sphinxcontrib-websupport"] 380 | lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.800)", "docutils-stubs"] 381 | test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"] 382 | 383 | [[package]] 384 | name = "sphinx-rtd-theme" 385 | version = "0.5.2" 386 | description = "Read the Docs theme for Sphinx" 387 | category = "main" 388 | optional = false 389 | python-versions = "*" 390 | 391 | [package.dependencies] 392 | docutils = "<0.17" 393 | sphinx = "*" 394 | 395 | [package.extras] 396 | dev = ["transifex-client", "sphinxcontrib-httpdomain", "bump2version"] 397 | 398 | [[package]] 399 | name = "sphinxcontrib-applehelp" 400 | version = "1.0.2" 401 | description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" 402 | category = "main" 403 | optional = false 404 | python-versions = ">=3.5" 405 | 406 | [package.extras] 407 | lint = ["flake8", "mypy", "docutils-stubs"] 408 | test = ["pytest"] 409 | 410 | [[package]] 411 | name = "sphinxcontrib-devhelp" 412 | version = "1.0.2" 413 | description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." 414 | category = "main" 415 | optional = false 416 | python-versions = ">=3.5" 417 | 418 | [package.extras] 419 | lint = ["flake8", "mypy", "docutils-stubs"] 420 | test = ["pytest"] 421 | 422 | [[package]] 423 | name = "sphinxcontrib-htmlhelp" 424 | version = "2.0.0" 425 | description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" 426 | category = "main" 427 | optional = false 428 | python-versions = ">=3.6" 429 | 430 | [package.extras] 431 | lint = ["flake8", "mypy", "docutils-stubs"] 432 | test = ["pytest", "html5lib"] 433 | 434 | [[package]] 435 | name = "sphinxcontrib-jsmath" 436 | version = "1.0.1" 437 | description = "A sphinx extension which renders display math in HTML via JavaScript" 438 | category = "main" 439 | optional = false 440 | python-versions = ">=3.5" 441 | 442 | [package.extras] 443 | test = ["pytest", "flake8", "mypy"] 444 | 445 | [[package]] 446 | name = "sphinxcontrib-qthelp" 447 | version = "1.0.3" 448 | description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." 449 | category = "main" 450 | optional = false 451 | python-versions = ">=3.5" 452 | 453 | [package.extras] 454 | lint = ["flake8", "mypy", "docutils-stubs"] 455 | test = ["pytest"] 456 | 457 | [[package]] 458 | name = "sphinxcontrib-serializinghtml" 459 | version = "1.1.5" 460 | description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." 461 | category = "main" 462 | optional = false 463 | python-versions = ">=3.5" 464 | 465 | [package.extras] 466 | lint = ["flake8", "mypy", "docutils-stubs"] 467 | test = ["pytest"] 468 | 469 | [[package]] 470 | name = "toml" 471 | version = "0.10.2" 472 | description = "Python Library for Tom's Obvious, Minimal Language" 473 | category = "dev" 474 | optional = false 475 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 476 | 477 | [[package]] 478 | name = "urllib3" 479 | version = "1.26.5" 480 | description = "HTTP library with thread-safe connection pooling, file post, and more." 481 | category = "main" 482 | optional = false 483 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 484 | 485 | [package.extras] 486 | brotli = ["brotlipy (>=0.6.0)"] 487 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 488 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 489 | 490 | [metadata] 491 | lock-version = "1.1" 492 | python-versions = "^3.8" 493 | content-hash = "897146666d755a9888ce3d99aa3006697bd818f3e037a5d10843b2aab2280cc2" 494 | 495 | [metadata.files] 496 | alabaster = [ 497 | {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, 498 | {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, 499 | ] 500 | appdirs = [ 501 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 502 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 503 | ] 504 | atomicwrites = [ 505 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 506 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 507 | ] 508 | attrs = [ 509 | {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, 510 | {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, 511 | ] 512 | babel = [ 513 | {file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"}, 514 | {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"}, 515 | ] 516 | black = [ 517 | {file = "black-21.5b2-py3-none-any.whl", hash = "sha256:e5cf21ebdffc7a9b29d73912b6a6a9a4df4ce70220d523c21647da2eae0751ef"}, 518 | {file = "black-21.5b2.tar.gz", hash = "sha256:1fc0e0a2c8ae7d269dfcf0c60a89afa299664f3e811395d40b1922dff8f854b5"}, 519 | ] 520 | blacken-docs = [ 521 | {file = "blacken_docs-1.10.0-py2.py3-none-any.whl", hash = "sha256:149197a0b17e83121fc10aca9eda1417728fdccebde930a6722f97d87ed30f4b"}, 522 | {file = "blacken_docs-1.10.0.tar.gz", hash = "sha256:e2121c95bf2f8a3ebb3110776d276f850f63b8e5753773ba2b4d0f415d862f23"}, 523 | ] 524 | certifi = [ 525 | {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, 526 | {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, 527 | ] 528 | chardet = [ 529 | {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, 530 | {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, 531 | ] 532 | click = [ 533 | {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, 534 | {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, 535 | ] 536 | colorama = [ 537 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 538 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 539 | ] 540 | coverage = [ 541 | {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, 542 | {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, 543 | {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, 544 | {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, 545 | {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, 546 | {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, 547 | {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, 548 | {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, 549 | {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, 550 | {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, 551 | {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, 552 | {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, 553 | {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, 554 | {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, 555 | {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, 556 | {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, 557 | {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, 558 | {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, 559 | {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, 560 | {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, 561 | {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, 562 | {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, 563 | {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, 564 | {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, 565 | {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, 566 | {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, 567 | {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, 568 | {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, 569 | {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, 570 | {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, 571 | {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, 572 | {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, 573 | {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, 574 | {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, 575 | {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, 576 | {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, 577 | {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, 578 | {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, 579 | {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, 580 | {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, 581 | {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, 582 | {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, 583 | {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, 584 | {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, 585 | {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, 586 | {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, 587 | {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, 588 | {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, 589 | {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, 590 | {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, 591 | {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, 592 | {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, 593 | ] 594 | docutils = [ 595 | {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, 596 | {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, 597 | ] 598 | idna = [ 599 | {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, 600 | {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, 601 | ] 602 | imagesize = [ 603 | {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, 604 | {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, 605 | ] 606 | iniconfig = [ 607 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 608 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 609 | ] 610 | jinja2 = [ 611 | {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, 612 | {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"}, 613 | ] 614 | m2r = [ 615 | {file = "m2r-0.2.1.tar.gz", hash = "sha256:bf90bad66cda1164b17e5ba4a037806d2443f2a4d5ddc9f6a5554a0322aaed99"}, 616 | ] 617 | markupsafe = [ 618 | {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, 619 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, 620 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, 621 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, 622 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, 623 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, 624 | {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, 625 | {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, 626 | {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, 627 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, 628 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, 629 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, 630 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, 631 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, 632 | {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, 633 | {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, 634 | {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, 635 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, 636 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, 637 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, 638 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, 639 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, 640 | {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, 641 | {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, 642 | {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, 643 | {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, 644 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, 645 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, 646 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, 647 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, 648 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, 649 | {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, 650 | {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, 651 | {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, 652 | ] 653 | mistune = [ 654 | {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"}, 655 | {file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"}, 656 | ] 657 | mypy-extensions = [ 658 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 659 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 660 | ] 661 | packaging = [ 662 | {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, 663 | {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, 664 | ] 665 | pathspec = [ 666 | {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, 667 | {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, 668 | ] 669 | pluggy = [ 670 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 671 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 672 | ] 673 | pretty-timeit = [ 674 | {file = "pretty-timeit-0.1.0.tar.gz", hash = "sha256:7fb25fa357400b7c759c86925d2b3ed54466289c5703ee089796fa8e23c43621"}, 675 | {file = "pretty_timeit-0.1.0-py3-none-any.whl", hash = "sha256:74e4ffbd0132c2cb44f85799657e3d39fcebbd6d03090880946008ced18c5c32"}, 676 | ] 677 | py = [ 678 | {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, 679 | {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, 680 | ] 681 | pygments = [ 682 | {file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"}, 683 | {file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"}, 684 | ] 685 | pyparsing = [ 686 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 687 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 688 | ] 689 | pytest = [ 690 | {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, 691 | {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, 692 | ] 693 | pytest-cov = [ 694 | {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, 695 | {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, 696 | ] 697 | pytz = [ 698 | {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, 699 | {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, 700 | ] 701 | regex = [ 702 | {file = "regex-2021.4.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000"}, 703 | {file = "regex-2021.4.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711"}, 704 | {file = "regex-2021.4.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:281d2fd05555079448537fe108d79eb031b403dac622621c78944c235f3fcf11"}, 705 | {file = "regex-2021.4.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bd28bc2e3a772acbb07787c6308e00d9626ff89e3bfcdebe87fa5afbfdedf968"}, 706 | {file = "regex-2021.4.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7c2a1af393fcc09e898beba5dd59196edaa3116191cc7257f9224beaed3e1aa0"}, 707 | {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c38c71df845e2aabb7fb0b920d11a1b5ac8526005e533a8920aea97efb8ec6a4"}, 708 | {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:96fcd1888ab4d03adfc9303a7b3c0bd78c5412b2bfbe76db5b56d9eae004907a"}, 709 | {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:ade17eb5d643b7fead300a1641e9f45401c98eee23763e9ed66a43f92f20b4a7"}, 710 | {file = "regex-2021.4.4-cp36-cp36m-win32.whl", hash = "sha256:e8e5b509d5c2ff12f8418006d5a90e9436766133b564db0abaec92fd27fcee29"}, 711 | {file = "regex-2021.4.4-cp36-cp36m-win_amd64.whl", hash = "sha256:11d773d75fa650cd36f68d7ca936e3c7afaae41b863b8c387a22aaa78d3c5c79"}, 712 | {file = "regex-2021.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d3029c340cfbb3ac0a71798100ccc13b97dddf373a4ae56b6a72cf70dfd53bc8"}, 713 | {file = "regex-2021.4.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:18c071c3eb09c30a264879f0d310d37fe5d3a3111662438889ae2eb6fc570c31"}, 714 | {file = "regex-2021.4.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:4c557a7b470908b1712fe27fb1ef20772b78079808c87d20a90d051660b1d69a"}, 715 | {file = "regex-2021.4.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:01afaf2ec48e196ba91b37451aa353cb7eda77efe518e481707e0515025f0cd5"}, 716 | {file = "regex-2021.4.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3a9cd17e6e5c7eb328517969e0cb0c3d31fd329298dd0c04af99ebf42e904f82"}, 717 | {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:90f11ff637fe8798933fb29f5ae1148c978cccb0452005bf4c69e13db951e765"}, 718 | {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:919859aa909429fb5aa9cf8807f6045592c85ef56fdd30a9a3747e513db2536e"}, 719 | {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:339456e7d8c06dd36a22e451d58ef72cef293112b559010db3d054d5560ef439"}, 720 | {file = "regex-2021.4.4-cp37-cp37m-win32.whl", hash = "sha256:67bdb9702427ceddc6ef3dc382455e90f785af4c13d495f9626861763ee13f9d"}, 721 | {file = "regex-2021.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:32e65442138b7b76dd8173ffa2cf67356b7bc1768851dded39a7a13bf9223da3"}, 722 | {file = "regex-2021.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1e1c20e29358165242928c2de1482fb2cf4ea54a6a6dea2bd7a0e0d8ee321500"}, 723 | {file = "regex-2021.4.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:314d66636c494ed9c148a42731b3834496cc9a2c4251b1661e40936814542b14"}, 724 | {file = "regex-2021.4.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6d1b01031dedf2503631d0903cb563743f397ccaf6607a5e3b19a3d76fc10480"}, 725 | {file = "regex-2021.4.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:741a9647fcf2e45f3a1cf0e24f5e17febf3efe8d4ba1281dcc3aa0459ef424dc"}, 726 | {file = "regex-2021.4.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4c46e22a0933dd783467cf32b3516299fb98cfebd895817d685130cc50cd1093"}, 727 | {file = "regex-2021.4.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:e512d8ef5ad7b898cdb2d8ee1cb09a8339e4f8be706d27eaa180c2f177248a10"}, 728 | {file = "regex-2021.4.4-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:980d7be47c84979d9136328d882f67ec5e50008681d94ecc8afa8a65ed1f4a6f"}, 729 | {file = "regex-2021.4.4-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:ce15b6d103daff8e9fee13cf7f0add05245a05d866e73926c358e871221eae87"}, 730 | {file = "regex-2021.4.4-cp38-cp38-win32.whl", hash = "sha256:a91aa8619b23b79bcbeb37abe286f2f408d2f2d6f29a17237afda55bb54e7aac"}, 731 | {file = "regex-2021.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:c0502c0fadef0d23b128605d69b58edb2c681c25d44574fc673b0e52dce71ee2"}, 732 | {file = "regex-2021.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:598585c9f0af8374c28edd609eb291b5726d7cbce16be6a8b95aa074d252ee17"}, 733 | {file = "regex-2021.4.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ee54ff27bf0afaf4c3b3a62bcd016c12c3fdb4ec4f413391a90bd38bc3624605"}, 734 | {file = "regex-2021.4.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7d9884d86dd4dd489e981d94a65cd30d6f07203d90e98f6f657f05170f6324c9"}, 735 | {file = "regex-2021.4.4-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:bf5824bfac591ddb2c1f0a5f4ab72da28994548c708d2191e3b87dd207eb3ad7"}, 736 | {file = "regex-2021.4.4-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:563085e55b0d4fb8f746f6a335893bda5c2cef43b2f0258fe1020ab1dd874df8"}, 737 | {file = "regex-2021.4.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9c3db21af35e3b3c05764461b262d6f05bbca08a71a7849fd79d47ba7bc33ed"}, 738 | {file = "regex-2021.4.4-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3916d08be28a1149fb97f7728fca1f7c15d309a9f9682d89d79db75d5e52091c"}, 739 | {file = "regex-2021.4.4-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:fd45ff9293d9274c5008a2054ecef86a9bfe819a67c7be1afb65e69b405b3042"}, 740 | {file = "regex-2021.4.4-cp39-cp39-win32.whl", hash = "sha256:fa4537fb4a98fe8fde99626e4681cc644bdcf2a795038533f9f711513a862ae6"}, 741 | {file = "regex-2021.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07"}, 742 | {file = "regex-2021.4.4.tar.gz", hash = "sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb"}, 743 | ] 744 | requests = [ 745 | {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, 746 | {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, 747 | ] 748 | snowballstemmer = [ 749 | {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"}, 750 | {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, 751 | ] 752 | sphinx = [ 753 | {file = "Sphinx-4.0.2-py3-none-any.whl", hash = "sha256:d1cb10bee9c4231f1700ec2e24a91be3f3a3aba066ea4ca9f3bbe47e59d5a1d4"}, 754 | {file = "Sphinx-4.0.2.tar.gz", hash = "sha256:b5c2ae4120bf00c799ba9b3699bc895816d272d120080fbc967292f29b52b48c"}, 755 | ] 756 | sphinx-rtd-theme = [ 757 | {file = "sphinx_rtd_theme-0.5.2-py2.py3-none-any.whl", hash = "sha256:4a05bdbe8b1446d77a01e20a23ebc6777c74f43237035e76be89699308987d6f"}, 758 | {file = "sphinx_rtd_theme-0.5.2.tar.gz", hash = "sha256:32bd3b5d13dc8186d7a42fc816a23d32e83a4827d7d9882948e7b837c232da5a"}, 759 | ] 760 | sphinxcontrib-applehelp = [ 761 | {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, 762 | {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, 763 | ] 764 | sphinxcontrib-devhelp = [ 765 | {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, 766 | {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, 767 | ] 768 | sphinxcontrib-htmlhelp = [ 769 | {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, 770 | {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, 771 | ] 772 | sphinxcontrib-jsmath = [ 773 | {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, 774 | {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, 775 | ] 776 | sphinxcontrib-qthelp = [ 777 | {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, 778 | {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, 779 | ] 780 | sphinxcontrib-serializinghtml = [ 781 | {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, 782 | {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, 783 | ] 784 | toml = [ 785 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 786 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 787 | ] 788 | urllib3 = [ 789 | {file = "urllib3-1.26.5-py2.py3-none-any.whl", hash = "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c"}, 790 | {file = "urllib3-1.26.5.tar.gz", hash = "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"}, 791 | ] 792 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "functionali" 3 | version = "0.2.0" 4 | description = "Functional programming tools for python. Putting the fun in functional programming 😉" 5 | authors = ["Abhinav Omprakash"] 6 | license="BSD-3-Clause" 7 | readme="README.md" 8 | homepage="https://functionali.readthedocs.io/en/latest/" 9 | repository="https://github.com/AbhinavOmprakash/functionali" 10 | keywords=["functional programming", "fp", "functools"] 11 | include=[ 12 | "LICENSE", 13 | ] 14 | 15 | [tool.poetry.dependencies] 16 | python = "^3.6" 17 | 18 | 19 | [tool.poetry.dev-dependencies] 20 | pytest = "^6.2.4" 21 | pytest-cov = "^2.12.1" 22 | Sphinx = "^4.0.2" 23 | black = "*" 24 | blacken-docs = "^*" 25 | pretty-timeit = "^0.1.0" 26 | sphinx-rtd-theme = "^0.5.2" 27 | 28 | [build-system] 29 | requires = ["poetry-core>=1.0.0"] 30 | build-backend = "poetry.core.masonry.api" 31 | -------------------------------------------------------------------------------- /tests/test_higher_order_functions.py: -------------------------------------------------------------------------------- 1 | from functionali import ( 2 | lazymap, 3 | lazyfilter, 4 | map, 5 | filter, 6 | curry, 7 | reduce, 8 | reduced, 9 | Reduced, 10 | foldr, 11 | comp, 12 | flip, 13 | trampoline, 14 | count, 15 | is_even, 16 | threadf, 17 | threadl, 18 | ) 19 | from operator import mul 20 | import pytest 21 | 22 | 23 | def inc(x): 24 | return x + 1 25 | 26 | 27 | def test_lazymap(): 28 | res = lazymap(inc, range(10)) 29 | assert list(res) == [i for i in range(1, 11)] 30 | # assert that res returns a new generator everytime it is called 31 | assert list(res) == [i for i in range(1, 11)] 32 | 33 | 34 | def test_map(): 35 | assert map(inc, [1, 2, 3]) == (2, 3, 4) 36 | assert map(inc, []) == () 37 | 38 | def inc_two(x, y): 39 | return [x + 1, y + 1] 40 | 41 | assert list(map(inc_two, [0, 1, 2], [10, 11, 12])) == [[1, 11], [2, 12], [3, 13]] 42 | 43 | 44 | def is_even(x): 45 | return x % 2 == 0 46 | 47 | 48 | def test_lazyfilter(): 49 | res = lazyfilter(is_even, [1, 2, 3]) 50 | 51 | assert list(res) == [2] 52 | # assert that res returns a new generator everytime it is called 53 | assert list(res) == [2] 54 | 55 | 56 | def test_filter(): 57 | assert list(filter(is_even, [1, 2, 3])) == [2] 58 | assert list(filter(is_even, [])) == [] 59 | 60 | 61 | def test_reduce(): 62 | assert 6 == reduce(mul, [1, 2, 3]) 63 | 64 | # test dicts 65 | assert 6 == reduce(lambda a, kv: mul(a, kv[1]), {"_": 1, "__": 2, "___": 3}, 1) 66 | 67 | # test premature termination 68 | def inc_while_odd(result, element): 69 | if element % 2 == 0: 70 | return reduced(result) 71 | else: 72 | result.append(element + 1) 73 | return result 74 | 75 | assert [2, 4, 6] == reduce(inc_while_odd, [1, 3, 5, 6, 7, 8], []) 76 | 77 | 78 | def test_reduced_returns_a_reduced_object(): 79 | assert isinstance(reduced(1), Reduced) 80 | 81 | 82 | def test_calling_Reduced_returns_a_value(): 83 | red_obj = reduced(1) 84 | assert 1 == red_obj() 85 | 86 | 87 | def test_foldr(): 88 | sub = lambda e, acc: acc - e # foldr function 89 | assert foldr(sub, [1, 2, 3, 10]) == 4 # (((10-3)-2)-1) = 4 90 | assert foldr(sub, [1, 2], 10) == 7 # ((10-2)-1) == 7 91 | assert foldr(sub, iter([1, 2, 3, 10])) == 4 # (((10-3)-2)-1) = 4 92 | 93 | 94 | def test_compose(): 95 | inc = lambda x: x + 1 96 | double_it = lambda x: x * 2 97 | 98 | assert comp(inc, double_it)(5) == inc( 99 | double_it(5) 100 | ) # inc(double_it(5)) => inc(10) => 11 101 | assert comp(double_it, inc)(5) == 12 # double_it(inc(5)) => double_it(6) => 12 102 | 103 | assert comp(count, filter)(is_even, [1, 2, 3, 4]) == 2 104 | 105 | 106 | def test_curry(): 107 | def fn(arg1, arg2, arg3): # test function 108 | return [arg1, arg2, arg3] 109 | 110 | curried_fn = curry(fn) 111 | assert [1, 2, 3] == curried_fn(1)(2)(3) 112 | 113 | def fn_with_no_args(): 114 | return 1 115 | 116 | curried_fn_with_no_args = curry(fn_with_no_args) 117 | assert 1 == curried_fn_with_no_args() 118 | 119 | 120 | def test_flip(): 121 | def fn(arg1, arg2, arg3): # test function 122 | return [arg1, arg2, arg3] 123 | 124 | flipped_fn = flip(fn) 125 | 126 | assert [1, 2, 3] == fn(1, 2, 3) 127 | assert [1, 2, 3] != flipped_fn(1, 2, 3) 128 | assert [1, 2, 3] == flipped_fn(3, 2, 1) # flipping args 129 | 130 | 131 | def test_trampoline(): 132 | def fact(x, curr=1, acc=1): 133 | if curr == x: 134 | return curr * acc 135 | else: 136 | return lambda: fact(x, curr + 1, acc * curr) 137 | 138 | assert trampoline(fact, 3) == 6 139 | 140 | 141 | @pytest.mark.parametrize( 142 | "input, expected", 143 | [ 144 | ([1, [lambda x: x]], 1), 145 | ([1, [[lambda a, b: a - b, 3]]], -2), 146 | ([1, [[lambda a, b: a + b, 2], [lambda a, b: a - b, 2]]], 1), 147 | ], 148 | ) 149 | def test_threadf(input, expected): 150 | assert threadf(*input) == expected 151 | 152 | 153 | @pytest.mark.parametrize( 154 | "input, expected", 155 | [ 156 | ([1, [lambda x: x]], 1), 157 | ([1, [[lambda a, b: a - b, 3]]], 2), 158 | ([1, [[lambda a, b: a + b, 2], [lambda a, b: a - b, 2]]], -1), 159 | ], 160 | ) 161 | def test_threadl(input, expected): 162 | assert threadl(*input) == expected 163 | -------------------------------------------------------------------------------- /tests/test_predicates.py: -------------------------------------------------------------------------------- 1 | from functionali import ( 2 | identity, 3 | equals, 4 | is_, 5 | less_than, 6 | less_than_eq, 7 | greater_than, 8 | greater_than_eq, 9 | complement, 10 | is_even, 11 | is_odd, 12 | is_prime, 13 | is_numeric, 14 | is_divisible, 15 | is_divisible_by, 16 | is_numeric, 17 | is_atom, 18 | contains, 19 | is_empty, 20 | is_nested, 21 | all_predicates, 22 | some_predicates, 23 | ) 24 | 25 | import pytest 26 | 27 | 28 | def test_identity(): 29 | assert identity(1) == 1 30 | 31 | 32 | def test_equals(): 33 | assert equals(1)(1) == True 34 | assert equals(1, 1) == True 35 | assert equals(1, 1, 1, 1, 1, 1, 1) == True 36 | 37 | assert equals(1)(2) == False 38 | assert equals(1, 2) == False 39 | assert equals(1, 1, 1, 1, 1, 1, 2) == False 40 | assert equals(1, 2, 1, 1, 1) == False 41 | 42 | 43 | def test_is_(): 44 | assert is_(1)(1) == True 45 | assert is_(1, 1) == True 46 | assert is_(1, 1, 1, 1, 1, 1, 1) == True 47 | 48 | assert is_(1, 1, 2) == False 49 | assert is_({1: "a"})({1: "a"}) == False 50 | assert is_({1}, {1}, {1}) == False 51 | 52 | 53 | def test_less_than(): 54 | assert less_than(2)(1) == True 55 | assert less_than(1)(1) == False 56 | assert less_than(1, 2) == True 57 | assert less_than(1, 2, 3, 4, 5, 6) == True 58 | 59 | assert less_than(1)(1) == False 60 | assert less_than(1, 3, 2) == False 61 | assert less_than(1, 2, 3, 0) == False 62 | assert less_than(1, 2, 3, 4, 1) == False 63 | assert less_than(1, 1, 2, 3) == False 64 | 65 | 66 | def test_less_than_eq(): 67 | assert less_than_eq(2)(1) == True 68 | assert less_than_eq(1)(1) == True 69 | assert less_than_eq(1, 2) == True 70 | assert less_than_eq(1, 2, 3, 4, 5, 6) == True 71 | assert less_than_eq(1, 1, 2, 3) == True 72 | 73 | assert less_than_eq(1, 1, 2, 3, 1) == False 74 | assert less_than_eq(1)(2) == False 75 | assert less_than_eq(1, 3, 2) == False 76 | assert less_than_eq(1, 2, 3, 0) == False 77 | assert less_than_eq(4, 1, 2) == False 78 | 79 | 80 | def test_greater_than(): 81 | assert greater_than(1)(2) == True 82 | assert greater_than(2, 1) == True 83 | assert greater_than(4, 3, 2, 1) == True 84 | 85 | assert greater_than(4, 2, 3, 1) == False 86 | assert greater_than(1)(1) == False 87 | assert greater_than(4, 3, 2, 4) == False 88 | assert greater_than(4, 4, 3, 2) == False 89 | 90 | 91 | def test_greater_than_eq(): 92 | assert greater_than_eq(1)(2) == True 93 | assert greater_than_eq(1)(1) == True 94 | assert greater_than_eq(2, 1) == True 95 | assert greater_than_eq(4, 3, 2, 1) == True 96 | assert greater_than_eq(1)(1) == True 97 | assert greater_than_eq(4, 4, 3, 2) == True 98 | 99 | assert greater_than_eq(4, 3, 2, 4) == False 100 | assert greater_than(4, 2, 3, 1) == False 101 | assert greater_than_eq(4, 6) == False 102 | assert greater_than_eq(4, 6, 3, 4) == False 103 | assert greater_than_eq(4, 4, 3, 5) == False 104 | 105 | 106 | def test_complement_(): 107 | assert complement(False) == True 108 | assert complement(True) == False 109 | 110 | assert complement(0) == True 111 | assert complement(1) == False 112 | 113 | assert complement([]) == True 114 | assert complement([1, 2, 3]) == False 115 | 116 | # test negated functions. 117 | def fn(el): 118 | return bool(el) 119 | 120 | negated_fn = complement(fn) 121 | assert negated_fn(True) != fn(True) 122 | assert negated_fn(False) == fn(True) 123 | 124 | 125 | def test_is_even(): 126 | assert is_even(2) == True 127 | assert is_even(3) == False 128 | 129 | 130 | def test_is_odd(): 131 | assert is_odd(3) == True 132 | assert is_odd(2) == False 133 | 134 | 135 | def test_is_prime(): 136 | assert is_prime(0) == False 137 | assert is_prime(1) == False 138 | assert is_prime(2) == True 139 | assert is_prime(3) == True 140 | assert is_prime(4) == False 141 | assert is_prime(101) == True 142 | assert is_prime(150) == False 143 | 144 | 145 | def test_is_divisible(): 146 | assert is_divisible(4, 2) == True 147 | assert is_divisible(4, 3) == False 148 | 149 | with pytest.raises(ZeroDivisionError): 150 | is_divisible(4, 0) 151 | 152 | 153 | def test_is_divisible_by(): 154 | is_divisible_by_five = is_divisible_by(5) 155 | assert is_divisible_by_five(10) == True 156 | assert is_divisible_by_five(7) == False 157 | 158 | is_divisible_by_zero = is_divisible_by(0) 159 | with pytest.raises(ZeroDivisionError): 160 | is_divisible_by_zero(10) 161 | 162 | 163 | def test_is_numeric(): 164 | assert is_numeric(1) == True 165 | assert is_numeric(1.253) == True 166 | assert is_numeric(complex("1+2j")) == True 167 | assert is_numeric("String") == False 168 | 169 | 170 | def test_is_atom(): 171 | assert is_atom("plain string") == True 172 | assert is_atom(1) == True 173 | assert is_atom(1.2) == True 174 | 175 | assert is_atom([1, 2]) == False 176 | assert is_atom({1: "a"}) == False 177 | 178 | 179 | def test_contains(): 180 | assert contains(1, [1, 2]) == True 181 | assert contains(100, [1, 2]) == False 182 | assert contains(1, {1: "a"}) == True 183 | assert contains(2, {1: "a"}) == False 184 | 185 | 186 | def test_is_empty(): 187 | assert is_empty([]) == True 188 | assert is_empty([1]) == False 189 | 190 | 191 | def test_is_nested(): 192 | assert is_nested([]) == False 193 | assert is_nested([[]]) == True 194 | assert is_nested([(), ()]) == True 195 | assert is_nested([(), {}]) == True 196 | 197 | assert is_nested({}) == False 198 | assert is_nested({1: {}}) == True 199 | assert is_nested({1: "a", 2: "b"}) == False 200 | assert is_nested({1: "a", 2: {1: "a", 2: "b"}}) == True 201 | 202 | 203 | def test_all_predicates(): 204 | is_even_and_prime = all_predicates(is_even, is_prime) 205 | 206 | assert is_even_and_prime(1) == False 207 | assert is_even_and_prime(2) == True 208 | assert is_even_and_prime(4) == False 209 | 210 | 211 | def test_some_predicates(): 212 | is_even_or_prime = some_predicates(is_even, is_prime) 213 | 214 | assert is_even_or_prime(1) == False 215 | assert is_even_or_prime(2) == True 216 | assert is_even_or_prime(3) == True 217 | assert is_even_or_prime(4) == True 218 | -------------------------------------------------------------------------------- /tests/test_seq_transform.py: -------------------------------------------------------------------------------- 1 | from functionali import ( 2 | cons, 3 | conj, 4 | concat, 5 | argzip, 6 | argmap, 7 | unzip, 8 | interleave, 9 | flatten, 10 | insert, 11 | remove, 12 | tuplize, 13 | ) 14 | 15 | from collections import deque, namedtuple 16 | 17 | 18 | def test_cons(): 19 | assert cons(3, []) == deque([3]) 20 | assert deque([3, 1, 2]) == cons(3, [1, 2]) 21 | assert deque([(3, "c"), (1, "a"), (2, "b")]) == cons((3, "c"), {1: "a", 2: "b"}) 22 | assert deque([3, 1, 2]) == cons(3, {1, 2}) 23 | assert deque([3, 1, 2]) == cons(3, deque([1, 2])) 24 | 25 | 26 | def test_conj(): 27 | assert conj([], 1) == [1] 28 | assert [1, 2, 3, 4, 5] == conj([1, 2, 3, 4], 5) 29 | assert [1, 2, 3, 4, [5]] == conj([1, 2, 3, 4], [5]) 30 | assert [1, 2, 3, 4, 5, 6, 7, 8] == conj([1, 2, 3, 4], 5, 6, 7, 8) 31 | assert [1, 2, 3, 4, [5, 6, 7]] == conj([1, 2, 3, 4], [5, 6, 7]) 32 | assert (1, 2, 3, 4, 5, 6, 7) == conj((1, 2, 3, 4), 5, 6, 7) 33 | assert (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11) == conj(range(10), 11) 34 | assert {1: "a", 2: "b", 3: "c"} == conj({1: "a", 2: "b"}, {3: "c"}) 35 | assert {1, 2, 3} == conj({1, 2}, 3) 36 | assert deque([3, 1, 2]) == conj(deque([1, 2]), 3) 37 | assert deque([4, 3, 1, 2]) == conj(deque([1, 2]), 3, 4) 38 | 39 | 40 | def test_concat(): 41 | assert concat((), 1) == (1,) 42 | assert [1, 2, 3, 4, 5] == concat([1, 2, 3, 4], 5) 43 | 44 | # The difference between concat And conj is seen 45 | # only with a deque, it makes sense only test a deque. 46 | assert deque([1, 2, 3]) == concat(deque([1, 2]), 3) 47 | assert deque([1, 2, 3, 4]) == concat(deque([1, 2]), 3, 4) 48 | assert deque([4, 3, 1, 2]) != concat(deque([1, 2]), 3, 4) 49 | 50 | 51 | def test_argmap(): 52 | inc = lambda x: x + 1 53 | dec = lambda x: x - 1 54 | 55 | assert list(argmap([inc, dec], [1])) == [2, 0] 56 | 57 | add = lambda a, b: a + b 58 | sub = lambda a, b: a - b 59 | 60 | assert list(argmap([add, sub], [2, 1])) == [3, 1] 61 | 62 | 63 | def test_argzip(): 64 | assert [(1, "number"), (2, "number"), (3, "number"), (4, "number")] == list( 65 | argzip([1, 2, 3, 4], "number") 66 | ) 67 | assert [ 68 | (1, "number", "int"), 69 | (2, "number", "int"), 70 | (3, "number", "int"), 71 | (4, "number", "int"), 72 | ] == list(argzip([1, 2, 3, 4], "number", "int")) 73 | 74 | 75 | def test_unzip(): 76 | assert ((1, 2, 3), ("a", "b", "c")) == unzip([[1, "a"], [2, "b"], [3, "c"]]) 77 | assert ((1, 2, 3), ("a", "b", "c"), ("A", "B", "C")) == unzip( 78 | [[1, "a", "A"], [2, "b", "B"], [3, "c", "C"]] 79 | ) 80 | assert (([1, "num"], [2, "num"]), (["a", "str"], ["b", "str"])) == unzip( 81 | [[[1, "num"], ["a", "str"]], [[2, "num"], ["b", "str"]]] 82 | ) 83 | 84 | 85 | def test_interleave(): 86 | assert (1, "a", 2, "b", 3, "c") == interleave([1, 2, 3], ["a", "b", "c"]) 87 | assert ( 88 | 1, 89 | "int", 90 | "a", 91 | "str", 92 | 2, 93 | "int", 94 | "b", 95 | "str", 96 | 3, 97 | "int", 98 | "c", 99 | "str", 100 | ) == interleave( 101 | [1, 2, 3], ["int", "int", "int"], ["a", "b", "c"], ["str", "str", "str"] 102 | ) 103 | 104 | 105 | def test_flatten(): 106 | assert flatten([1, 2, [3, [4], 5], 6, 7]) == (1, 2, 3, 4, 5, 6, 7) 107 | # with iterator 108 | assert flatten(iter([1, 2, [3, [4], 5], 6, 7])) == (1, 2, 3, 4, 5, 6, 7) 109 | # with generator 110 | assert flatten((i for i in [1, 2, [3, [4], 5], 6, 7])) == (1, 2, 3, 4, 5, 6, 7) 111 | 112 | 113 | def test_insert(): 114 | assert insert(2, []) == (2,) 115 | assert (1, 2, 3, 4) == insert(3, [1, 2, 4]) 116 | assert (1, 2, 3, 4, 2) == insert(3, [1, 2, 4, 2]) 117 | assert (1, 2, 3, 4) == insert(4, [1, 2, 3]) 118 | assert ((1, "a"), (2, "b"), (3, "c")) == insert((2, "b"), {1: "a", 3: "c"}) 119 | 120 | # for the key parameter 121 | Person = namedtuple("Person", ("name", "age")) 122 | person1 = Person("John", 18) 123 | person2 = Person("Abe", 50) 124 | person3 = Person("Cassy", 25) 125 | assert (person1, person3, person2) == insert( 126 | person3, (person1, person2), key=lambda p: p.age 127 | ) 128 | assert (person3, person1, person2) == insert( 129 | person3, (person1, person2), key=lambda p: p.name 130 | ) 131 | 132 | 133 | def test_remove(): 134 | is_pos = lambda x: x >= 0 135 | assert remove(is_pos, range(-5, 5)) == (-5, -4, -3, -2, -1) 136 | 137 | def test_tuplize(): 138 | assert tuplize([1,2,3]) == (1,2,3) 139 | assert tuplize([1,2,[3,4],5]) == (1, 2, (3, 4), 5) 140 | assert tuplize([]) == () 141 | assert tuplize([1,2,3,[4,[5]]]) == (1,2,3,(4,(5,))) -------------------------------------------------------------------------------- /tests/test_seq_traverse.py: -------------------------------------------------------------------------------- 1 | from functionali import ( 2 | iter_, 3 | reversed_, 4 | first, 5 | ffirst, 6 | second, 7 | third, 8 | fourth, 9 | fifth, 10 | last, 11 | butlast, 12 | rest, 13 | take, 14 | drop, 15 | take_while, 16 | drop_while, 17 | split_with, 18 | count, 19 | count_, 20 | ) 21 | 22 | from typing import Iterator 23 | 24 | 25 | def test_iter_(): 26 | # tests that the behavior of iter_ differs from iter 27 | # When it comes to dictionaries 28 | d = {1: "a", 2: "b"} 29 | assert isinstance(iter_(d), Iterator) == True 30 | assert tuple(iter_({1: "a", 2: "b", 3: "c"})) == ((1, "a"), (2, "b"), (3, "c")) 31 | 32 | 33 | def test_reversed_(): 34 | d = {1: "a", 2: "b"} 35 | assert isinstance(reversed_(d), Iterator) == True 36 | assert tuple(reversed_({1: "a", 2: "b", 3: "c"})) == ((3, "c"), (2, "b"), (1, "a")) 37 | 38 | # test that iterators return a reversed iterators 39 | assert tuple(reversed_(iter([1, 2, 3]))) == (3, 2, 1) 40 | 41 | 42 | def test_first(): 43 | assert 1 == first([1, 2, 3]) 44 | assert 1 == first((1, 2, 3)) 45 | assert 1 == first({1, 2, 3}) 46 | assert (1, "a") == first({1: "a", 2: "b"}) 47 | assert None == first([]) 48 | 49 | 50 | def test_ffirst(): 51 | assert 1 == ffirst([[1], [2], [3]]) 52 | assert 1 == ffirst(((1,), (2,), (3,))) 53 | assert 1 == ffirst({(1, 2), (3, 4), (5, 6)}) 54 | assert None == ffirst([]) 55 | 56 | 57 | def test_last(): 58 | assert 1 == last([1]) 59 | assert 3 == last([1, 2, 3]) 60 | assert 3 == last(iter({1, 2, 3})) 61 | assert (3, "c") == last({1: "a", 2: "b", 3: "c"}) 62 | assert None == last([]) 63 | 64 | 65 | def test_rest(): 66 | # convert to tuple since rest returns iterator 67 | assert (2, 3) == tuple(rest([1, 2, 3])) 68 | assert (2, 3) == tuple(rest((1, 2, 3))) 69 | assert (2, 3) == tuple(rest({1, 2, 3})) 70 | assert ((2, "b"), (3, "c")) == tuple(rest({1: "a", 2: "b", 3: "c"})) 71 | assert () == tuple(rest([])) 72 | 73 | 74 | def test_second(): 75 | assert 2 == second([1, 2, 3]) 76 | assert 2 == second(iter((1, 2, 3))) 77 | assert (2, "b") == second({1: "a", 2: "b"}) 78 | assert None == second([]) 79 | assert None == second([1]) 80 | 81 | 82 | def test_third(): 83 | assert 3 == third([1, 2, 3]) 84 | assert 3 == third(iter((1, 2, 3))) 85 | assert (3, "c") == third({1: "a", 2: "b", 3: "c"}) 86 | assert None == third([]) 87 | 88 | assert None == third([1, 2]) 89 | 90 | 91 | def test_fourth(): 92 | assert 4 == fourth([1, 2, 3, 4]) 93 | assert 4 == fourth(iter((1, 2, 3, 4))) 94 | assert 4 == fourth({1, 2, 3, 4}) 95 | assert (4, "d") == fourth({1: "a", 2: "b", 3: "c", 4: "d"}) 96 | assert None == fourth([]) 97 | 98 | assert None == fourth([1, 2]) 99 | 100 | 101 | def test_fifth(): 102 | assert 5 == fifth([1, 2, 3, 4, 5]) 103 | assert 5 == fifth(iter((1, 2, 3, 4, 5))) 104 | assert 5 == fifth({1, 2, 3, 4, 5}) 105 | assert (5, "e") == fifth({1: "a", 2: "b", 3: "c", 4: "d", 5: "e"}) 106 | assert None == fifth([]) 107 | 108 | assert None == fifth([1, 2]) 109 | 110 | 111 | def test_butlast(): 112 | assert (1, 2) == butlast([1, 2, 3]) 113 | assert (1, 2) == butlast(iter((1, 2, 3))) 114 | assert (1, 2) == butlast({1, 2, 3}) 115 | assert ((1, "a"), (2, "b")) == butlast({1: "a", 2: "b", 3: "c"}) 116 | assert None == butlast([1]) 117 | assert None == butlast([]) 118 | 119 | 120 | def test_take(): 121 | assert (1, 2, 3) == take(3, [1, 2, 3, 4, 5]) 122 | assert () == take(3, []) 123 | assert ((1, "a"), (2, "b")) == take(2, {1: "a", 2: "b", 3: "c"}) 124 | 125 | 126 | def test_drop(): 127 | assert (4, 5) == drop(3, [1, 2, 3, 4, 5]) 128 | assert () == drop(3, []) 129 | assert ((3, "c"),) == drop(2, {1: "a", 2: "b", 3: "c"}) 130 | 131 | 132 | def test_take_while(): 133 | def is_even(n): 134 | return n % 2 == 0 135 | 136 | def is_even_dict(d): 137 | # checks if the key of dict d is even 138 | return d[0] % 2 == 0 139 | 140 | assert (2, 4, 6) == take_while(is_even, [2, 4, 6, 7, 8, 9, 10]) 141 | assert () == take_while(is_even, [1, 2, 4, 6, 7, 8, 9, 10]) 142 | assert ((2, "a"), (4, "b")) == take_while(is_even_dict, {2: "a", 4: "b", 5: "c"}) 143 | 144 | 145 | def test_drop_while(): 146 | def is_even(n): 147 | return n % 2 == 0 148 | 149 | def is_even_dict(d): 150 | # checks if the key of dict d is even 151 | return d[0] % 2 == 0 152 | 153 | assert (7, 8, 9, 10) == drop_while(is_even, [2, 4, 6, 7, 8, 9, 10]) 154 | assert (1, 2, 4, 6, 7, 8, 9, 10) == drop_while(is_even, [1, 2, 4, 6, 7, 8, 9, 10]) 155 | assert ((5, "c"),) == drop_while(is_even_dict, {2: "a", 4: "b", 5: "c"}) 156 | 157 | 158 | def test_split_with(): 159 | def is_even(n): 160 | return n % 2 == 0 161 | 162 | assert ((2, 4, 6), (7, 8, 9, 10)) == split_with(is_even, [2, 4, 6, 7, 8, 9, 10]) 163 | assert ((), (1, 2, 4, 6, 7, 8, 9, 10)) == split_with( 164 | is_even, [1, 2, 4, 6, 7, 8, 9, 10] 165 | ) 166 | 167 | 168 | def test_count(): 169 | assert count([]) == 0 170 | assert count(iter([])) == 0 171 | assert count([1, 2, 3]) == 3 172 | assert count(range(3)) == 3 173 | 174 | 175 | def test_count_(): 176 | assert count_([]) == (0, []) 177 | assert count_(iter([])) == (0, []) 178 | assert count_([1, 2, 3]) == (3, [1, 2, 3]) 179 | assert count_(range(3)) == ( 180 | 3, 181 | range(0, 3), 182 | ) # since range has a __len__ attribute it will be returned as is 183 | assert count_(map(lambda x: x + 1, [1, 2, 3, 4])) == (4, [2, 3, 4, 5]) 184 | --------------------------------------------------------------------------------