├── .coveragerc ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── python-app.yml │ └── python-publish.yml ├── .gitignore ├── .readthedocs.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __init__.py ├── codecov.yml ├── development.txt ├── docs ├── Makefile ├── make.bat └── source │ ├── _build │ ├── doctrees │ │ ├── contributing.doctree │ │ ├── environment.pickle │ │ ├── examples.doctree │ │ ├── get_started.doctree │ │ ├── index.doctree │ │ ├── introduction.doctree │ │ ├── modules.doctree │ │ └── pyfuncol.doctree │ └── html │ │ ├── .buildinfo │ │ ├── .doctrees │ │ ├── environment.pickle │ │ ├── index.doctree │ │ └── introduction.doctree │ │ ├── _modules │ │ ├── index.html │ │ └── pyfuncol │ │ │ ├── dict.html │ │ │ ├── list.html │ │ │ └── set.html │ │ ├── _sources │ │ ├── contributing.md.txt │ │ ├── examples.md.txt │ │ ├── get_started.md.txt │ │ ├── index.rst.txt │ │ ├── introduction.md.txt │ │ ├── modules.rst.txt │ │ └── pyfuncol.rst.txt │ │ ├── _static │ │ ├── alabaster.css │ │ ├── basic.css │ │ ├── css │ │ │ ├── badge_only.css │ │ │ ├── fonts │ │ │ │ ├── Roboto-Slab-Bold.woff │ │ │ │ ├── Roboto-Slab-Bold.woff2 │ │ │ │ ├── Roboto-Slab-Regular.woff │ │ │ │ ├── Roboto-Slab-Regular.woff2 │ │ │ │ ├── fontawesome-webfont.eot │ │ │ │ ├── fontawesome-webfont.svg │ │ │ │ ├── fontawesome-webfont.ttf │ │ │ │ ├── fontawesome-webfont.woff │ │ │ │ ├── fontawesome-webfont.woff2 │ │ │ │ ├── lato-bold-italic.woff │ │ │ │ ├── lato-bold-italic.woff2 │ │ │ │ ├── lato-bold.woff │ │ │ │ ├── lato-bold.woff2 │ │ │ │ ├── lato-normal-italic.woff │ │ │ │ ├── lato-normal-italic.woff2 │ │ │ │ ├── lato-normal.woff │ │ │ │ └── lato-normal.woff2 │ │ │ └── theme.css │ │ ├── custom.css │ │ ├── doctools.js │ │ ├── documentation_options.js │ │ ├── file.png │ │ ├── jquery-3.5.1.js │ │ ├── jquery.js │ │ ├── js │ │ │ ├── badge_only.js │ │ │ ├── html5shiv-printshiv.min.js │ │ │ ├── html5shiv.min.js │ │ │ └── theme.js │ │ ├── language_data.js │ │ ├── minus.png │ │ ├── plus.png │ │ ├── pygments.css │ │ ├── searchtools.js │ │ ├── underscore-1.13.1.js │ │ └── underscore.js │ │ ├── contributing.html │ │ ├── examples.html │ │ ├── genindex.html │ │ ├── get_started.html │ │ ├── index.html │ │ ├── introduction.html │ │ ├── modules.html │ │ ├── objects.inv │ │ ├── py-modindex.html │ │ ├── pyfuncol.html │ │ ├── search.html │ │ └── searchindex.js │ ├── conf.py │ ├── contributing.md │ ├── examples.md │ ├── get_started.md │ ├── index.rst │ ├── introduction.md │ ├── modules.rst │ └── pyfuncol.rst ├── pyfuncol ├── __init__.py ├── dict.py ├── extend_builtins.py ├── list.py ├── set.py └── tests │ ├── __init__.py │ ├── test_dict.py │ ├── test_list.py │ ├── test_no_forbiddenfruit.py │ └── test_set.py ├── requirements.txt ├── setup.cfg └── setup.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = **/tests/* -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['paypal.me/gondolav'] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] Title" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | 1. ... 17 | 18 | **Expected behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Desktop (please complete the following information):** 22 | - OS: [e.g. macOS Monterey 12.1] 23 | - Python version [e.g. 3.9] 24 | - pyfuncol version [e.g. 1.0] 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE] Title" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" 9 | # Files stored in repository root 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | - package-ecosystem: "github-actions" 14 | # Workflow files stored in the 15 | # default location of `.github/workflows` 16 | directory: "/" 17 | schedule: 18 | interval: "daily" 19 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | Please delete options that are not relevant. 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update 15 | 16 | ## Checklist 17 | 18 | - [ ] My code follows the style guidelines of this project 19 | - [ ] I have made corresponding changes to the documentation 20 | - [ ] I have added tests that prove my fix is effective or that my feature works 21 | -------------------------------------------------------------------------------- /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: CI 5 | 6 | on: 7 | push: 8 | pull_request: 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up Python 3.10 17 | uses: actions/setup-python@v4.5.0 18 | with: 19 | python-version: "3.10" 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | if [ -f development.txt ]; then pip install -r development.txt; fi 24 | - name: Test with pytest 25 | run: | 26 | pytest --cov-config=.coveragerc --cov=pyfuncol --cov-report=xml 27 | - name: Upload code coverage to Codecov 28 | uses: codecov/codecov-action@v3 29 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | jobs: 16 | deploy: 17 | 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Set up Python 23 | uses: actions/setup-python@v4.5.0 24 | with: 25 | python-version: '3.x' 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install build 30 | - name: Build package 31 | run: python -m build 32 | - name: Publish package 33 | uses: pypa/gh-action-pypi-publish@f5622bde02b04381239da3573277701ceca8f6a0 34 | with: 35 | user: __token__ 36 | password: ${{ secrets.PYPI_API_TOKEN }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | 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 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | sphinx: 9 | configuration: docs/source/conf.py 10 | 11 | formats: 12 | - pdf 13 | 14 | python: 15 | version: 3.8 16 | install: 17 | - requirements: development.txt 18 | - method: setuptools 19 | path: . 20 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | andrea.veneziano@icloud.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Setup 4 | 5 | Fork the repo, then install all development requirements with: 6 | 7 | ```shell 8 | pip install -r development.txt 9 | ``` 10 | 11 | When your changes are ready, submit a pull request! 12 | 13 | ## Style 14 | 15 | For formatting and code style, we use [black](https://github.com/psf/black). Docstrings should follow the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings). 16 | 17 | ## Tests 18 | 19 | To run the tests, execute `pytest` at the root of the project. 20 | 21 | To run the tests with coverage enabled, execute: 22 | 23 | ```shell 24 | pytest --cov-config=.coveragerc --cov=pyfuncol --cov-report=xml 25 | ``` 26 | 27 | ## Docs 28 | 29 | The docs are hosted on [Read the Docs](https://pyfuncol.readthedocs.io/en/latest/). Source files are in `docs/source/`. 30 | 31 | To build them locally, run in `docs/`: 32 | 33 | ```shell 34 | make html 35 | ``` 36 | 37 | The HTML files will be stored in `docs/build/`. 38 | 39 | ## Project structure 40 | 41 | ``` 42 | ┌── docs - Contains the docs source code 43 | ├── pyfuncol - Contains all the library source code 44 | ├── tests - Contains tests for all the modules 45 | ├── __init__.py - Contains the function calls that extend built-in types 46 | ├── dict.py - Contains extension functions for dictionaries 47 | ├── list.py - Contains extension functions for lists 48 | └── ... 49 | └── ... 50 | ``` 51 | 52 | ## Release 53 | 54 | To publish a new release on [PyPI](https://pypi.org/project/pyfuncol/): 55 | 56 | 1. Update the version in `setup.py` 57 | 2. Update the version (`release` field) in `docs/source/conf.py` 58 | 3. Push the version bump 59 | 4. Create a new release on [GitHub](https://github.com/didactic-meme/pyfuncol/releases). The newly created tag and the release title should match the version in `setup.py` and `docs/source/conf.py` with 'v' prepended. An example: for version `1.1.1`, the tag and release title should be `v1.1.1`. 60 | 61 | The GitHub release creation will trigger the deploy workflow that builds and uploads the project to PyPI. 62 | 63 | ## Code of Conduct 64 | 65 | Our Code of Conduct is [here](https://github.com/didactic-meme/pyfuncol/blob/main/CODE_OF_CONDUCT.md). By contributing to pyfuncol, you implicitly accept it. 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Andrea Veneziano 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyfuncol 2 | 3 | ![CI](https://github.com/didactic-meme/pyfuncol/actions/workflows/python-app.yml/badge.svg) 4 | [![codecov](https://codecov.io/gh/didactic-meme/pyfuncol/branch/main/graph/badge.svg)](https://codecov.io/gh/didactic-meme/pyfuncol) 5 | ![PyPI](https://img.shields.io/pypi/v/pyfuncol?color=blue) 6 | [![Downloads](https://pepy.tech/badge/pyfuncol)](https://pepy.tech/project/pyfuncol) 7 | [![Documentation Status](https://readthedocs.org/projects/pyfuncol/badge/?version=latest)](https://pyfuncol.readthedocs.io/en/latest/?badge=latest) 8 | [![GitHub license](https://img.shields.io/github/license/didactic-meme/pyfuncol)](https://github.com/didactic-meme/pyfuncol/blob/main/LICENSE) 9 | 10 | - [pyfuncol](#pyfuncol) 11 | - [Installation](#installation) 12 | - [Usage](#usage) 13 | - [Usage without forbiddenfruit](#usage-without-forbiddenfruit) 14 | - [API](#api) 15 | - [Documentation](#documentation) 16 | - [Compatibility](#compatibility) 17 | - [Contributing](#contributing) 18 | - [License](#license) 19 | 20 | A Python functional collections library. It _extends_ collections built-in types with useful methods to write functional Python code. It uses [Forbidden Fruit](https://github.com/clarete/forbiddenfruit) under the hood. 21 | 22 | pyfuncol provides: 23 | 24 | - Standard "eager" methods, such as `map`, `flat_map`, `group_by`, etc. 25 | - Parallel methods, such as `par_map`, `par_flat_map`, etc. 26 | - Pure methods that leverage memoization to improve performance, such as `pure_map`, `pure_flat_map`, etc. 27 | - Lazy methods that return iterators and never materialize results, such as `lazy_map`, `lazy_flat_map`, etc. 28 | 29 | pyfuncol can also be [used without forbiddenfruit](#usage-without-forbiddenfruit). 30 | 31 | ## Installation 32 | 33 | `pip install pyfuncol` 34 | 35 | ## Usage 36 | 37 | > **Note:** If you are not using forbiddenfruit, the functions will not extend the builtins. Please [see here](#usage-without-forbiddenfruit) for usage without forbiddenfruit. 38 | 39 | To use the methods, you just need to import pyfuncol. Some examples: 40 | 41 | ```python 42 | import pyfuncol 43 | 44 | [1, 2, 3, 4].map(lambda x: x * 2).filter(lambda x: x > 4) 45 | # [6, 8] 46 | 47 | [1, 2, 3, 4].fold_left(0, lambda acc, n: acc + n) 48 | # 10 49 | 50 | {1, 2, 3, 4}.map(lambda x: x * 2).filter_not(lambda x: x <= 4) 51 | # {6, 8} 52 | 53 | ["abc", "def", "e"].group_by(lambda s: len(s)) 54 | # {3: ["abc", "def"], 1: ["e"]} 55 | 56 | {"a": 1, "b": 2, "c": 3}.flat_map(lambda kv: {kv[0]: kv[1] ** 2}) 57 | # {"a": 1, "b": 4, "c": 9} 58 | ``` 59 | 60 | pyfuncol provides parallel operations (for now `par_map`, `par_flat_map`, `par_filter` and `par_filter_not`): 61 | 62 | ```python 63 | [1, 2, 3, 4].par_map(lambda x: x * 2).par_filter(lambda x: x > 4) 64 | # [6, 8] 65 | 66 | {1, 2, 3, 4}.par_map(lambda x: x * 2).par_filter_not(lambda x: x <= 4) 67 | # {6, 8} 68 | 69 | {"a": 1, "b": 2, "c": 3}.par_flat_map(lambda kv: {kv[0]: kv[1] ** 2}) 70 | # {"a": 1, "b": 4, "c": 9} 71 | ``` 72 | 73 | pyfuncol provides operations leveraging memoization to improve performance (for now `pure_map`, `pure_flat_map`, `pure_filter` and `pure_filter_not`). These versions work only for **pure** functions (i.e., all calls to the same args return the same value) on hashable inputs: 74 | 75 | ```python 76 | [1, 2, 3, 4].pure_map(lambda x: x * 2).pure_filter(lambda x: x > 4) 77 | # [6, 8] 78 | 79 | {1, 2, 3, 4}.pure_map(lambda x: x * 2).pure_filter_not(lambda x: x <= 4) 80 | # {6, 8} 81 | 82 | {"a": 1, "b": 2, "c": 3}.pure_flat_map(lambda kv: {kv[0]: kv[1] ** 2}) 83 | # {"a": 1, "b": 4, "c": 9} 84 | ``` 85 | 86 | pyfuncol provides lazy operations that never materialize results: 87 | 88 | ```python 89 | list([1, 2, 3, 4].lazy_map(lambda x: x * 2).lazy_filter(lambda x: x > 4)) 90 | # [6, 8] 91 | 92 | list({1, 2, 3, 4}.lazy_map(lambda x: x * 2).lazy_filter_not(lambda x: x <= 4)) 93 | # [6, 8] 94 | 95 | list({"a": 1, "b": 2, "c": 3}.lazy_flat_map(lambda kv: {kv[0]: kv[1] ** 2})) 96 | # [("a", 1), ("b", 4), ("c", 9)] 97 | 98 | set([1, 2, 3, 4].lazy_map(lambda x: x * 2).lazy_filter(lambda x: x > 4)) 99 | # {6, 8} 100 | ``` 101 | 102 | ### Usage without forbiddenfruit 103 | 104 | If you are using a Python interpreter other than CPython, forbiddenfruit will not work. 105 | 106 | Fortunately, if forbiddenfruit does not work on your installation or if you do not want to use it, pyfuncol also supports direct function calls without extending builtins. 107 | 108 | ```python 109 | from pyfuncol import list as pfclist 110 | 111 | pfclist.map([1, 2, 3], lambda x: x * 2) 112 | # [2, 4, 6] 113 | ``` 114 | 115 | ### API 116 | 117 | For lists, please refer to the [docs](https://pyfuncol.readthedocs.io/en/latest/pyfuncol.html#module-pyfuncol.list). 118 | 119 | For dictionaries, please refer to the [docs](https://pyfuncol.readthedocs.io/en/latest/pyfuncol.html#module-pyfuncol.dict). 120 | 121 | For sets and frozensets, please refer to the [docs](https://pyfuncol.readthedocs.io/en/latest/pyfuncol.html#module-pyfuncol.set). 122 | 123 | For more details, please have a look at the [API reference](https://pyfuncol.readthedocs.io/en/latest/modules.html). 124 | 125 | We support all subclasses with default constructors (`OrderedDict`, for example). 126 | 127 | ## Documentation 128 | 129 | See . 130 | 131 | ## Compatibility 132 | 133 | For functions to extend built-ins, [Forbidden Fruit](https://github.com/clarete/forbiddenfruit) is necessary (CPython only). 134 | 135 | ## Contributing 136 | 137 | See the [contributing guide](https://github.com/didactic-meme/pyfuncol/blob/main/CONTRIBUTING.md) for detailed instructions on how to get started with the project. 138 | 139 | ## License 140 | 141 | pyfuncol is licensed under the [MIT license](https://github.com/didactic-meme/pyfuncol/blob/main/LICENSE). 142 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/__init__.py -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: off 4 | project: 5 | default: 6 | # basic 7 | target: 95% 8 | -------------------------------------------------------------------------------- /development.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | black==23.1.0 3 | pytest==7.2.2 4 | pytest-cov==4.0.0 5 | myst-parser==1.0.0 6 | Sphinx==6.1.3 7 | sphinx-rtd-theme==1.2.0 8 | -------------------------------------------------------------------------------- /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 = source 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/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=source 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.https://www.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/source/_build/doctrees/contributing.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/doctrees/contributing.doctree -------------------------------------------------------------------------------- /docs/source/_build/doctrees/environment.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/doctrees/environment.pickle -------------------------------------------------------------------------------- /docs/source/_build/doctrees/examples.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/doctrees/examples.doctree -------------------------------------------------------------------------------- /docs/source/_build/doctrees/get_started.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/doctrees/get_started.doctree -------------------------------------------------------------------------------- /docs/source/_build/doctrees/index.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/doctrees/index.doctree -------------------------------------------------------------------------------- /docs/source/_build/doctrees/introduction.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/doctrees/introduction.doctree -------------------------------------------------------------------------------- /docs/source/_build/doctrees/modules.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/doctrees/modules.doctree -------------------------------------------------------------------------------- /docs/source/_build/doctrees/pyfuncol.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/doctrees/pyfuncol.doctree -------------------------------------------------------------------------------- /docs/source/_build/html/.buildinfo: -------------------------------------------------------------------------------- 1 | # Sphinx build info version 1 2 | # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. 3 | config: 81d4c97122ee566d295f0a6a4f07523d 4 | tags: 645f666f9bcd5a90fca523b33c5a78b7 5 | -------------------------------------------------------------------------------- /docs/source/_build/html/.doctrees/environment.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/html/.doctrees/environment.pickle -------------------------------------------------------------------------------- /docs/source/_build/html/.doctrees/index.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/html/.doctrees/index.doctree -------------------------------------------------------------------------------- /docs/source/_build/html/.doctrees/introduction.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/html/.doctrees/introduction.doctree -------------------------------------------------------------------------------- /docs/source/_build/html/_modules/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Overview: module code — pyfuncol 1.3.1 documentation 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 49 | 50 |
54 | 55 |
56 |
57 |
58 |
    59 |
  • »
  • 60 |
  • Overview: module code
  • 61 |
  • 62 |
  • 63 |
64 |
65 |
66 |
67 |
68 | 69 |

All modules for which code is available

70 | 74 | 75 |
76 |
77 |
78 | 79 |
80 | 81 |
82 |

© Copyright 2021, Andrea Veneziano.

83 |
84 | 85 | Built with Sphinx using a 86 | theme 87 | provided by Read the Docs. 88 | 89 | 90 |
91 |
92 |
93 |
94 |
95 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /docs/source/_build/html/_sources/contributing.md.txt: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Setup 4 | 5 | Fork the repo, then install all development requirements with: 6 | 7 | ```shell 8 | pip install -r development.txt 9 | ``` 10 | 11 | When your changes are ready, submit a pull request! 12 | 13 | ## Style 14 | 15 | For formatting and code style, we use [black](https://github.com/psf/black). Docstrings should follow the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings). 16 | 17 | ## Tests 18 | 19 | To run the tests, execute `pytest` at the root of the project. 20 | 21 | To run the tests with coverage enabled, execute: 22 | 23 | ```shell 24 | pytest --cov-config=.coveragerc --cov=pyfuncol --cov-report=xml 25 | ``` 26 | 27 | ## Docs 28 | 29 | The docs are hosted on [Read the Docs](https://pyfuncol.readthedocs.io/en/latest/). Source files are in `docs/source/`. 30 | 31 | To build them locally, run in `docs/`: 32 | 33 | ```shell 34 | make html 35 | ``` 36 | 37 | The HTML files will be stored in `docs/build/`. 38 | 39 | ## Project structure 40 | 41 | ``` 42 | ┌── docs - Contains the docs source code 43 | ├── pyfuncol - Contains all the library source code 44 | ├── tests - Contains tests for all the modules 45 | ├── __init__.py - Contains the function calls that extend built-in types 46 | ├── dict.py - Contains extension functions for dictionaries 47 | ├── list.py - Contains extension functions for lists 48 | └── ... 49 | └── ... 50 | ``` 51 | 52 | ## Release 53 | 54 | To publish a new release on [PyPI](https://pypi.org/project/pyfuncol/): 55 | 56 | 1. Update the version in `setup.py` 57 | 2. Update the version (`release` field) in `docs/source/conf.py` 58 | 3. Create a new release on [GitHub](https://github.com/didactic-meme/pyfuncol/releases). The newly created tag and the release title should match the version in `setup.py` and `docs/source/conf.py` with 'v' prepended. An example: for version `1.1.1`, the tag and release title should be `v1.1.1`. 59 | 60 | The GitHub release creation will trigger the deploy workflow that builds and uploads the project to PyPI. 61 | 62 | ## Code of Conduct 63 | 64 | Our Code of Conduct is [here](https://github.com/didactic-meme/pyfuncol/blob/main/CODE_OF_CONDUCT.md). By contributing to pyfuncol, you implicitly accept it. 65 | -------------------------------------------------------------------------------- /docs/source/_build/html/_sources/examples.md.txt: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | > **Note:** If you are not using forbiddenfruit, the functions will not extend the builtins. Please [see here](#usage-without-forbiddenfruit) for usage without forbiddenfruit 4 | 5 | To use the methods, you just need to import pyfuncol. Some examples: 6 | 7 | ```python 8 | import pyfuncol 9 | 10 | [1, 2, 3, 4].map(lambda x: x * 2).filter(lambda x: x > 4) 11 | # [6, 8] 12 | 13 | [1, 2, 3, 4].fold_left(0, lambda acc, n: acc + n) 14 | # 10 15 | 16 | {1, 2, 3, 4}.map(lambda x: x * 2).filter_not(lambda x: x <= 4) 17 | # {6, 8} 18 | 19 | ["abc", "def", "e"].group_by(lambda s: len(s)) 20 | # {3: ["abc", "def"], 1: ["e"]} 21 | 22 | {"a": 1, "b": 2, "c": 3}.flat_map(lambda kv: {kv[0]: kv[1] ** 2}) 23 | # {"a": 1, "b": 4, "c": 9} 24 | ``` 25 | 26 | pyfuncol provides parallel operations (for now `par_map`, `par_flat_map`, `par_filter` and `par_filter_not`): 27 | 28 | ```python 29 | [1, 2, 3, 4].par_map(lambda x: x * 2).par_filter(lambda x: x > 4) 30 | # [6, 8] 31 | 32 | {1, 2, 3, 4}.par_map(lambda x: x * 2).par_filter_not(lambda x: x <= 4) 33 | # {6, 8} 34 | 35 | {"a": 1, "b": 2, "c": 3}.par_flat_map(lambda kv: {kv[0]: kv[1] ** 2}) 36 | # {"a": 1, "b": 4, "c": 9} 37 | ``` 38 | 39 | pyfuncol provides operations leveraging memoization to improve performance (for now `pure_map`, `pure_flat_map`, `pure_filter` and `pure_filter_not`). These versions work only for **pure** functions (i.e., all calls to the same args return the same value) on hashable inputs: 40 | 41 | ```python 42 | [1, 2, 3, 4].pure_map(lambda x: x * 2).pure_filter(lambda x: x > 4) 43 | # [6, 8] 44 | 45 | {1, 2, 3, 4}.pure_map(lambda x: x * 2).pure_filter_not(lambda x: x <= 4) 46 | # {6, 8} 47 | 48 | {"a": 1, "b": 2, "c": 3}.pure_flat_map(lambda kv: {kv[0]: kv[1] ** 2}) 49 | # {"a": 1, "b": 4, "c": 9} 50 | ``` 51 | 52 | pyfuncol provides lazy operations that never materialize results: 53 | 54 | ```python 55 | list([1, 2, 3, 4].lazy_map(lambda x: x * 2).lazy_filter(lambda x: x > 4)) 56 | # [6, 8] 57 | 58 | list({1, 2, 3, 4}.lazy_map(lambda x: x * 2).lazy_filter_not(lambda x: x <= 4)) 59 | # [6, 8] 60 | 61 | list({"a": 1, "b": 2, "c": 3}.lazy_flat_map(lambda kv: {kv[0]: kv[1] ** 2})) 62 | # [("a", 1), ("b", 4), ("c", 9)] 63 | 64 | set([1, 2, 3, 4].lazy_map(lambda x: x * 2).lazy_filter(lambda x: x > 4)) 65 | # {6, 8} 66 | ``` 67 | 68 | We support all subclasses with default constructors (`OrderedDict`, for example). 69 | 70 | (usage-without-forbiddenfruit)= 71 | ## Usage without forbiddenfruit 72 | 73 | If you are using a Python intepreter other than CPython, forbiddenfruit will not work. 74 | 75 | Fortunately, if forbiddenfruit does not work on your installation or if you do not want to use it, pyfuncol also supports direct function calls without extending builtins. 76 | 77 | ```python 78 | from pyfuncol import list as pfclist 79 | 80 | pfclist.map([1, 2, 3], lambda x: x * 2) 81 | # [2, 4, 6] 82 | ``` 83 | -------------------------------------------------------------------------------- /docs/source/_build/html/_sources/get_started.md.txt: -------------------------------------------------------------------------------- 1 | # Get Started 2 | 3 | pyfuncol is supported only on Python 3.6+ and CPython (+ forbiddenfruit) is required for extending builtins. Albeit untested, it might still work on older Python versions. 4 | 5 | To install it, run: 6 | 7 | ```shell 8 | pip install pyfuncol 9 | ``` 10 | -------------------------------------------------------------------------------- /docs/source/_build/html/_sources/index.rst.txt: -------------------------------------------------------------------------------- 1 | .. pyfuncol documentation master file, created by 2 | sphinx-quickstart on Sat Dec 18 23:25:15 2021. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to pyfuncol's documentation! 7 | ==================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents 12 | 13 | introduction 14 | get_started 15 | examples 16 | contributing 17 | modules 18 | 19 | 20 | 21 | Indices and tables 22 | ================== 23 | 24 | * :ref:`genindex` 25 | * :ref:`modindex` 26 | * :ref:`search` 27 | -------------------------------------------------------------------------------- /docs/source/_build/html/_sources/introduction.md.txt: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | [pyfuncol](https://github.com/didactic-meme/pyfuncol) is a Python functional collections library. It _extends_ collections built-in types with useful methods to write functional Python code. It uses [Forbidden Fruit](https://github.com/clarete/forbiddenfruit) under the hood. 4 | 5 | pyfuncol provides: 6 | 7 | - Standard "eager" methods, such as `map`, `flat_map`, `group_by`, etc. 8 | - Parallel methods, such as `par_map`, `par_flat_map`, etc. 9 | - Pure methods that leverage memoization to improve performance, such as `pure_map`, `pure_flat_map`, etc. 10 | - Lazy methods that return iterators and never materialize results, such as `lazy_map`, `lazy_flat_map`, etc. 11 | 12 | pyfuncol can also be [used without forbiddenfruit](usage-without-forbiddenfruit). 13 | -------------------------------------------------------------------------------- /docs/source/_build/html/_sources/modules.rst.txt: -------------------------------------------------------------------------------- 1 | API reference 2 | ============= 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | pyfuncol 8 | -------------------------------------------------------------------------------- /docs/source/_build/html/_sources/pyfuncol.rst.txt: -------------------------------------------------------------------------------- 1 | pyfuncol package 2 | ================ 3 | 4 | Submodules 5 | ---------- 6 | 7 | pyfuncol.dict module 8 | -------------------- 9 | 10 | .. automodule:: pyfuncol.dict 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | pyfuncol.list module 16 | -------------------- 17 | 18 | .. automodule:: pyfuncol.list 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | pyfuncol.set module 24 | -------------------- 25 | 26 | .. automodule:: pyfuncol.set 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | 32 | Module contents 33 | --------------- 34 | 35 | .. automodule:: pyfuncol 36 | :members: 37 | :undoc-members: 38 | :show-inheritance: 39 | -------------------------------------------------------------------------------- /docs/source/_build/html/_static/alabaster.css: -------------------------------------------------------------------------------- 1 | @import url("basic.css"); 2 | 3 | /* -- page layout ----------------------------------------------------------- */ 4 | 5 | body { 6 | font-family: Georgia, serif; 7 | font-size: 17px; 8 | background-color: #fff; 9 | color: #000; 10 | margin: 0; 11 | padding: 0; 12 | } 13 | 14 | 15 | div.document { 16 | width: 940px; 17 | margin: 30px auto 0 auto; 18 | } 19 | 20 | div.documentwrapper { 21 | float: left; 22 | width: 100%; 23 | } 24 | 25 | div.bodywrapper { 26 | margin: 0 0 0 220px; 27 | } 28 | 29 | div.sphinxsidebar { 30 | width: 220px; 31 | font-size: 14px; 32 | line-height: 1.5; 33 | } 34 | 35 | hr { 36 | border: 1px solid #B1B4B6; 37 | } 38 | 39 | div.body { 40 | background-color: #fff; 41 | color: #3E4349; 42 | padding: 0 30px 0 30px; 43 | } 44 | 45 | div.body > .section { 46 | text-align: left; 47 | } 48 | 49 | div.footer { 50 | width: 940px; 51 | margin: 20px auto 30px auto; 52 | font-size: 14px; 53 | color: #888; 54 | text-align: right; 55 | } 56 | 57 | div.footer a { 58 | color: #888; 59 | } 60 | 61 | p.caption { 62 | font-family: inherit; 63 | font-size: inherit; 64 | } 65 | 66 | 67 | div.relations { 68 | display: none; 69 | } 70 | 71 | 72 | div.sphinxsidebar a { 73 | color: #444; 74 | text-decoration: none; 75 | border-bottom: 1px dotted #999; 76 | } 77 | 78 | div.sphinxsidebar a:hover { 79 | border-bottom: 1px solid #999; 80 | } 81 | 82 | div.sphinxsidebarwrapper { 83 | padding: 18px 10px; 84 | } 85 | 86 | div.sphinxsidebarwrapper p.logo { 87 | padding: 0; 88 | margin: -10px 0 0 0px; 89 | text-align: center; 90 | } 91 | 92 | div.sphinxsidebarwrapper h1.logo { 93 | margin-top: -10px; 94 | text-align: center; 95 | margin-bottom: 5px; 96 | text-align: left; 97 | } 98 | 99 | div.sphinxsidebarwrapper h1.logo-name { 100 | margin-top: 0px; 101 | } 102 | 103 | div.sphinxsidebarwrapper p.blurb { 104 | margin-top: 0; 105 | font-style: normal; 106 | } 107 | 108 | div.sphinxsidebar h3, 109 | div.sphinxsidebar h4 { 110 | font-family: Georgia, serif; 111 | color: #444; 112 | font-size: 24px; 113 | font-weight: normal; 114 | margin: 0 0 5px 0; 115 | padding: 0; 116 | } 117 | 118 | div.sphinxsidebar h4 { 119 | font-size: 20px; 120 | } 121 | 122 | div.sphinxsidebar h3 a { 123 | color: #444; 124 | } 125 | 126 | div.sphinxsidebar p.logo a, 127 | div.sphinxsidebar h3 a, 128 | div.sphinxsidebar p.logo a:hover, 129 | div.sphinxsidebar h3 a:hover { 130 | border: none; 131 | } 132 | 133 | div.sphinxsidebar p { 134 | color: #555; 135 | margin: 10px 0; 136 | } 137 | 138 | div.sphinxsidebar ul { 139 | margin: 10px 0; 140 | padding: 0; 141 | color: #000; 142 | } 143 | 144 | div.sphinxsidebar ul li.toctree-l1 > a { 145 | font-size: 120%; 146 | } 147 | 148 | div.sphinxsidebar ul li.toctree-l2 > a { 149 | font-size: 110%; 150 | } 151 | 152 | div.sphinxsidebar input { 153 | border: 1px solid #CCC; 154 | font-family: Georgia, serif; 155 | font-size: 1em; 156 | } 157 | 158 | div.sphinxsidebar hr { 159 | border: none; 160 | height: 1px; 161 | color: #AAA; 162 | background: #AAA; 163 | 164 | text-align: left; 165 | margin-left: 0; 166 | width: 50%; 167 | } 168 | 169 | div.sphinxsidebar .badge { 170 | border-bottom: none; 171 | } 172 | 173 | div.sphinxsidebar .badge:hover { 174 | border-bottom: none; 175 | } 176 | 177 | /* To address an issue with donation coming after search */ 178 | div.sphinxsidebar h3.donation { 179 | margin-top: 10px; 180 | } 181 | 182 | /* -- body styles ----------------------------------------------------------- */ 183 | 184 | a { 185 | color: #004B6B; 186 | text-decoration: underline; 187 | } 188 | 189 | a:hover { 190 | color: #6D4100; 191 | text-decoration: underline; 192 | } 193 | 194 | div.body h1, 195 | div.body h2, 196 | div.body h3, 197 | div.body h4, 198 | div.body h5, 199 | div.body h6 { 200 | font-family: Georgia, serif; 201 | font-weight: normal; 202 | margin: 30px 0px 10px 0px; 203 | padding: 0; 204 | } 205 | 206 | div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } 207 | div.body h2 { font-size: 180%; } 208 | div.body h3 { font-size: 150%; } 209 | div.body h4 { font-size: 130%; } 210 | div.body h5 { font-size: 100%; } 211 | div.body h6 { font-size: 100%; } 212 | 213 | a.headerlink { 214 | color: #DDD; 215 | padding: 0 4px; 216 | text-decoration: none; 217 | } 218 | 219 | a.headerlink:hover { 220 | color: #444; 221 | background: #EAEAEA; 222 | } 223 | 224 | div.body p, div.body dd, div.body li { 225 | line-height: 1.4em; 226 | } 227 | 228 | div.admonition { 229 | margin: 20px 0px; 230 | padding: 10px 30px; 231 | background-color: #EEE; 232 | border: 1px solid #CCC; 233 | } 234 | 235 | div.admonition tt.xref, div.admonition code.xref, div.admonition a tt { 236 | background-color: #FBFBFB; 237 | border-bottom: 1px solid #fafafa; 238 | } 239 | 240 | div.admonition p.admonition-title { 241 | font-family: Georgia, serif; 242 | font-weight: normal; 243 | font-size: 24px; 244 | margin: 0 0 10px 0; 245 | padding: 0; 246 | line-height: 1; 247 | } 248 | 249 | div.admonition p.last { 250 | margin-bottom: 0; 251 | } 252 | 253 | div.highlight { 254 | background-color: #fff; 255 | } 256 | 257 | dt:target, .highlight { 258 | background: #FAF3E8; 259 | } 260 | 261 | div.warning { 262 | background-color: #FCC; 263 | border: 1px solid #FAA; 264 | } 265 | 266 | div.danger { 267 | background-color: #FCC; 268 | border: 1px solid #FAA; 269 | -moz-box-shadow: 2px 2px 4px #D52C2C; 270 | -webkit-box-shadow: 2px 2px 4px #D52C2C; 271 | box-shadow: 2px 2px 4px #D52C2C; 272 | } 273 | 274 | div.error { 275 | background-color: #FCC; 276 | border: 1px solid #FAA; 277 | -moz-box-shadow: 2px 2px 4px #D52C2C; 278 | -webkit-box-shadow: 2px 2px 4px #D52C2C; 279 | box-shadow: 2px 2px 4px #D52C2C; 280 | } 281 | 282 | div.caution { 283 | background-color: #FCC; 284 | border: 1px solid #FAA; 285 | } 286 | 287 | div.attention { 288 | background-color: #FCC; 289 | border: 1px solid #FAA; 290 | } 291 | 292 | div.important { 293 | background-color: #EEE; 294 | border: 1px solid #CCC; 295 | } 296 | 297 | div.note { 298 | background-color: #EEE; 299 | border: 1px solid #CCC; 300 | } 301 | 302 | div.tip { 303 | background-color: #EEE; 304 | border: 1px solid #CCC; 305 | } 306 | 307 | div.hint { 308 | background-color: #EEE; 309 | border: 1px solid #CCC; 310 | } 311 | 312 | div.seealso { 313 | background-color: #EEE; 314 | border: 1px solid #CCC; 315 | } 316 | 317 | div.topic { 318 | background-color: #EEE; 319 | } 320 | 321 | p.admonition-title { 322 | display: inline; 323 | } 324 | 325 | p.admonition-title:after { 326 | content: ":"; 327 | } 328 | 329 | pre, tt, code { 330 | font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 331 | font-size: 0.9em; 332 | } 333 | 334 | .hll { 335 | background-color: #FFC; 336 | margin: 0 -12px; 337 | padding: 0 12px; 338 | display: block; 339 | } 340 | 341 | img.screenshot { 342 | } 343 | 344 | tt.descname, tt.descclassname, code.descname, code.descclassname { 345 | font-size: 0.95em; 346 | } 347 | 348 | tt.descname, code.descname { 349 | padding-right: 0.08em; 350 | } 351 | 352 | img.screenshot { 353 | -moz-box-shadow: 2px 2px 4px #EEE; 354 | -webkit-box-shadow: 2px 2px 4px #EEE; 355 | box-shadow: 2px 2px 4px #EEE; 356 | } 357 | 358 | table.docutils { 359 | border: 1px solid #888; 360 | -moz-box-shadow: 2px 2px 4px #EEE; 361 | -webkit-box-shadow: 2px 2px 4px #EEE; 362 | box-shadow: 2px 2px 4px #EEE; 363 | } 364 | 365 | table.docutils td, table.docutils th { 366 | border: 1px solid #888; 367 | padding: 0.25em 0.7em; 368 | } 369 | 370 | table.field-list, table.footnote { 371 | border: none; 372 | -moz-box-shadow: none; 373 | -webkit-box-shadow: none; 374 | box-shadow: none; 375 | } 376 | 377 | table.footnote { 378 | margin: 15px 0; 379 | width: 100%; 380 | border: 1px solid #EEE; 381 | background: #FDFDFD; 382 | font-size: 0.9em; 383 | } 384 | 385 | table.footnote + table.footnote { 386 | margin-top: -15px; 387 | border-top: none; 388 | } 389 | 390 | table.field-list th { 391 | padding: 0 0.8em 0 0; 392 | } 393 | 394 | table.field-list td { 395 | padding: 0; 396 | } 397 | 398 | table.field-list p { 399 | margin-bottom: 0.8em; 400 | } 401 | 402 | /* Cloned from 403 | * https://github.com/sphinx-doc/sphinx/commit/ef60dbfce09286b20b7385333d63a60321784e68 404 | */ 405 | .field-name { 406 | -moz-hyphens: manual; 407 | -ms-hyphens: manual; 408 | -webkit-hyphens: manual; 409 | hyphens: manual; 410 | } 411 | 412 | table.footnote td.label { 413 | width: .1px; 414 | padding: 0.3em 0 0.3em 0.5em; 415 | } 416 | 417 | table.footnote td { 418 | padding: 0.3em 0.5em; 419 | } 420 | 421 | dl { 422 | margin: 0; 423 | padding: 0; 424 | } 425 | 426 | dl dd { 427 | margin-left: 30px; 428 | } 429 | 430 | blockquote { 431 | margin: 0 0 0 30px; 432 | padding: 0; 433 | } 434 | 435 | ul, ol { 436 | /* Matches the 30px from the narrow-screen "li > ul" selector below */ 437 | margin: 10px 0 10px 30px; 438 | padding: 0; 439 | } 440 | 441 | pre { 442 | background: #EEE; 443 | padding: 7px 30px; 444 | margin: 15px 0px; 445 | line-height: 1.3em; 446 | } 447 | 448 | div.viewcode-block:target { 449 | background: #ffd; 450 | } 451 | 452 | dl pre, blockquote pre, li pre { 453 | margin-left: 0; 454 | padding-left: 30px; 455 | } 456 | 457 | tt, code { 458 | background-color: #ecf0f3; 459 | color: #222; 460 | /* padding: 1px 2px; */ 461 | } 462 | 463 | tt.xref, code.xref, a tt { 464 | background-color: #FBFBFB; 465 | border-bottom: 1px solid #fff; 466 | } 467 | 468 | a.reference { 469 | text-decoration: none; 470 | border-bottom: 1px dotted #004B6B; 471 | } 472 | 473 | /* Don't put an underline on images */ 474 | a.image-reference, a.image-reference:hover { 475 | border-bottom: none; 476 | } 477 | 478 | a.reference:hover { 479 | border-bottom: 1px solid #6D4100; 480 | } 481 | 482 | a.footnote-reference { 483 | text-decoration: none; 484 | font-size: 0.7em; 485 | vertical-align: top; 486 | border-bottom: 1px dotted #004B6B; 487 | } 488 | 489 | a.footnote-reference:hover { 490 | border-bottom: 1px solid #6D4100; 491 | } 492 | 493 | a:hover tt, a:hover code { 494 | background: #EEE; 495 | } 496 | 497 | 498 | @media screen and (max-width: 870px) { 499 | 500 | div.sphinxsidebar { 501 | display: none; 502 | } 503 | 504 | div.document { 505 | width: 100%; 506 | 507 | } 508 | 509 | div.documentwrapper { 510 | margin-left: 0; 511 | margin-top: 0; 512 | margin-right: 0; 513 | margin-bottom: 0; 514 | } 515 | 516 | div.bodywrapper { 517 | margin-top: 0; 518 | margin-right: 0; 519 | margin-bottom: 0; 520 | margin-left: 0; 521 | } 522 | 523 | ul { 524 | margin-left: 0; 525 | } 526 | 527 | li > ul { 528 | /* Matches the 30px from the "ul, ol" selector above */ 529 | margin-left: 30px; 530 | } 531 | 532 | .document { 533 | width: auto; 534 | } 535 | 536 | .footer { 537 | width: auto; 538 | } 539 | 540 | .bodywrapper { 541 | margin: 0; 542 | } 543 | 544 | .footer { 545 | width: auto; 546 | } 547 | 548 | .github { 549 | display: none; 550 | } 551 | 552 | 553 | 554 | } 555 | 556 | 557 | 558 | @media screen and (max-width: 875px) { 559 | 560 | body { 561 | margin: 0; 562 | padding: 20px 30px; 563 | } 564 | 565 | div.documentwrapper { 566 | float: none; 567 | background: #fff; 568 | } 569 | 570 | div.sphinxsidebar { 571 | display: block; 572 | float: none; 573 | width: 102.5%; 574 | margin: 50px -30px -20px -30px; 575 | padding: 10px 20px; 576 | background: #333; 577 | color: #FFF; 578 | } 579 | 580 | div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, 581 | div.sphinxsidebar h3 a { 582 | color: #fff; 583 | } 584 | 585 | div.sphinxsidebar a { 586 | color: #AAA; 587 | } 588 | 589 | div.sphinxsidebar p.logo { 590 | display: none; 591 | } 592 | 593 | div.document { 594 | width: 100%; 595 | margin: 0; 596 | } 597 | 598 | div.footer { 599 | display: none; 600 | } 601 | 602 | div.bodywrapper { 603 | margin: 0; 604 | } 605 | 606 | div.body { 607 | min-height: 0; 608 | padding: 0; 609 | } 610 | 611 | .rtd_doc_footer { 612 | display: none; 613 | } 614 | 615 | .document { 616 | width: auto; 617 | } 618 | 619 | .footer { 620 | width: auto; 621 | } 622 | 623 | .footer { 624 | width: auto; 625 | } 626 | 627 | .github { 628 | display: none; 629 | } 630 | } 631 | 632 | 633 | /* misc. */ 634 | 635 | .revsys-inline { 636 | display: none!important; 637 | } 638 | 639 | /* Make nested-list/multi-paragraph items look better in Releases changelog 640 | * pages. Without this, docutils' magical list fuckery causes inconsistent 641 | * formatting between different release sub-lists. 642 | */ 643 | div#changelog > div.section > ul > li > p:only-child { 644 | margin-bottom: 0; 645 | } 646 | 647 | /* Hide fugly table cell borders in ..bibliography:: directive output */ 648 | table.docutils.citation, table.docutils.citation td, table.docutils.citation th { 649 | border: none; 650 | /* Below needed in some edge cases; if not applied, bottom shadows appear */ 651 | -moz-box-shadow: none; 652 | -webkit-box-shadow: none; 653 | box-shadow: none; 654 | } 655 | 656 | 657 | /* relbar */ 658 | 659 | .related { 660 | line-height: 30px; 661 | width: 100%; 662 | font-size: 0.9rem; 663 | } 664 | 665 | .related.top { 666 | border-bottom: 1px solid #EEE; 667 | margin-bottom: 20px; 668 | } 669 | 670 | .related.bottom { 671 | border-top: 1px solid #EEE; 672 | } 673 | 674 | .related ul { 675 | padding: 0; 676 | margin: 0; 677 | list-style: none; 678 | } 679 | 680 | .related li { 681 | display: inline; 682 | } 683 | 684 | nav#rellinks { 685 | float: right; 686 | } 687 | 688 | nav#rellinks li+li:before { 689 | content: "|"; 690 | } 691 | 692 | nav#breadcrumbs li+li:before { 693 | content: "\00BB"; 694 | } 695 | 696 | /* Hide certain items when printing */ 697 | @media print { 698 | div.related { 699 | display: none; 700 | } 701 | } -------------------------------------------------------------------------------- /docs/source/_build/html/_static/css/badge_only.css: -------------------------------------------------------------------------------- 1 | .fa:before{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}} -------------------------------------------------------------------------------- /docs/source/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff -------------------------------------------------------------------------------- /docs/source/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff2 -------------------------------------------------------------------------------- /docs/source/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff -------------------------------------------------------------------------------- /docs/source/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff2 -------------------------------------------------------------------------------- /docs/source/_build/html/_static/css/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/html/_static/css/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /docs/source/_build/html/_static/css/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/html/_static/css/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /docs/source/_build/html/_static/css/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/html/_static/css/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /docs/source/_build/html/_static/css/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/html/_static/css/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /docs/source/_build/html/_static/css/fonts/lato-bold-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/html/_static/css/fonts/lato-bold-italic.woff -------------------------------------------------------------------------------- /docs/source/_build/html/_static/css/fonts/lato-bold-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/html/_static/css/fonts/lato-bold-italic.woff2 -------------------------------------------------------------------------------- /docs/source/_build/html/_static/css/fonts/lato-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/html/_static/css/fonts/lato-bold.woff -------------------------------------------------------------------------------- /docs/source/_build/html/_static/css/fonts/lato-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/html/_static/css/fonts/lato-bold.woff2 -------------------------------------------------------------------------------- /docs/source/_build/html/_static/css/fonts/lato-normal-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/html/_static/css/fonts/lato-normal-italic.woff -------------------------------------------------------------------------------- /docs/source/_build/html/_static/css/fonts/lato-normal-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/html/_static/css/fonts/lato-normal-italic.woff2 -------------------------------------------------------------------------------- /docs/source/_build/html/_static/css/fonts/lato-normal.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/html/_static/css/fonts/lato-normal.woff -------------------------------------------------------------------------------- /docs/source/_build/html/_static/css/fonts/lato-normal.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/html/_static/css/fonts/lato-normal.woff2 -------------------------------------------------------------------------------- /docs/source/_build/html/_static/custom.css: -------------------------------------------------------------------------------- 1 | /* This file intentionally left blank. */ 2 | -------------------------------------------------------------------------------- /docs/source/_build/html/_static/doctools.js: -------------------------------------------------------------------------------- 1 | /* 2 | * doctools.js 3 | * ~~~~~~~~~~~ 4 | * 5 | * Sphinx JavaScript utilities for all documentation. 6 | * 7 | * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | /** 13 | * select a different prefix for underscore 14 | */ 15 | $u = _.noConflict(); 16 | 17 | /** 18 | * make the code below compatible with browsers without 19 | * an installed firebug like debugger 20 | if (!window.console || !console.firebug) { 21 | var names = ["log", "debug", "info", "warn", "error", "assert", "dir", 22 | "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", 23 | "profile", "profileEnd"]; 24 | window.console = {}; 25 | for (var i = 0; i < names.length; ++i) 26 | window.console[names[i]] = function() {}; 27 | } 28 | */ 29 | 30 | /** 31 | * small helper function to urldecode strings 32 | * 33 | * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL 34 | */ 35 | jQuery.urldecode = function(x) { 36 | if (!x) { 37 | return x 38 | } 39 | return decodeURIComponent(x.replace(/\+/g, ' ')); 40 | }; 41 | 42 | /** 43 | * small helper function to urlencode strings 44 | */ 45 | jQuery.urlencode = encodeURIComponent; 46 | 47 | /** 48 | * This function returns the parsed url parameters of the 49 | * current request. Multiple values per key are supported, 50 | * it will always return arrays of strings for the value parts. 51 | */ 52 | jQuery.getQueryParameters = function(s) { 53 | if (typeof s === 'undefined') 54 | s = document.location.search; 55 | var parts = s.substr(s.indexOf('?') + 1).split('&'); 56 | var result = {}; 57 | for (var i = 0; i < parts.length; i++) { 58 | var tmp = parts[i].split('=', 2); 59 | var key = jQuery.urldecode(tmp[0]); 60 | var value = jQuery.urldecode(tmp[1]); 61 | if (key in result) 62 | result[key].push(value); 63 | else 64 | result[key] = [value]; 65 | } 66 | return result; 67 | }; 68 | 69 | /** 70 | * highlight a given string on a jquery object by wrapping it in 71 | * span elements with the given class name. 72 | */ 73 | jQuery.fn.highlightText = function(text, className) { 74 | function highlight(node, addItems) { 75 | if (node.nodeType === 3) { 76 | var val = node.nodeValue; 77 | var pos = val.toLowerCase().indexOf(text); 78 | if (pos >= 0 && 79 | !jQuery(node.parentNode).hasClass(className) && 80 | !jQuery(node.parentNode).hasClass("nohighlight")) { 81 | var span; 82 | var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); 83 | if (isInSVG) { 84 | span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); 85 | } else { 86 | span = document.createElement("span"); 87 | span.className = className; 88 | } 89 | span.appendChild(document.createTextNode(val.substr(pos, text.length))); 90 | node.parentNode.insertBefore(span, node.parentNode.insertBefore( 91 | document.createTextNode(val.substr(pos + text.length)), 92 | node.nextSibling)); 93 | node.nodeValue = val.substr(0, pos); 94 | if (isInSVG) { 95 | var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); 96 | var bbox = node.parentElement.getBBox(); 97 | rect.x.baseVal.value = bbox.x; 98 | rect.y.baseVal.value = bbox.y; 99 | rect.width.baseVal.value = bbox.width; 100 | rect.height.baseVal.value = bbox.height; 101 | rect.setAttribute('class', className); 102 | addItems.push({ 103 | "parent": node.parentNode, 104 | "target": rect}); 105 | } 106 | } 107 | } 108 | else if (!jQuery(node).is("button, select, textarea")) { 109 | jQuery.each(node.childNodes, function() { 110 | highlight(this, addItems); 111 | }); 112 | } 113 | } 114 | var addItems = []; 115 | var result = this.each(function() { 116 | highlight(this, addItems); 117 | }); 118 | for (var i = 0; i < addItems.length; ++i) { 119 | jQuery(addItems[i].parent).before(addItems[i].target); 120 | } 121 | return result; 122 | }; 123 | 124 | /* 125 | * backward compatibility for jQuery.browser 126 | * This will be supported until firefox bug is fixed. 127 | */ 128 | if (!jQuery.browser) { 129 | jQuery.uaMatch = function(ua) { 130 | ua = ua.toLowerCase(); 131 | 132 | var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || 133 | /(webkit)[ \/]([\w.]+)/.exec(ua) || 134 | /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || 135 | /(msie) ([\w.]+)/.exec(ua) || 136 | ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || 137 | []; 138 | 139 | return { 140 | browser: match[ 1 ] || "", 141 | version: match[ 2 ] || "0" 142 | }; 143 | }; 144 | jQuery.browser = {}; 145 | jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; 146 | } 147 | 148 | /** 149 | * Small JavaScript module for the documentation. 150 | */ 151 | var Documentation = { 152 | 153 | init : function() { 154 | this.fixFirefoxAnchorBug(); 155 | this.highlightSearchWords(); 156 | this.initIndexTable(); 157 | if (DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) { 158 | this.initOnKeyListeners(); 159 | } 160 | }, 161 | 162 | /** 163 | * i18n support 164 | */ 165 | TRANSLATIONS : {}, 166 | PLURAL_EXPR : function(n) { return n === 1 ? 0 : 1; }, 167 | LOCALE : 'unknown', 168 | 169 | // gettext and ngettext don't access this so that the functions 170 | // can safely bound to a different name (_ = Documentation.gettext) 171 | gettext : function(string) { 172 | var translated = Documentation.TRANSLATIONS[string]; 173 | if (typeof translated === 'undefined') 174 | return string; 175 | return (typeof translated === 'string') ? translated : translated[0]; 176 | }, 177 | 178 | ngettext : function(singular, plural, n) { 179 | var translated = Documentation.TRANSLATIONS[singular]; 180 | if (typeof translated === 'undefined') 181 | return (n == 1) ? singular : plural; 182 | return translated[Documentation.PLURALEXPR(n)]; 183 | }, 184 | 185 | addTranslations : function(catalog) { 186 | for (var key in catalog.messages) 187 | this.TRANSLATIONS[key] = catalog.messages[key]; 188 | this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')'); 189 | this.LOCALE = catalog.locale; 190 | }, 191 | 192 | /** 193 | * add context elements like header anchor links 194 | */ 195 | addContextElements : function() { 196 | $('div[id] > :header:first').each(function() { 197 | $('\u00B6'). 198 | attr('href', '#' + this.id). 199 | attr('title', _('Permalink to this headline')). 200 | appendTo(this); 201 | }); 202 | $('dt[id]').each(function() { 203 | $('\u00B6'). 204 | attr('href', '#' + this.id). 205 | attr('title', _('Permalink to this definition')). 206 | appendTo(this); 207 | }); 208 | }, 209 | 210 | /** 211 | * workaround a firefox stupidity 212 | * see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075 213 | */ 214 | fixFirefoxAnchorBug : function() { 215 | if (document.location.hash && $.browser.mozilla) 216 | window.setTimeout(function() { 217 | document.location.href += ''; 218 | }, 10); 219 | }, 220 | 221 | /** 222 | * highlight the search words provided in the url in the text 223 | */ 224 | highlightSearchWords : function() { 225 | var params = $.getQueryParameters(); 226 | var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : []; 227 | if (terms.length) { 228 | var body = $('div.body'); 229 | if (!body.length) { 230 | body = $('body'); 231 | } 232 | window.setTimeout(function() { 233 | $.each(terms, function() { 234 | body.highlightText(this.toLowerCase(), 'highlighted'); 235 | }); 236 | }, 10); 237 | $('') 239 | .appendTo($('#searchbox')); 240 | } 241 | }, 242 | 243 | /** 244 | * init the domain index toggle buttons 245 | */ 246 | initIndexTable : function() { 247 | var togglers = $('img.toggler').click(function() { 248 | var src = $(this).attr('src'); 249 | var idnum = $(this).attr('id').substr(7); 250 | $('tr.cg-' + idnum).toggle(); 251 | if (src.substr(-9) === 'minus.png') 252 | $(this).attr('src', src.substr(0, src.length-9) + 'plus.png'); 253 | else 254 | $(this).attr('src', src.substr(0, src.length-8) + 'minus.png'); 255 | }).css('display', ''); 256 | if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) { 257 | togglers.click(); 258 | } 259 | }, 260 | 261 | /** 262 | * helper function to hide the search marks again 263 | */ 264 | hideSearchWords : function() { 265 | $('#searchbox .highlight-link').fadeOut(300); 266 | $('span.highlighted').removeClass('highlighted'); 267 | var url = new URL(window.location); 268 | url.searchParams.delete('highlight'); 269 | window.history.replaceState({}, '', url); 270 | }, 271 | 272 | /** 273 | * make the url absolute 274 | */ 275 | makeURL : function(relativeURL) { 276 | return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL; 277 | }, 278 | 279 | /** 280 | * get the current relative url 281 | */ 282 | getCurrentURL : function() { 283 | var path = document.location.pathname; 284 | var parts = path.split(/\//); 285 | $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() { 286 | if (this === '..') 287 | parts.pop(); 288 | }); 289 | var url = parts.join('/'); 290 | return path.substring(url.lastIndexOf('/') + 1, path.length - 1); 291 | }, 292 | 293 | initOnKeyListeners: function() { 294 | $(document).keydown(function(event) { 295 | var activeElementType = document.activeElement.tagName; 296 | // don't navigate when in search box, textarea, dropdown or button 297 | if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT' 298 | && activeElementType !== 'BUTTON' && !event.altKey && !event.ctrlKey && !event.metaKey 299 | && !event.shiftKey) { 300 | switch (event.keyCode) { 301 | case 37: // left 302 | var prevHref = $('link[rel="prev"]').prop('href'); 303 | if (prevHref) { 304 | window.location.href = prevHref; 305 | return false; 306 | } 307 | break; 308 | case 39: // right 309 | var nextHref = $('link[rel="next"]').prop('href'); 310 | if (nextHref) { 311 | window.location.href = nextHref; 312 | return false; 313 | } 314 | break; 315 | } 316 | } 317 | }); 318 | } 319 | }; 320 | 321 | // quick alias for translations 322 | _ = Documentation.gettext; 323 | 324 | $(document).ready(function() { 325 | Documentation.init(); 326 | }); 327 | -------------------------------------------------------------------------------- /docs/source/_build/html/_static/documentation_options.js: -------------------------------------------------------------------------------- 1 | var DOCUMENTATION_OPTIONS = { 2 | URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), 3 | VERSION: '1.3.1', 4 | LANGUAGE: 'None', 5 | COLLAPSE_INDEX: false, 6 | BUILDER: 'html', 7 | FILE_SUFFIX: '.html', 8 | LINK_SUFFIX: '.html', 9 | HAS_SOURCE: true, 10 | SOURCELINK_SUFFIX: '.txt', 11 | NAVIGATION_WITH_KEYS: false 12 | }; -------------------------------------------------------------------------------- /docs/source/_build/html/_static/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/html/_static/file.png -------------------------------------------------------------------------------- /docs/source/_build/html/_static/js/badge_only.js: -------------------------------------------------------------------------------- 1 | !function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=4)}({4:function(e,t,r){}}); -------------------------------------------------------------------------------- /docs/source/_build/html/_static/js/html5shiv-printshiv.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @preserve HTML5 Shiv 3.7.3-pre | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed 3 | */ 4 | !function(a,b){function c(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=y.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=y.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),y.elements=c+" "+a,j(b)}function f(a){var b=x[a[v]];return b||(b={},w++,a[v]=w,x[w]=b),b}function g(a,c,d){if(c||(c=b),q)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():u.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||t.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),q)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return y.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(y,b.frag)}function j(a){a||(a=b);var d=f(a);return!y.shivCSS||p||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),q||i(a,d),a}function k(a){for(var b,c=a.getElementsByTagName("*"),e=c.length,f=RegExp("^(?:"+d().join("|")+")$","i"),g=[];e--;)b=c[e],f.test(b.nodeName)&&g.push(b.applyElement(l(b)));return g}function l(a){for(var b,c=a.attributes,d=c.length,e=a.ownerDocument.createElement(A+":"+a.nodeName);d--;)b=c[d],b.specified&&e.setAttribute(b.nodeName,b.nodeValue);return e.style.cssText=a.style.cssText,e}function m(a){for(var b,c=a.split("{"),e=c.length,f=RegExp("(^|[\\s,>+~])("+d().join("|")+")(?=[[\\s,>+~#.:]|$)","gi"),g="$1"+A+"\\:$2";e--;)b=c[e]=c[e].split("}"),b[b.length-1]=b[b.length-1].replace(f,g),c[e]=b.join("}");return c.join("{")}function n(a){for(var b=a.length;b--;)a[b].removeNode()}function o(a){function b(){clearTimeout(g._removeSheetTimer),d&&d.removeNode(!0),d=null}var d,e,g=f(a),h=a.namespaces,i=a.parentWindow;return!B||a.printShived?a:("undefined"==typeof h[A]&&h.add(A),i.attachEvent("onbeforeprint",function(){b();for(var f,g,h,i=a.styleSheets,j=[],l=i.length,n=Array(l);l--;)n[l]=i[l];for(;h=n.pop();)if(!h.disabled&&z.test(h.media)){try{f=h.imports,g=f.length}catch(o){g=0}for(l=0;g>l;l++)n.push(f[l]);try{j.push(h.cssText)}catch(o){}}j=m(j.reverse().join("")),e=k(a),d=c(a,j)}),i.attachEvent("onafterprint",function(){n(e),clearTimeout(g._removeSheetTimer),g._removeSheetTimer=setTimeout(b,500)}),a.printShived=!0,a)}var p,q,r="3.7.3",s=a.html5||{},t=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,u=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,v="_html5shiv",w=0,x={};!function(){try{var a=b.createElement("a");a.innerHTML="",p="hidden"in a,q=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){p=!0,q=!0}}();var y={elements:s.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:r,shivCSS:s.shivCSS!==!1,supportsUnknownElements:q,shivMethods:s.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=y,j(b);var z=/^$|\b(?:all|print)\b/,A="html5shiv",B=!q&&function(){var c=b.documentElement;return!("undefined"==typeof b.namespaces||"undefined"==typeof b.parentWindow||"undefined"==typeof c.applyElement||"undefined"==typeof c.removeNode||"undefined"==typeof a.attachEvent)}();y.type+=" print",y.shivPrint=o,o(b),"object"==typeof module&&module.exports&&(module.exports=y)}("undefined"!=typeof window?window:this,document); -------------------------------------------------------------------------------- /docs/source/_build/html/_static/js/html5shiv.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @preserve HTML5 Shiv 3.7.3 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed 3 | */ 4 | !function(a,b){function c(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=t.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=t.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),t.elements=c+" "+a,j(b)}function f(a){var b=s[a[q]];return b||(b={},r++,a[q]=r,s[r]=b),b}function g(a,c,d){if(c||(c=b),l)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():p.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||o.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),l)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return t.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(t,b.frag)}function j(a){a||(a=b);var d=f(a);return!t.shivCSS||k||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),l||i(a,d),a}var k,l,m="3.7.3-pre",n=a.html5||{},o=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,p=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,q="_html5shiv",r=0,s={};!function(){try{var a=b.createElement("a");a.innerHTML="",k="hidden"in a,l=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){k=!0,l=!0}}();var t={elements:n.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:n.shivCSS!==!1,supportsUnknownElements:l,shivMethods:n.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=t,j(b),"object"==typeof module&&module.exports&&(module.exports=t)}("undefined"!=typeof window?window:this,document); -------------------------------------------------------------------------------- /docs/source/_build/html/_static/js/theme.js: -------------------------------------------------------------------------------- 1 | !function(n){var e={};function t(i){if(e[i])return e[i].exports;var o=e[i]={i:i,l:!1,exports:{}};return n[i].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=n,t.c=e,t.d=function(n,e,i){t.o(n,e)||Object.defineProperty(n,e,{enumerable:!0,get:i})},t.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},t.t=function(n,e){if(1&e&&(n=t(n)),8&e)return n;if(4&e&&"object"==typeof n&&n&&n.__esModule)return n;var i=Object.create(null);if(t.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:n}),2&e&&"string"!=typeof n)for(var o in n)t.d(i,o,function(e){return n[e]}.bind(null,o));return i},t.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return t.d(e,"a",e),e},t.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},t.p="",t(t.s=0)}([function(n,e,t){t(1),n.exports=t(3)},function(n,e,t){(function(){var e="undefined"!=typeof window?window.jQuery:t(2);n.exports.ThemeNav={navBar:null,win:null,winScroll:!1,winResize:!1,linkScroll:!1,winPosition:0,winHeight:null,docHeight:null,isRunning:!1,enable:function(n){var t=this;void 0===n&&(n=!0),t.isRunning||(t.isRunning=!0,e((function(e){t.init(e),t.reset(),t.win.on("hashchange",t.reset),n&&t.win.on("scroll",(function(){t.linkScroll||t.winScroll||(t.winScroll=!0,requestAnimationFrame((function(){t.onScroll()})))})),t.win.on("resize",(function(){t.winResize||(t.winResize=!0,requestAnimationFrame((function(){t.onResize()})))})),t.onResize()})))},enableSticky:function(){this.enable(!0)},init:function(n){n(document);var e=this;this.navBar=n("div.wy-side-scroll:first"),this.win=n(window),n(document).on("click","[data-toggle='wy-nav-top']",(function(){n("[data-toggle='wy-nav-shift']").toggleClass("shift"),n("[data-toggle='rst-versions']").toggleClass("shift")})).on("click",".wy-menu-vertical .current ul li a",(function(){var t=n(this);n("[data-toggle='wy-nav-shift']").removeClass("shift"),n("[data-toggle='rst-versions']").toggleClass("shift"),e.toggleCurrent(t),e.hashChange()})).on("click","[data-toggle='rst-current-version']",(function(){n("[data-toggle='rst-versions']").toggleClass("shift-up")})),n("table.docutils:not(.field-list,.footnote,.citation)").wrap("
"),n("table.docutils.footnote").wrap("
"),n("table.docutils.citation").wrap("
"),n(".wy-menu-vertical ul").not(".simple").siblings("a").each((function(){var t=n(this);expand=n(''),expand.on("click",(function(n){return e.toggleCurrent(t),n.stopPropagation(),!1})),t.prepend(expand)}))},reset:function(){var n=encodeURI(window.location.hash)||"#";try{var e=$(".wy-menu-vertical"),t=e.find('[href="'+n+'"]');if(0===t.length){var i=$('.document [id="'+n.substring(1)+'"]').closest("div.section");0===(t=e.find('[href="#'+i.attr("id")+'"]')).length&&(t=e.find('[href="#"]'))}if(t.length>0){$(".wy-menu-vertical .current").removeClass("current").attr("aria-expanded","false"),t.addClass("current").attr("aria-expanded","true"),t.closest("li.toctree-l1").parent().addClass("current").attr("aria-expanded","true");for(let n=1;n<=10;n++)t.closest("li.toctree-l"+n).addClass("current").attr("aria-expanded","true");t[0].scrollIntoView()}}catch(n){console.log("Error expanding nav for anchor",n)}},onScroll:function(){this.winScroll=!1;var n=this.win.scrollTop(),e=n+this.winHeight,t=this.navBar.scrollTop()+(n-this.winPosition);n<0||e>this.docHeight||(this.navBar.scrollTop(t),this.winPosition=n)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",(function(){this.linkScroll=!1}))},toggleCurrent:function(n){var e=n.closest("li");e.siblings("li.current").removeClass("current").attr("aria-expanded","false"),e.siblings().find("li.current").removeClass("current").attr("aria-expanded","false");var t=e.find("> ul li");t.length&&(t.removeClass("current").attr("aria-expanded","false"),e.toggleClass("current").attr("aria-expanded",(function(n,e){return"true"==e?"false":"true"})))}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:n.exports.ThemeNav,StickyNav:n.exports.ThemeNav}),function(){for(var n=0,e=["ms","moz","webkit","o"],t=0;t0 63 | var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 64 | var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 65 | var s_v = "^(" + C + ")?" + v; // vowel in stem 66 | 67 | this.stemWord = function (w) { 68 | var stem; 69 | var suffix; 70 | var firstch; 71 | var origword = w; 72 | 73 | if (w.length < 3) 74 | return w; 75 | 76 | var re; 77 | var re2; 78 | var re3; 79 | var re4; 80 | 81 | firstch = w.substr(0,1); 82 | if (firstch == "y") 83 | w = firstch.toUpperCase() + w.substr(1); 84 | 85 | // Step 1a 86 | re = /^(.+?)(ss|i)es$/; 87 | re2 = /^(.+?)([^s])s$/; 88 | 89 | if (re.test(w)) 90 | w = w.replace(re,"$1$2"); 91 | else if (re2.test(w)) 92 | w = w.replace(re2,"$1$2"); 93 | 94 | // Step 1b 95 | re = /^(.+?)eed$/; 96 | re2 = /^(.+?)(ed|ing)$/; 97 | if (re.test(w)) { 98 | var fp = re.exec(w); 99 | re = new RegExp(mgr0); 100 | if (re.test(fp[1])) { 101 | re = /.$/; 102 | w = w.replace(re,""); 103 | } 104 | } 105 | else if (re2.test(w)) { 106 | var fp = re2.exec(w); 107 | stem = fp[1]; 108 | re2 = new RegExp(s_v); 109 | if (re2.test(stem)) { 110 | w = stem; 111 | re2 = /(at|bl|iz)$/; 112 | re3 = new RegExp("([^aeiouylsz])\\1$"); 113 | re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); 114 | if (re2.test(w)) 115 | w = w + "e"; 116 | else if (re3.test(w)) { 117 | re = /.$/; 118 | w = w.replace(re,""); 119 | } 120 | else if (re4.test(w)) 121 | w = w + "e"; 122 | } 123 | } 124 | 125 | // Step 1c 126 | re = /^(.+?)y$/; 127 | if (re.test(w)) { 128 | var fp = re.exec(w); 129 | stem = fp[1]; 130 | re = new RegExp(s_v); 131 | if (re.test(stem)) 132 | w = stem + "i"; 133 | } 134 | 135 | // Step 2 136 | re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; 137 | if (re.test(w)) { 138 | var fp = re.exec(w); 139 | stem = fp[1]; 140 | suffix = fp[2]; 141 | re = new RegExp(mgr0); 142 | if (re.test(stem)) 143 | w = stem + step2list[suffix]; 144 | } 145 | 146 | // Step 3 147 | re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; 148 | if (re.test(w)) { 149 | var fp = re.exec(w); 150 | stem = fp[1]; 151 | suffix = fp[2]; 152 | re = new RegExp(mgr0); 153 | if (re.test(stem)) 154 | w = stem + step3list[suffix]; 155 | } 156 | 157 | // Step 4 158 | re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; 159 | re2 = /^(.+?)(s|t)(ion)$/; 160 | if (re.test(w)) { 161 | var fp = re.exec(w); 162 | stem = fp[1]; 163 | re = new RegExp(mgr1); 164 | if (re.test(stem)) 165 | w = stem; 166 | } 167 | else if (re2.test(w)) { 168 | var fp = re2.exec(w); 169 | stem = fp[1] + fp[2]; 170 | re2 = new RegExp(mgr1); 171 | if (re2.test(stem)) 172 | w = stem; 173 | } 174 | 175 | // Step 5 176 | re = /^(.+?)e$/; 177 | if (re.test(w)) { 178 | var fp = re.exec(w); 179 | stem = fp[1]; 180 | re = new RegExp(mgr1); 181 | re2 = new RegExp(meq1); 182 | re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); 183 | if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) 184 | w = stem; 185 | } 186 | re = /ll$/; 187 | re2 = new RegExp(mgr1); 188 | if (re.test(w) && re2.test(w)) { 189 | re = /.$/; 190 | w = w.replace(re,""); 191 | } 192 | 193 | // and turn initial Y back to y 194 | if (firstch == "y") 195 | w = firstch.toLowerCase() + w.substr(1); 196 | return w; 197 | } 198 | } 199 | 200 | 201 | 202 | 203 | var splitChars = (function() { 204 | var result = {}; 205 | var singles = [96, 180, 187, 191, 215, 247, 749, 885, 903, 907, 909, 930, 1014, 1648, 206 | 1748, 1809, 2416, 2473, 2481, 2526, 2601, 2609, 2612, 2615, 2653, 2702, 207 | 2706, 2729, 2737, 2740, 2857, 2865, 2868, 2910, 2928, 2948, 2961, 2971, 208 | 2973, 3085, 3089, 3113, 3124, 3213, 3217, 3241, 3252, 3295, 3341, 3345, 209 | 3369, 3506, 3516, 3633, 3715, 3721, 3736, 3744, 3748, 3750, 3756, 3761, 210 | 3781, 3912, 4239, 4347, 4681, 4695, 4697, 4745, 4785, 4799, 4801, 4823, 211 | 4881, 5760, 5901, 5997, 6313, 7405, 8024, 8026, 8028, 8030, 8117, 8125, 212 | 8133, 8181, 8468, 8485, 8487, 8489, 8494, 8527, 11311, 11359, 11687, 11695, 213 | 11703, 11711, 11719, 11727, 11735, 12448, 12539, 43010, 43014, 43019, 43587, 214 | 43696, 43713, 64286, 64297, 64311, 64317, 64319, 64322, 64325, 65141]; 215 | var i, j, start, end; 216 | for (i = 0; i < singles.length; i++) { 217 | result[singles[i]] = true; 218 | } 219 | var ranges = [[0, 47], [58, 64], [91, 94], [123, 169], [171, 177], [182, 184], [706, 709], 220 | [722, 735], [741, 747], [751, 879], [888, 889], [894, 901], [1154, 1161], 221 | [1318, 1328], [1367, 1368], [1370, 1376], [1416, 1487], [1515, 1519], [1523, 1568], 222 | [1611, 1631], [1642, 1645], [1750, 1764], [1767, 1773], [1789, 1790], [1792, 1807], 223 | [1840, 1868], [1958, 1968], [1970, 1983], [2027, 2035], [2038, 2041], [2043, 2047], 224 | [2070, 2073], [2075, 2083], [2085, 2087], [2089, 2307], [2362, 2364], [2366, 2383], 225 | [2385, 2391], [2402, 2405], [2419, 2424], [2432, 2436], [2445, 2446], [2449, 2450], 226 | [2483, 2485], [2490, 2492], [2494, 2509], [2511, 2523], [2530, 2533], [2546, 2547], 227 | [2554, 2564], [2571, 2574], [2577, 2578], [2618, 2648], [2655, 2661], [2672, 2673], 228 | [2677, 2692], [2746, 2748], [2750, 2767], [2769, 2783], [2786, 2789], [2800, 2820], 229 | [2829, 2830], [2833, 2834], [2874, 2876], [2878, 2907], [2914, 2917], [2930, 2946], 230 | [2955, 2957], [2966, 2968], [2976, 2978], [2981, 2983], [2987, 2989], [3002, 3023], 231 | [3025, 3045], [3059, 3076], [3130, 3132], [3134, 3159], [3162, 3167], [3170, 3173], 232 | [3184, 3191], [3199, 3204], [3258, 3260], [3262, 3293], [3298, 3301], [3312, 3332], 233 | [3386, 3388], [3390, 3423], [3426, 3429], [3446, 3449], [3456, 3460], [3479, 3481], 234 | [3518, 3519], [3527, 3584], [3636, 3647], [3655, 3663], [3674, 3712], [3717, 3718], 235 | [3723, 3724], [3726, 3731], [3752, 3753], [3764, 3772], [3774, 3775], [3783, 3791], 236 | [3802, 3803], [3806, 3839], [3841, 3871], [3892, 3903], [3949, 3975], [3980, 4095], 237 | [4139, 4158], [4170, 4175], [4182, 4185], [4190, 4192], [4194, 4196], [4199, 4205], 238 | [4209, 4212], [4226, 4237], [4250, 4255], [4294, 4303], [4349, 4351], [4686, 4687], 239 | [4702, 4703], [4750, 4751], [4790, 4791], [4806, 4807], [4886, 4887], [4955, 4968], 240 | [4989, 4991], [5008, 5023], [5109, 5120], [5741, 5742], [5787, 5791], [5867, 5869], 241 | [5873, 5887], [5906, 5919], [5938, 5951], [5970, 5983], [6001, 6015], [6068, 6102], 242 | [6104, 6107], [6109, 6111], [6122, 6127], [6138, 6159], [6170, 6175], [6264, 6271], 243 | [6315, 6319], [6390, 6399], [6429, 6469], [6510, 6511], [6517, 6527], [6572, 6592], 244 | [6600, 6607], [6619, 6655], [6679, 6687], [6741, 6783], [6794, 6799], [6810, 6822], 245 | [6824, 6916], [6964, 6980], [6988, 6991], [7002, 7042], [7073, 7085], [7098, 7167], 246 | [7204, 7231], [7242, 7244], [7294, 7400], [7410, 7423], [7616, 7679], [7958, 7959], 247 | [7966, 7967], [8006, 8007], [8014, 8015], [8062, 8063], [8127, 8129], [8141, 8143], 248 | [8148, 8149], [8156, 8159], [8173, 8177], [8189, 8303], [8306, 8307], [8314, 8318], 249 | [8330, 8335], [8341, 8449], [8451, 8454], [8456, 8457], [8470, 8472], [8478, 8483], 250 | [8506, 8507], [8512, 8516], [8522, 8525], [8586, 9311], [9372, 9449], [9472, 10101], 251 | [10132, 11263], [11493, 11498], [11503, 11516], [11518, 11519], [11558, 11567], 252 | [11622, 11630], [11632, 11647], [11671, 11679], [11743, 11822], [11824, 12292], 253 | [12296, 12320], [12330, 12336], [12342, 12343], [12349, 12352], [12439, 12444], 254 | [12544, 12548], [12590, 12592], [12687, 12689], [12694, 12703], [12728, 12783], 255 | [12800, 12831], [12842, 12880], [12896, 12927], [12938, 12976], [12992, 13311], 256 | [19894, 19967], [40908, 40959], [42125, 42191], [42238, 42239], [42509, 42511], 257 | [42540, 42559], [42592, 42593], [42607, 42622], [42648, 42655], [42736, 42774], 258 | [42784, 42785], [42889, 42890], [42893, 43002], [43043, 43055], [43062, 43071], 259 | [43124, 43137], [43188, 43215], [43226, 43249], [43256, 43258], [43260, 43263], 260 | [43302, 43311], [43335, 43359], [43389, 43395], [43443, 43470], [43482, 43519], 261 | [43561, 43583], [43596, 43599], [43610, 43615], [43639, 43641], [43643, 43647], 262 | [43698, 43700], [43703, 43704], [43710, 43711], [43715, 43738], [43742, 43967], 263 | [44003, 44015], [44026, 44031], [55204, 55215], [55239, 55242], [55292, 55295], 264 | [57344, 63743], [64046, 64047], [64110, 64111], [64218, 64255], [64263, 64274], 265 | [64280, 64284], [64434, 64466], [64830, 64847], [64912, 64913], [64968, 65007], 266 | [65020, 65135], [65277, 65295], [65306, 65312], [65339, 65344], [65371, 65381], 267 | [65471, 65473], [65480, 65481], [65488, 65489], [65496, 65497]]; 268 | for (i = 0; i < ranges.length; i++) { 269 | start = ranges[i][0]; 270 | end = ranges[i][1]; 271 | for (j = start; j <= end; j++) { 272 | result[j] = true; 273 | } 274 | } 275 | return result; 276 | })(); 277 | 278 | function splitQuery(query) { 279 | var result = []; 280 | var start = -1; 281 | for (var i = 0; i < query.length; i++) { 282 | if (splitChars[query.charCodeAt(i)]) { 283 | if (start !== -1) { 284 | result.push(query.slice(start, i)); 285 | start = -1; 286 | } 287 | } else if (start === -1) { 288 | start = i; 289 | } 290 | } 291 | if (start !== -1) { 292 | result.push(query.slice(start)); 293 | } 294 | return result; 295 | } 296 | 297 | 298 | -------------------------------------------------------------------------------- /docs/source/_build/html/_static/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/html/_static/minus.png -------------------------------------------------------------------------------- /docs/source/_build/html/_static/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/html/_static/plus.png -------------------------------------------------------------------------------- /docs/source/_build/html/_static/pygments.css: -------------------------------------------------------------------------------- 1 | pre { line-height: 125%; } 2 | td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } 3 | span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } 4 | td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } 5 | span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } 6 | .highlight .hll { background-color: #ffffcc } 7 | .highlight { background: #eeffcc; } 8 | .highlight .c { color: #408090; font-style: italic } /* Comment */ 9 | .highlight .err { border: 1px solid #FF0000 } /* Error */ 10 | .highlight .k { color: #007020; font-weight: bold } /* Keyword */ 11 | .highlight .o { color: #666666 } /* Operator */ 12 | .highlight .ch { color: #408090; font-style: italic } /* Comment.Hashbang */ 13 | .highlight .cm { color: #408090; font-style: italic } /* Comment.Multiline */ 14 | .highlight .cp { color: #007020 } /* Comment.Preproc */ 15 | .highlight .cpf { color: #408090; font-style: italic } /* Comment.PreprocFile */ 16 | .highlight .c1 { color: #408090; font-style: italic } /* Comment.Single */ 17 | .highlight .cs { color: #408090; background-color: #fff0f0 } /* Comment.Special */ 18 | .highlight .gd { color: #A00000 } /* Generic.Deleted */ 19 | .highlight .ge { font-style: italic } /* Generic.Emph */ 20 | .highlight .gr { color: #FF0000 } /* Generic.Error */ 21 | .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 22 | .highlight .gi { color: #00A000 } /* Generic.Inserted */ 23 | .highlight .go { color: #333333 } /* Generic.Output */ 24 | .highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ 25 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 26 | .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 27 | .highlight .gt { color: #0044DD } /* Generic.Traceback */ 28 | .highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ 29 | .highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ 30 | .highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ 31 | .highlight .kp { color: #007020 } /* Keyword.Pseudo */ 32 | .highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ 33 | .highlight .kt { color: #902000 } /* Keyword.Type */ 34 | .highlight .m { color: #208050 } /* Literal.Number */ 35 | .highlight .s { color: #4070a0 } /* Literal.String */ 36 | .highlight .na { color: #4070a0 } /* Name.Attribute */ 37 | .highlight .nb { color: #007020 } /* Name.Builtin */ 38 | .highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ 39 | .highlight .no { color: #60add5 } /* Name.Constant */ 40 | .highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */ 41 | .highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */ 42 | .highlight .ne { color: #007020 } /* Name.Exception */ 43 | .highlight .nf { color: #06287e } /* Name.Function */ 44 | .highlight .nl { color: #002070; font-weight: bold } /* Name.Label */ 45 | .highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ 46 | .highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */ 47 | .highlight .nv { color: #bb60d5 } /* Name.Variable */ 48 | .highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */ 49 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 50 | .highlight .mb { color: #208050 } /* Literal.Number.Bin */ 51 | .highlight .mf { color: #208050 } /* Literal.Number.Float */ 52 | .highlight .mh { color: #208050 } /* Literal.Number.Hex */ 53 | .highlight .mi { color: #208050 } /* Literal.Number.Integer */ 54 | .highlight .mo { color: #208050 } /* Literal.Number.Oct */ 55 | .highlight .sa { color: #4070a0 } /* Literal.String.Affix */ 56 | .highlight .sb { color: #4070a0 } /* Literal.String.Backtick */ 57 | .highlight .sc { color: #4070a0 } /* Literal.String.Char */ 58 | .highlight .dl { color: #4070a0 } /* Literal.String.Delimiter */ 59 | .highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ 60 | .highlight .s2 { color: #4070a0 } /* Literal.String.Double */ 61 | .highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ 62 | .highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */ 63 | .highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ 64 | .highlight .sx { color: #c65d09 } /* Literal.String.Other */ 65 | .highlight .sr { color: #235388 } /* Literal.String.Regex */ 66 | .highlight .s1 { color: #4070a0 } /* Literal.String.Single */ 67 | .highlight .ss { color: #517918 } /* Literal.String.Symbol */ 68 | .highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ 69 | .highlight .fm { color: #06287e } /* Name.Function.Magic */ 70 | .highlight .vc { color: #bb60d5 } /* Name.Variable.Class */ 71 | .highlight .vg { color: #bb60d5 } /* Name.Variable.Global */ 72 | .highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */ 73 | .highlight .vm { color: #bb60d5 } /* Name.Variable.Magic */ 74 | .highlight .il { color: #208050 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /docs/source/_build/html/contributing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Contributing — pyfuncol 1.3.1 documentation 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 61 | 62 |
66 | 67 |
68 |
69 |
70 | 77 |
78 |
79 |
80 |
81 | 82 |
83 |

Contributing

84 |
85 |

Setup

86 |

Fork the repo, then install all development requirements with:

87 |
pip install -r development.txt
 88 | 
89 |
90 |

When your changes are ready, submit a pull request!

91 |
92 |
93 |

Style

94 |

For formatting and code style, we use black. Docstrings should follow the Google Python Style Guide.

95 |
96 |
97 |

Tests

98 |

To run the tests, execute pytest at the root of the project.

99 |

To run the tests with coverage enabled, execute:

100 |
pytest --cov-config=.coveragerc --cov=pyfuncol --cov-report=xml
101 | 
102 |
103 |
104 |
105 |

Docs

106 |

The docs are hosted on Read the Docs. Source files are in docs/source/.

107 |

To build them locally, run in docs/:

108 |
make html
109 | 
110 |
111 |

The HTML files will be stored in docs/build/.

112 |
113 |
114 |

Project structure

115 |
┌── docs - Contains the docs source code
116 | ├── pyfuncol - Contains all the library source code
117 |     ├── tests - Contains tests for all the modules
118 |     ├── __init__.py - Contains the function calls that extend built-in types
119 |     ├── dict.py - Contains extension functions for dictionaries
120 |     ├── list.py - Contains extension functions for lists
121 |     └── ...
122 | └── ...
123 | 
124 |
125 |
126 |
127 |

Release

128 |

To publish a new release on PyPI:

129 |
    130 |
  1. Update the version in setup.py

  2. 131 |
  3. Update the version (release field) in docs/source/conf.py

  4. 132 |
  5. Create a new release on GitHub. The newly created tag and the release title should match the version in setup.py and docs/source/conf.py with ‘v’ prepended. An example: for version 1.1.1, the tag and release title should be v1.1.1.

  6. 133 |
134 |

The GitHub release creation will trigger the deploy workflow that builds and uploads the project to PyPI.

135 |
136 |
137 |

Code of Conduct

138 |

Our Code of Conduct is here. By contributing to pyfuncol, you implicitly accept it.

139 |
140 |
141 | 142 | 143 |
144 |
145 |
149 | 150 |
151 | 152 |
153 |

© Copyright 2021, Andrea Veneziano.

154 |
155 | 156 | Built with Sphinx using a 157 | theme 158 | provided by Read the Docs. 159 | 160 | 161 |
162 |
163 |
164 |
165 |
166 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /docs/source/_build/html/get_started.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Get Started — pyfuncol 1.3.1 documentation 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 52 | 53 |
57 | 58 |
59 |
60 |
61 | 68 |
69 |
70 |
71 |
72 | 73 |
74 |

Get Started

75 |

pyfuncol is supported only on Python 3.6+ and CPython (+ forbiddenfruit) is required for extending builtins. Albeit untested, it might still work on older Python versions.

76 |

To install it, run:

77 |
pip install pyfuncol
 78 | 
79 |
80 |
81 | 82 | 83 |
84 |
85 |
89 | 90 |
91 | 92 |
93 |

© Copyright 2021, Andrea Veneziano.

94 |
95 | 96 | Built with Sphinx using a 97 | theme 98 | provided by Read the Docs. 99 | 100 | 101 |
102 |
103 |
104 |
105 |
106 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /docs/source/_build/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Welcome to pyfuncol’s documentation! — pyfuncol 1.3.1 documentation 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 51 | 52 |
56 | 57 |
58 |
59 |
60 |
    61 |
  • »
  • 62 |
  • Welcome to pyfuncol’s documentation!
  • 63 |
  • 64 | Edit on GitHub 65 |
  • 66 |
67 |
68 |
69 |
70 |
71 | 72 |
73 |

Welcome to pyfuncol’s documentation!

74 |
75 |

Contents

76 | 98 |
99 |
100 |
101 |

Indices and tables

102 | 107 |
108 | 109 | 110 |
111 |
112 |
115 | 116 |
117 | 118 |
119 |

© Copyright 2021, Andrea Veneziano.

120 |
121 | 122 | Built with Sphinx using a 123 | theme 124 | provided by Read the Docs. 125 | 126 | 127 |
128 |
129 |
130 |
131 |
132 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /docs/source/_build/html/introduction.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Introduction — pyfuncol 1.3.1 documentation 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 52 | 53 |
57 | 58 |
59 |
60 |
61 | 68 |
69 |
70 |
71 |
72 | 73 |
74 |

Introduction

75 |

pyfuncol is a Python functional collections library. It extends collections built-in types with useful methods to write functional Python code. It uses Forbidden Fruit under the hood.

76 |

pyfuncol provides:

77 |
    78 |
  • Standard “eager” methods, such as map, flat_map, group_by, etc.

  • 79 |
  • Parallel methods, such as par_map, par_flat_map, etc.

  • 80 |
  • Pure methods that leverage memoization to improve performance, such as pure_map, pure_flat_map, etc.

  • 81 |
  • Lazy methods that return iterators and never materialize results, such as lazy_map, lazy_flat_map, etc.

  • 82 |
83 |

pyfuncol can also be used without forbiddenfruit.

84 |
85 | 86 | 87 |
88 |
89 |
93 | 94 |
95 | 96 |
97 |

© Copyright 2021, Andrea Veneziano.

98 |
99 | 100 | Built with Sphinx using a 101 | theme 102 | provided by Read the Docs. 103 | 104 | 105 |
106 |
107 |
108 |
109 |
110 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /docs/source/_build/html/modules.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | API reference — pyfuncol 1.3.1 documentation 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 55 | 56 |
60 | 61 |
62 |
63 |
64 | 71 |
72 |
73 |
74 |
75 | 76 |
77 |

API reference

78 | 90 |
91 | 92 | 93 |
94 |
95 |
99 | 100 |
101 | 102 |
103 |

© Copyright 2021, Andrea Veneziano.

104 |
105 | 106 | Built with Sphinx using a 107 | theme 108 | provided by Read the Docs. 109 | 110 | 111 |
112 |
113 |
114 |
115 |
116 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /docs/source/_build/html/objects.inv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/docs/source/_build/html/objects.inv -------------------------------------------------------------------------------- /docs/source/_build/html/py-modindex.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Python Module Index — pyfuncol 1.3.1 documentation 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 52 | 53 |
57 | 58 |
59 |
60 |
61 |
    62 |
  • »
  • 63 |
  • Python Module Index
  • 64 |
  • 65 |
  • 66 |
67 |
68 |
69 |
70 |
71 | 72 | 73 |

Python Module Index

74 | 75 |
76 | p 77 |
78 | 79 | 80 | 81 | 83 | 84 | 86 | 89 | 90 | 91 | 94 | 95 | 96 | 99 | 100 | 101 | 104 |
 
82 | p
87 | pyfuncol 88 |
    92 | pyfuncol.dict 93 |
    97 | pyfuncol.list 98 |
    102 | pyfuncol.set 103 |
105 | 106 | 107 |
108 |
109 |
110 | 111 |
112 | 113 |
114 |

© Copyright 2021, Andrea Veneziano.

115 |
116 | 117 | Built with Sphinx using a 118 | theme 119 | provided by Read the Docs. 120 | 121 | 122 |
123 |
124 |
125 |
126 |
127 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /docs/source/_build/html/search.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Search — pyfuncol 1.3.1 documentation 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 52 | 53 |
57 | 58 |
59 |
60 |
61 |
    62 |
  • »
  • 63 |
  • Search
  • 64 |
  • 65 |
  • 66 |
67 |
68 |
69 |
70 |
71 | 72 | 79 | 80 | 81 |
82 | 83 |
84 | 85 |
86 |
87 |
88 | 89 |
90 | 91 |
92 |

© Copyright 2021, Andrea Veneziano.

93 |
94 | 95 | Built with Sphinx using a 96 | theme 97 | provided by Read the Docs. 98 | 99 | 100 |
101 |
102 |
103 |
104 |
105 | 110 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /docs/source/_build/html/searchindex.js: -------------------------------------------------------------------------------- 1 | Search.setIndex({docnames:["contributing","examples","get_started","index","introduction","modules","pyfuncol"],envversion:{"sphinx.domains.c":2,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":4,"sphinx.domains.index":1,"sphinx.domains.javascript":2,"sphinx.domains.math":2,"sphinx.domains.python":3,"sphinx.domains.rst":2,"sphinx.domains.std":2,"sphinx.ext.viewcode":1,sphinx:56},filenames:["contributing.md","examples.md","get_started.md","index.rst","introduction.md","modules.rst","pyfuncol.rst"],objects:{"":[[6,0,0,"-","pyfuncol"]],"pyfuncol.dict":[[6,1,1,"","contains"],[6,1,1,"","count"],[6,1,1,"","filter"],[6,1,1,"","filter_not"],[6,1,1,"","find"],[6,1,1,"","flat_map"],[6,1,1,"","fold_left"],[6,1,1,"","fold_right"],[6,1,1,"","forall"],[6,1,1,"","foreach"],[6,1,1,"","is_empty"],[6,1,1,"","lazy_filter"],[6,1,1,"","lazy_filter_not"],[6,1,1,"","lazy_flat_map"],[6,1,1,"","lazy_map"],[6,1,1,"","map"],[6,1,1,"","par_filter"],[6,1,1,"","par_filter_not"],[6,1,1,"","par_flat_map"],[6,1,1,"","par_map"],[6,1,1,"","pure_filter"],[6,1,1,"","pure_filter_not"],[6,1,1,"","pure_flat_map"],[6,1,1,"","pure_map"],[6,1,1,"","size"],[6,1,1,"","to_iterator"],[6,1,1,"","to_list"]],"pyfuncol.list":[[6,1,1,"","contains"],[6,1,1,"","distinct"],[6,1,1,"","filter"],[6,1,1,"","filter_not"],[6,1,1,"","find"],[6,1,1,"","flat_map"],[6,1,1,"","flatten"],[6,1,1,"","fold_left"],[6,1,1,"","fold_right"],[6,1,1,"","forall"],[6,1,1,"","foreach"],[6,1,1,"","group_by"],[6,1,1,"","head"],[6,1,1,"","index_of"],[6,1,1,"","is_empty"],[6,1,1,"","lazy_distinct"],[6,1,1,"","lazy_filter"],[6,1,1,"","lazy_filter_not"],[6,1,1,"","lazy_flat_map"],[6,1,1,"","lazy_flatten"],[6,1,1,"","lazy_map"],[6,1,1,"","lazy_take"],[6,1,1,"","length"],[6,1,1,"","map"],[6,1,1,"","par_filter"],[6,1,1,"","par_filter_not"],[6,1,1,"","par_flat_map"],[6,1,1,"","par_map"],[6,1,1,"","pure_filter"],[6,1,1,"","pure_filter_not"],[6,1,1,"","pure_flat_map"],[6,1,1,"","pure_map"],[6,1,1,"","size"],[6,1,1,"","tail"],[6,1,1,"","take"],[6,1,1,"","to_iterator"]],"pyfuncol.set":[[6,1,1,"","contains"],[6,1,1,"","filter"],[6,1,1,"","filter_not"],[6,1,1,"","find"],[6,1,1,"","flat_map"],[6,1,1,"","fold_left"],[6,1,1,"","fold_right"],[6,1,1,"","forall"],[6,1,1,"","foreach"],[6,1,1,"","group_by"],[6,1,1,"","is_empty"],[6,1,1,"","lazy_filter"],[6,1,1,"","lazy_filter_not"],[6,1,1,"","lazy_flat_map"],[6,1,1,"","lazy_map"],[6,1,1,"","length"],[6,1,1,"","map"],[6,1,1,"","par_filter"],[6,1,1,"","par_filter_not"],[6,1,1,"","par_flat_map"],[6,1,1,"","par_map"],[6,1,1,"","pure_filter"],[6,1,1,"","pure_filter_not"],[6,1,1,"","pure_flat_map"],[6,1,1,"","pure_map"],[6,1,1,"","size"],[6,1,1,"","to_iterator"]],pyfuncol:[[6,0,0,"-","dict"],[6,0,0,"-","list"],[6,0,0,"-","set"]]},objnames:{"0":["py","module","Python module"],"1":["py","function","Python function"]},objtypes:{"0":"py:module","1":"py:function"},terms:{"0":1,"1":[0,1,6],"10":1,"2":1,"3":[1,2],"4":1,"6":[1,2],"8":1,"9":1,"default":1,"do":[1,6],"function":[0,1,4,6],"import":1,"int":6,"new":[0,6],"return":[1,4,6],"true":6,A:6,By:0,For:0,If:[1,6],It:4,The:[0,6],These:1,To:[0,1,2],__init__:0,abc:1,acc:1,accept:0,accord:6,albeit:2,alia:6,all:[0,1,6],also:[1,4],alwai:6,an:[0,6],ani:6,api:3,appli:6,ar:[0,1,6],arg:1,associ:6,b:[1,6],between:6,binari:6,bind:6,black:0,bool:6,build:[0,6],built:[0,4],builtin:[1,2],c:[1,6],call:[0,1,6],callabl:6,can:4,chang:0,code:[3,4],collect:[4,6],commut:6,comput:6,conduct:3,conf:0,config:0,consecut:6,consist:6,constructor:1,contain:[0,6],content:5,contribut:3,convert:6,correspond:6,count:6,cov:0,coverag:0,coveragerc:0,cpython:[1,2],creat:0,creation:0,d:6,def:1,deploi:0,develop:0,dict:[0,5],dictionari:[0,6],differ:6,direct:1,discrimin:6,distinct:6,doc:3,docstr:0,doe:[1,6],duplic:6,e:[1,6],each:6,eager:4,effect:6,elem:6,element:6,els:6,empti:6,enabl:0,entri:6,etc:4,exampl:[0,3],execut:0,exist:6,extend:[0,1,2,4],extens:0,f:6,fals:6,field:0,file:0,filter:[1,6],filter_not:[1,6],find:6,first:6,flat_map:[1,4,6],flatten:6,fold_left:[1,6],fold_right:6,follow:0,foral:6,forbidden:4,forbiddenfruit:[2,3,4],foreach:6,fork:0,form:6,format:0,fortun:1,from:[1,6],fruit:4,get:3,github:0,given:6,go:6,googl:0,group:6,group_bi:[1,4,6],guid:0,ha:6,hash:6,hashabl:[1,6],head:6,here:[0,1],hold:6,hood:4,host:0,html:0,i:[1,6],ignor:6,implicitli:0,improv:[1,4,6],index:[3,6],index_of:6,indexerror:6,infinit:6,input:[1,6],insert:6,instal:[0,1,2],intepret:1,introduct:3,is_empti:6,iter:[4,6],its:6,just:1,k:6,kei:6,kv:1,lambda:1,lazi:[1,4,6],lazili:6,lazy_distinct:6,lazy_filt:[1,6],lazy_filter_not:[1,6],lazy_flat_map:[1,4,6],lazy_flatten:6,lazy_map:[1,4,6],lazy_tak:6,lead:6,left:6,len:1,length:6,less:6,leverag:[1,4],librari:[0,4],list:[0,1,5],local:0,look:6,mai:6,make:0,map:[1,4,6],match:0,materi:[1,4],memoiz:[1,4,6],method:[1,4],might:[2,6],modul:[0,3,5],must:6,n:[1,6],need:1,neg:6,never:[1,4],newli:0,none:6,note:[1,6],now:1,number:6,occurr:6,older:2,onli:[1,2,6],op:6,oper:[1,6],option:6,order:6,ordereddict:1,other:1,otherwis:6,our:0,p:6,packag:[3,5],page:3,pair:6,par_filt:[1,6],par_filter_not:[1,6],par_flat_map:[1,4,6],par_map:[1,4,6],parallel:[1,4,6],paramet:6,partit:6,perform:[1,4,6],pfclist:1,pip:[0,2],pleas:1,predic:6,prepend:0,project:3,provid:[1,4],publish:0,pull:0,pure:[1,4,6],pure_filt:[1,6],pure_filter_not:[1,6],pure_flat_map:[1,4,6],pure_map:[1,4,6],py:0,pyfuncol:[0,1,2,4,5],pypi:0,pytest:0,python:[0,1,2,4],r:0,rais:6,read:0,readi:0,refer:3,releas:3,repo:0,report:0,request:0,requir:[0,2],rest:6,result:[1,4,6],right:6,root:0,run:[0,2,6],s:1,same:[1,6],satisfi:6,search:3,see:1,select:6,self:6,sequenc:6,set:[1,5],setup:3,should:0,side:6,size:6,some:[1,6],sourc:[0,6],standard:4,start:[3,6],still:2,store:0,structur:3,style:3,subclass:1,submit:0,submodul:5,support:[1,2],tag:0,tail:6,take:6,termin:6,test:[3,6],than:[1,6],them:0,thi:6,titl:0,to_iter:6,to_list:6,trigger:0,tupl:6,txt:0,type:[0,4,6],u:6,under:4,underli:6,unless:6,untest:2,updat:0,upload:0,us:[0,1,4,6],usag:3,v1:0,v:0,valu:[1,6],version:[0,1,2],want:1,warn:6,we:[0,1],when:0,where:6,whether:6,which:6,whole:6,whose:6,without:[3,4,6],work:[1,2],workflow:0,write:4,x1:6,x:1,x_1:6,x_2:6,x_n:6,xml:0,xn:6,you:[0,1],your:[0,1],z:6},titles:["Contributing","Examples","Get Started","Welcome to pyfuncol\u2019s documentation!","Introduction","API reference","pyfuncol package"],titleterms:{api:5,code:0,conduct:0,content:[3,6],contribut:0,dict:6,doc:0,document:3,exampl:1,forbiddenfruit:1,get:2,indic:3,introduct:4,list:6,modul:6,packag:6,project:0,pyfuncol:[3,6],refer:5,releas:0,s:3,set:6,setup:0,start:2,structur:0,style:0,submodul:6,tabl:3,test:0,usag:1,welcom:3,without:1}}) -------------------------------------------------------------------------------- /docs/source/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 = "pyfuncol" 22 | copyright = "2021, Andrea Veneziano" 23 | author = "Andrea Veneziano" 24 | description = "Functional collections extension functions for Python" 25 | 26 | # The full version, including alpha/beta/rc tags 27 | release = "1.3.1" 28 | 29 | 30 | # -- General configuration --------------------------------------------------- 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = [ 36 | "myst_parser", 37 | "sphinx.ext.autodoc", 38 | "sphinx.ext.napoleon", 39 | "sphinx.ext.autosectionlabel", 40 | "sphinx.ext.viewcode", 41 | ] 42 | 43 | autodoc_typehints = "signature" 44 | 45 | autodoc_default_options = { 46 | "undoc-members": True, 47 | "member-order": "bysource", 48 | "autosummary": True, 49 | } 50 | 51 | # Add any paths that contain templates here, relative to this directory. 52 | templates_path = ["_templates"] 53 | 54 | # List of patterns, relative to source directory, that match files and 55 | # directories to ignore when looking for source files. 56 | # This pattern also affects html_static_path and html_extra_path. 57 | exclude_patterns = [] 58 | 59 | # The name of the Pygments (syntax highlighting) style to use. 60 | pygments_style = "sphinx" 61 | 62 | # -- Options for HTML output ------------------------------------------------- 63 | 64 | # The theme to use for HTML and HTML Help pages. See the documentation for 65 | # a list of builtin themes. 66 | # 67 | html_theme = "sphinx_rtd_theme" 68 | 69 | # Add any paths that contain custom static files (such as style sheets) here, 70 | # relative to this directory. They are copied after the builtin static files, 71 | # so a file named "default.css" will overwrite the builtin "default.css". 72 | html_static_path = ["_static"] 73 | 74 | html_context = { 75 | "display_github": True, 76 | "github_user": "didactic-meme", 77 | "github_repo": "pyfuncol", 78 | "github_version": "main/docs/", 79 | } 80 | -------------------------------------------------------------------------------- /docs/source/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Setup 4 | 5 | Fork the repo, then install all development requirements with: 6 | 7 | ```shell 8 | pip install -r development.txt 9 | ``` 10 | 11 | When your changes are ready, submit a pull request! 12 | 13 | ## Style 14 | 15 | For formatting and code style, we use [black](https://github.com/psf/black). Docstrings should follow the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings). 16 | 17 | ## Tests 18 | 19 | To run the tests, execute `pytest` at the root of the project. 20 | 21 | To run the tests with coverage enabled, execute: 22 | 23 | ```shell 24 | pytest --cov-config=.coveragerc --cov=pyfuncol --cov-report=xml 25 | ``` 26 | 27 | ## Docs 28 | 29 | The docs are hosted on [Read the Docs](https://pyfuncol.readthedocs.io/en/latest/). Source files are in `docs/source/`. 30 | 31 | To build them locally, run in `docs/`: 32 | 33 | ```shell 34 | make html 35 | ``` 36 | 37 | The HTML files will be stored in `docs/build/`. 38 | 39 | ## Project structure 40 | 41 | ``` 42 | ┌── docs - Contains the docs source code 43 | ├── pyfuncol - Contains all the library source code 44 | ├── tests - Contains tests for all the modules 45 | ├── __init__.py - Contains the function calls that extend built-in types 46 | ├── dict.py - Contains extension functions for dictionaries 47 | ├── list.py - Contains extension functions for lists 48 | └── ... 49 | └── ... 50 | ``` 51 | 52 | ## Release 53 | 54 | To publish a new release on [PyPI](https://pypi.org/project/pyfuncol/): 55 | 56 | 1. Update the version in `setup.py` 57 | 2. Update the version (`release` field) in `docs/source/conf.py` 58 | 3. Create a new release on [GitHub](https://github.com/didactic-meme/pyfuncol/releases). The newly created tag and the release title should match the version in `setup.py` and `docs/source/conf.py` with 'v' prepended. An example: for version `1.1.1`, the tag and release title should be `v1.1.1`. 59 | 60 | The GitHub release creation will trigger the deploy workflow that builds and uploads the project to PyPI. 61 | 62 | ## Code of Conduct 63 | 64 | Our Code of Conduct is [here](https://github.com/didactic-meme/pyfuncol/blob/main/CODE_OF_CONDUCT.md). By contributing to pyfuncol, you implicitly accept it. 65 | -------------------------------------------------------------------------------- /docs/source/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | > **Note:** If you are not using forbiddenfruit, the functions will not extend the builtins. Please [see here](#usage-without-forbiddenfruit) for usage without forbiddenfruit 4 | 5 | To use the methods, you just need to import pyfuncol. Some examples: 6 | 7 | ```python 8 | import pyfuncol 9 | 10 | [1, 2, 3, 4].map(lambda x: x * 2).filter(lambda x: x > 4) 11 | # [6, 8] 12 | 13 | [1, 2, 3, 4].fold_left(0, lambda acc, n: acc + n) 14 | # 10 15 | 16 | {1, 2, 3, 4}.map(lambda x: x * 2).filter_not(lambda x: x <= 4) 17 | # {6, 8} 18 | 19 | ["abc", "def", "e"].group_by(lambda s: len(s)) 20 | # {3: ["abc", "def"], 1: ["e"]} 21 | 22 | {"a": 1, "b": 2, "c": 3}.flat_map(lambda kv: {kv[0]: kv[1] ** 2}) 23 | # {"a": 1, "b": 4, "c": 9} 24 | ``` 25 | 26 | pyfuncol provides parallel operations (for now `par_map`, `par_flat_map`, `par_filter` and `par_filter_not`): 27 | 28 | ```python 29 | [1, 2, 3, 4].par_map(lambda x: x * 2).par_filter(lambda x: x > 4) 30 | # [6, 8] 31 | 32 | {1, 2, 3, 4}.par_map(lambda x: x * 2).par_filter_not(lambda x: x <= 4) 33 | # {6, 8} 34 | 35 | {"a": 1, "b": 2, "c": 3}.par_flat_map(lambda kv: {kv[0]: kv[1] ** 2}) 36 | # {"a": 1, "b": 4, "c": 9} 37 | ``` 38 | 39 | pyfuncol provides operations leveraging memoization to improve performance (for now `pure_map`, `pure_flat_map`, `pure_filter` and `pure_filter_not`). These versions work only for **pure** functions (i.e., all calls to the same args return the same value) on hashable inputs: 40 | 41 | ```python 42 | [1, 2, 3, 4].pure_map(lambda x: x * 2).pure_filter(lambda x: x > 4) 43 | # [6, 8] 44 | 45 | {1, 2, 3, 4}.pure_map(lambda x: x * 2).pure_filter_not(lambda x: x <= 4) 46 | # {6, 8} 47 | 48 | {"a": 1, "b": 2, "c": 3}.pure_flat_map(lambda kv: {kv[0]: kv[1] ** 2}) 49 | # {"a": 1, "b": 4, "c": 9} 50 | ``` 51 | 52 | pyfuncol provides lazy operations that never materialize results: 53 | 54 | ```python 55 | list([1, 2, 3, 4].lazy_map(lambda x: x * 2).lazy_filter(lambda x: x > 4)) 56 | # [6, 8] 57 | 58 | list({1, 2, 3, 4}.lazy_map(lambda x: x * 2).lazy_filter_not(lambda x: x <= 4)) 59 | # [6, 8] 60 | 61 | list({"a": 1, "b": 2, "c": 3}.lazy_flat_map(lambda kv: {kv[0]: kv[1] ** 2})) 62 | # [("a", 1), ("b", 4), ("c", 9)] 63 | 64 | set([1, 2, 3, 4].lazy_map(lambda x: x * 2).lazy_filter(lambda x: x > 4)) 65 | # {6, 8} 66 | ``` 67 | 68 | We support all subclasses with default constructors (`OrderedDict`, for example). 69 | 70 | (usage-without-forbiddenfruit)= 71 | ## Usage without forbiddenfruit 72 | 73 | If you are using a Python intepreter other than CPython, forbiddenfruit will not work. 74 | 75 | Fortunately, if forbiddenfruit does not work on your installation or if you do not want to use it, pyfuncol also supports direct function calls without extending builtins. 76 | 77 | ```python 78 | from pyfuncol import list as pfclist 79 | 80 | pfclist.map([1, 2, 3], lambda x: x * 2) 81 | # [2, 4, 6] 82 | ``` 83 | -------------------------------------------------------------------------------- /docs/source/get_started.md: -------------------------------------------------------------------------------- 1 | # Get Started 2 | 3 | pyfuncol is supported only on Python 3.6+ and CPython (+ forbiddenfruit) is required for extending builtins. Albeit untested, it might still work on older Python versions. 4 | 5 | To install it, run: 6 | 7 | ```shell 8 | pip install pyfuncol 9 | ``` 10 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. pyfuncol documentation master file, created by 2 | sphinx-quickstart on Sat Dec 18 23:25:15 2021. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to pyfuncol's documentation! 7 | ==================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents 12 | 13 | introduction 14 | get_started 15 | examples 16 | contributing 17 | modules 18 | 19 | 20 | 21 | Indices and tables 22 | ================== 23 | 24 | * :ref:`genindex` 25 | * :ref:`modindex` 26 | * :ref:`search` 27 | -------------------------------------------------------------------------------- /docs/source/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | [pyfuncol](https://github.com/didactic-meme/pyfuncol) is a Python functional collections library. It _extends_ collections built-in types with useful methods to write functional Python code. It uses [Forbidden Fruit](https://github.com/clarete/forbiddenfruit) under the hood. 4 | 5 | pyfuncol provides: 6 | 7 | - Standard "eager" methods, such as `map`, `flat_map`, `group_by`, etc. 8 | - Parallel methods, such as `par_map`, `par_flat_map`, etc. 9 | - Pure methods that leverage memoization to improve performance, such as `pure_map`, `pure_flat_map`, etc. 10 | - Lazy methods that return iterators and never materialize results, such as `lazy_map`, `lazy_flat_map`, etc. 11 | 12 | pyfuncol can also be [used without forbiddenfruit](usage-without-forbiddenfruit). 13 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | API reference 2 | ============= 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | pyfuncol 8 | -------------------------------------------------------------------------------- /docs/source/pyfuncol.rst: -------------------------------------------------------------------------------- 1 | pyfuncol package 2 | ================ 3 | 4 | Submodules 5 | ---------- 6 | 7 | pyfuncol.dict module 8 | -------------------- 9 | 10 | .. automodule:: pyfuncol.dict 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | pyfuncol.list module 16 | -------------------- 17 | 18 | .. automodule:: pyfuncol.list 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | pyfuncol.set module 24 | -------------------- 25 | 26 | .. automodule:: pyfuncol.set 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | 32 | Module contents 33 | --------------- 34 | 35 | .. automodule:: pyfuncol 36 | :members: 37 | :undoc-members: 38 | :show-inheritance: 39 | -------------------------------------------------------------------------------- /pyfuncol/__init__.py: -------------------------------------------------------------------------------- 1 | from . import extend_builtins -------------------------------------------------------------------------------- /pyfuncol/dict.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Dict, Iterator, Optional, Tuple, TypeVar, List, cast 2 | import functools 3 | import dask 4 | 5 | A = TypeVar("A") 6 | B = TypeVar("B") 7 | C = TypeVar("C") 8 | D = TypeVar("D") 9 | U = TypeVar("U") 10 | 11 | 12 | def contains(self: Dict[A, B], key: A) -> bool: 13 | """ 14 | Tests whether this dict contains a binding for a key. 15 | 16 | Args: 17 | key: The key to find. 18 | 19 | Returns: 20 | True if the dict contains a binding for the key, False otherwise. 21 | """ 22 | return key in self 23 | 24 | 25 | def size(self: Dict[A, B]) -> int: 26 | """ 27 | Computes the size of this dict. 28 | 29 | Returns: 30 | The size of the dict. 31 | """ 32 | return len(self) 33 | 34 | 35 | def filter(self: Dict[A, B], p: Callable[[Tuple[A, B]], bool]) -> Dict[A, B]: 36 | """ 37 | Selects all elements of this dict which satisfy a predicate. 38 | 39 | Args: 40 | p: The predicate to satisfy. 41 | 42 | Returns: 43 | The filtered dict. 44 | """ 45 | return type(self)({k: v for k, v in self.items() if p((k, v))}) 46 | 47 | 48 | def filter_not(self: Dict[A, B], p: Callable[[Tuple[A, B]], bool]) -> Dict[A, B]: 49 | """ 50 | Selects all elements of this dict which do not satisfy a predicate. 51 | 52 | Args: 53 | p: The predicate to not satisfy. 54 | 55 | Returns: 56 | The filtered dict. 57 | """ 58 | return type(self)({k: v for k, v in self.items() if not p((k, v))}) 59 | 60 | 61 | def flat_map(self: Dict[A, B], f: Callable[[Tuple[A, B]], Dict[C, D]]) -> Dict[C, D]: 62 | """ 63 | Builds a new dict by applying a function to all elements of this dict and using the elements of the resulting collections. 64 | 65 | Args: 66 | f: The function to apply to all elements. 67 | 68 | Returns: 69 | The new dict. 70 | """ 71 | res = cast(Dict[C, D], type(self)()) 72 | for k, v in self.items(): 73 | d = f((k, v)) 74 | res.update(d) 75 | return res 76 | 77 | 78 | def foreach(self: Dict[A, B], f: Callable[[Tuple[A, B]], U]) -> None: 79 | """ 80 | Apply f to each element for its side effects. 81 | 82 | Args: 83 | f: The function to apply to all elements for its side effects. 84 | """ 85 | for k, v in self.items(): 86 | f((k, v)) 87 | 88 | 89 | def is_empty(self: Dict[A, B]) -> bool: 90 | """ 91 | Tests whether the dict is empty. 92 | 93 | Returns: 94 | True if the dict is empty, False otherwise. 95 | """ 96 | return len(self) == 0 97 | 98 | 99 | def map(self: Dict[A, B], f: Callable[[Tuple[A, B]], Tuple[C, D]]) -> Dict[C, D]: 100 | """ 101 | Builds a new dict by applying a function to all elements of this dict. 102 | 103 | Args: 104 | f: The function to apply to all elements. 105 | 106 | Returns: 107 | The new dict. 108 | """ 109 | return cast(Dict[C, D], type(self)(f(x) for x in self.items())) 110 | 111 | 112 | def to_list(self: Dict[A, B]) -> List[Tuple[A, B]]: 113 | """ 114 | Converts this dict to a list of (key, value) pairs. 115 | 116 | Returns: 117 | A list of pairs corresponding to the entries of the dict 118 | """ 119 | return [(k, v) for k, v in self.items()] 120 | 121 | 122 | def to_iterator(self: Dict[A, B]) -> Iterator[Tuple[A, B]]: 123 | """ 124 | Converts this dict to an iterator of (key, value) pairs. 125 | 126 | Returns: 127 | An iterator of pairs corresponding to the entries of the dict 128 | """ 129 | return ((k, v) for k, v in self.items()) 130 | 131 | 132 | def count(self: Dict[A, B], p: Callable[[Tuple[A, B]], bool]) -> int: 133 | """ 134 | Counts the number of elements in the collection which satisfy a predicate. 135 | 136 | Note: will not terminate for infinite-sized collections. 137 | 138 | Args: 139 | p: The predicate used to test elements. 140 | 141 | Returns: 142 | The number of elements satisfying the predicate p. 143 | """ 144 | c = 0 145 | for t in self.items(): 146 | if p(t): 147 | c += 1 148 | 149 | return c 150 | 151 | 152 | def fold_left(self: Dict[A, B], z: B, op: Callable[[B, Tuple[A, B]], B]) -> B: 153 | """ 154 | Applies a binary operator to a start value and all elements of this collection, going left to right. 155 | 156 | Note: will not terminate for infinite-sized collections. 157 | 158 | Note: might return different results for different runs, unless the underlying collection type is ordered or the operator is associative and commutative. 159 | 160 | Args: 161 | z: The start value. 162 | op: The binary operator. 163 | 164 | Returns: 165 | The result of inserting op between consecutive elements of this collection, going left to right with the start value z on the left: 166 | 167 | op(...op(z, x_1), x_2, ..., x_n) 168 | where x1, ..., xn are the elements of this collection. Returns z if this collection is empty. 169 | """ 170 | acc = z 171 | for t in self.items(): 172 | acc = op(acc, t) 173 | 174 | return acc 175 | 176 | 177 | def fold_right(self: Dict[A, B], z: B, op: Callable[[Tuple[A, B], B], B]) -> B: 178 | """ 179 | Applies a binary operator to a start value and all elements of this collection, going right to left. 180 | 181 | Note: will not terminate for infinite-sized collections. 182 | 183 | Note: might return different results for different runs, unless the underlying collection type is ordered or the operator is associative and commutative. 184 | 185 | Args: 186 | z: The start value. 187 | op: The binary operator. 188 | 189 | Returns: 190 | The result of inserting op between consecutive elements of this collection, going right to left with the start value z on the right: 191 | 192 | op(x_1, op(x_2, ... op(x_n, z)...)) 193 | where x1, ..., xn are the elements of this collection. Returns z if this collection is empty. 194 | """ 195 | acc = z 196 | for t in reversed(self.items()): 197 | acc = op(t, acc) 198 | 199 | return acc 200 | 201 | 202 | def forall(self: Dict[A, B], p: Callable[[Tuple[A, B]], bool]) -> bool: 203 | """ 204 | Tests whether a predicate holds for all elements of this collection. 205 | 206 | Note: may not terminate for infinite-sized collections. 207 | 208 | Args: 209 | p: The predicate used to test elements. 210 | 211 | Returns: 212 | True if this collection is empty or the given predicate p holds for all elements of this collection, otherwise False. 213 | """ 214 | for t in self.items(): 215 | if not p(t): 216 | return False 217 | return True 218 | 219 | 220 | def find(self: Dict[A, B], p: Callable[[Tuple[A, B]], bool]) -> Optional[Tuple[A, B]]: 221 | """ 222 | Finds the first element of the collection satisfying a predicate, if any. 223 | 224 | Note: may not terminate for infinite-sized collections. 225 | 226 | Note: might return different results for different runs, unless the underlying collection type is ordered. 227 | 228 | Args: 229 | p: The predicate used to test elements. 230 | 231 | Returns: 232 | An option value containing the first element in the collection that satisfies p, or None if none exists. 233 | """ 234 | for t in self.items(): 235 | if p(t): 236 | return t 237 | 238 | return None 239 | 240 | 241 | # Parallel operations 242 | 243 | 244 | def par_filter(self: Dict[A, B], p: Callable[[Tuple[A, B]], bool]) -> Dict[A, B]: 245 | """ 246 | Selects in parallel all elements of this dict which satisfy a predicate. 247 | 248 | Args: 249 | p: The predicate to satisfy. 250 | 251 | Returns: 252 | The filtered dict. 253 | """ 254 | preds = dask.compute(*(dask.delayed(p)(x) for x in self.items())) 255 | return type(self)({k: v for i, (k, v) in enumerate(self.items()) if preds[i]}) 256 | 257 | 258 | def par_filter_not(self: Dict[A, B], p: Callable[[Tuple[A, B]], bool]) -> Dict[A, B]: 259 | """ 260 | Selects in parallel all elements of this dict which do not satisfy a predicate. 261 | 262 | Args: 263 | p: The predicate to not satisfy. 264 | 265 | Returns: 266 | The filtered dict. 267 | """ 268 | preds = dask.compute(*(dask.delayed(p)(x) for x in self.items())) 269 | return type(self)({k: v for i, (k, v) in enumerate(self.items()) if not preds[i]}) 270 | 271 | 272 | def par_flat_map( 273 | self: Dict[A, B], f: Callable[[Tuple[A, B]], Dict[C, D]] 274 | ) -> Dict[C, D]: 275 | """ 276 | Builds a new dict by applying a function in parallel to all elements of this dict and using the elements of the resulting collections. 277 | 278 | Args: 279 | f: The function to apply to all elements. 280 | 281 | Returns: 282 | The new dict. 283 | """ 284 | applications = dask.compute(*(dask.delayed(f)(x) for x in self.items())) 285 | return cast( 286 | Dict[C, D], type(self)({k: v for y in applications for k, v in y.items()}) 287 | ) 288 | 289 | 290 | def par_map(self: Dict[A, B], f: Callable[[Tuple[A, B]], Tuple[C, D]]) -> Dict[C, D]: 291 | """ 292 | Builds a new dict by applying a function in parallel to all elements of this dict. 293 | 294 | Args: 295 | f: The function to apply to all elements. 296 | 297 | Returns: 298 | The new dict. 299 | """ 300 | return cast( 301 | Dict[C, D], 302 | type(self)((dask.compute(*(dask.delayed(f)(x) for x in self.items())))), 303 | ) 304 | 305 | 306 | # Pure operations 307 | 308 | 309 | def pure_map(self: Dict[A, B], f: Callable[[Tuple[A, B]], Tuple[C, D]]) -> Dict[C, D]: 310 | """ 311 | Builds a new dict by applying a function to all elements of this dict using memoization to improve performance. 312 | 313 | WARNING: f must be a PURE function i.e., calling f on the same input must always lead to the same result! 314 | 315 | Type A must be hashable using `hash()` function. 316 | 317 | Args: 318 | f: The function to apply to all elements. 319 | 320 | Returns: 321 | The new dict. 322 | """ 323 | f_cache = functools.cache(f) 324 | return cast(Dict[C, D], type(self)(f_cache(x) for x in self.items())) 325 | 326 | 327 | def pure_flat_map( 328 | self: Dict[A, B], f: Callable[[Tuple[A, B]], Dict[C, D]] 329 | ) -> Dict[C, D]: 330 | """ 331 | Builds a new dict by applying a function to all elements of this dict and using the elements of the resulting collections using memoization to improve performance. 332 | 333 | WARNING: f must be a PURE function i.e., calling f on the same input must always lead to the same result! 334 | 335 | Type A must be hashable using `hash()` function. 336 | 337 | Args: 338 | f: The function to apply to all elements. 339 | 340 | Returns: 341 | The new dict. 342 | """ 343 | res = cast(Dict[C, D], type(self)()) 344 | f_cache = functools.cache(f) 345 | for k, v in self.items(): 346 | d = f_cache((k, v)) 347 | res.update(d) 348 | return res 349 | 350 | 351 | def pure_filter(self: Dict[A, B], p: Callable[[Tuple[A, B]], bool]) -> Dict[A, B]: 352 | """ 353 | Selects all elements of this dict which satisfy a predicate using memoization to improve performance. 354 | 355 | WARNING: p must be a PURE function i.e., calling p on the same input must always lead to the same result! 356 | 357 | Type A must be hashable using `hash()` function. 358 | 359 | 360 | Args: 361 | p: The predicate to satisfy. 362 | 363 | Returns: 364 | The filtered dict. 365 | """ 366 | p_cache = functools.cache(p) 367 | return type(self)({k: v for k, v in self.items() if p_cache((k, v))}) 368 | 369 | 370 | def pure_filter_not(self: Dict[A, B], p: Callable[[Tuple[A, B]], bool]) -> Dict[A, B]: 371 | """ 372 | Selects all elements of this dict which do not satisfy a predicate using memoization to improve performance. 373 | 374 | WARNING: p must be a PURE function i.e., calling p on the same input must always lead to the same result! 375 | 376 | Type A must be hashable using `hash()` function. 377 | 378 | 379 | Args: 380 | p: The predicate not to satisfy. 381 | 382 | Returns: 383 | The filtered dict. 384 | """ 385 | p_cache = functools.cache(p) 386 | return type(self)({k: v for k, v in self.items() if not p_cache((k, v))}) 387 | 388 | 389 | # Lazy operations 390 | 391 | 392 | def lazy_map( 393 | self: Dict[A, B], f: Callable[[Tuple[A, B]], Tuple[C, D]] 394 | ) -> Iterator[Tuple[C, D]]: 395 | """ 396 | Builds a new list of tuples by applying a function to all elements of this dict, lazily. 397 | 398 | Args: 399 | f: The function to apply to all elements. 400 | 401 | Returns: 402 | The new lazy list of tuples, as an iterator. 403 | """ 404 | for x in self.items(): 405 | yield f(x) 406 | 407 | 408 | def lazy_flat_map( 409 | self: Dict[A, B], f: Callable[[Tuple[A, B]], Dict[C, D]] 410 | ) -> Iterator[Tuple[C, D]]: 411 | """ 412 | Builds a new list of tuples by applying a function to all elements of this dict and using the elements of the resulting collections, lazily. 413 | 414 | Args: 415 | f: The function to apply to all elements. 416 | 417 | Returns: 418 | The new lazy list of tuples, as an iterator. 419 | """ 420 | return (y for x in self.items() for y in f(x).items()) 421 | 422 | 423 | def lazy_filter( 424 | self: Dict[A, B], p: Callable[[Tuple[A, B]], bool] 425 | ) -> Iterator[Tuple[A, B]]: 426 | """ 427 | Selects all elements of this dict which satisfy a predicate, lazily. 428 | 429 | Args: 430 | p: The predicate to satisfy. 431 | 432 | Returns: 433 | The filtered lazy list of tuples, as an iterator. 434 | """ 435 | for x in self.items(): 436 | if p(x): 437 | yield x 438 | 439 | 440 | def lazy_filter_not( 441 | self: Dict[A, B], p: Callable[[Tuple[A, B]], bool] 442 | ) -> Iterator[Tuple[A, B]]: 443 | """ 444 | Selects all elements of this dict which do not satisfy a predicate, lazily. 445 | 446 | Args: 447 | p: The predicate to not satisfy. 448 | 449 | Returns: 450 | The filtered lazy list of tuples, as an iterator. 451 | """ 452 | for x in self.items(): 453 | if not p(x): 454 | yield x 455 | -------------------------------------------------------------------------------- /pyfuncol/extend_builtins.py: -------------------------------------------------------------------------------- 1 | from warnings import warn 2 | 3 | from . import dict as pfcdict 4 | from . import list as pfclist 5 | from . import set as pfcset 6 | 7 | EXTEND_BUILTINS = False 8 | try: 9 | from forbiddenfruit import curse 10 | 11 | EXTEND_BUILTINS = True 12 | except ImportError: 13 | warn( 14 | "[WARNING] You are using pyfuncol without forbiddenfruit. Functions will not extend builtins" 15 | ) 16 | 17 | 18 | def extend_dict(): 19 | """ 20 | Extends the dict built-in type with methods. 21 | """ 22 | curse(dict, "contains", pfcdict.contains) 23 | curse(dict, "size", pfcdict.size) 24 | curse(dict, "filter", pfcdict.filter) 25 | curse(dict, "filter_not", pfcdict.filter_not) 26 | curse(dict, "flat_map", pfcdict.flat_map) 27 | curse(dict, "foreach", pfcdict.foreach) 28 | curse(dict, "is_empty", pfcdict.is_empty) 29 | curse(dict, "map", pfcdict.map) 30 | curse(dict, "to_list", pfcdict.to_list) 31 | curse(dict, "to_iterator", pfcdict.to_iterator) 32 | curse(dict, "count", pfcdict.count) 33 | curse(dict, "fold_left", pfcdict.fold_left) 34 | curse(dict, "fold_right", pfcdict.fold_right) 35 | curse(dict, "forall", pfcdict.forall) 36 | curse(dict, "find", pfcdict.find) 37 | 38 | # Parallel operations 39 | curse(dict, "par_map", pfcdict.par_map) 40 | curse(dict, "par_filter", pfcdict.par_filter) 41 | curse(dict, "par_filter_not", pfcdict.par_filter_not) 42 | curse(dict, "par_flat_map", pfcdict.par_flat_map) 43 | 44 | # Pure operations 45 | curse(dict, "pure_map", pfcdict.pure_map) 46 | curse(dict, "pure_flat_map", pfcdict.pure_flat_map) 47 | curse(dict, "pure_filter", pfcdict.pure_filter) 48 | curse(dict, "pure_filter_not", pfcdict.pure_filter_not) 49 | 50 | # Lazy operations 51 | curse(dict, "lazy_map", pfcdict.lazy_map) 52 | curse(dict, "lazy_flat_map", pfcdict.lazy_flat_map) 53 | curse(dict, "lazy_filter", pfcdict.lazy_filter) 54 | curse(dict, "lazy_filter_not", pfcdict.lazy_filter_not) 55 | 56 | 57 | def extend_list(): 58 | """ 59 | Extends the list built-in type with methods. 60 | """ 61 | curse(list, "map", pfclist.map) 62 | curse(list, "filter", pfclist.filter) 63 | curse(list, "filter_not", pfclist.filter_not) 64 | curse(list, "flat_map", pfclist.flat_map) 65 | curse(list, "flatten", pfclist.flatten) 66 | curse(list, "contains", pfclist.contains) 67 | curse(list, "distinct", pfclist.distinct) 68 | curse(list, "foreach", pfclist.foreach) 69 | curse(list, "group_by", pfclist.group_by) 70 | curse(list, "is_empty", pfclist.is_empty) 71 | curse(list, "size", pfclist.size) 72 | curse(list, "find", pfclist.find) 73 | curse(list, "index_of", pfclist.index_of) 74 | curse(list, "fold_left", pfclist.fold_left) 75 | curse(list, "fold_right", pfclist.fold_right) 76 | curse(list, "forall", pfclist.forall) 77 | curse(list, "head", pfclist.head) 78 | curse(list, "tail", pfclist.tail) 79 | curse(list, "take", pfclist.take) 80 | curse(list, "length", pfclist.length) 81 | curse(list, "to_iterator", pfclist.to_iterator) 82 | 83 | # Parallel operations 84 | curse(list, "par_map", pfclist.par_map) 85 | curse(list, "par_filter", pfclist.par_filter) 86 | curse(list, "par_filter_not", pfclist.par_filter_not) 87 | curse(list, "par_flat_map", pfclist.par_flat_map) 88 | 89 | # Pure operations 90 | curse(list, "pure_map", pfclist.pure_map) 91 | curse(list, "pure_flat_map", pfclist.pure_flat_map) 92 | curse(list, "pure_filter", pfclist.pure_filter) 93 | curse(list, "pure_filter_not", pfclist.pure_filter_not) 94 | 95 | # Lazy operations 96 | curse(list, "lazy_map", pfclist.lazy_map) 97 | curse(list, "lazy_flat_map", pfclist.lazy_flat_map) 98 | curse(list, "lazy_filter", pfclist.lazy_filter) 99 | curse(list, "lazy_filter_not", pfclist.lazy_filter_not) 100 | curse(list, "lazy_flatten", pfclist.lazy_flatten) 101 | curse(list, "lazy_distinct", pfclist.lazy_distinct) 102 | curse(list, "lazy_take", pfclist.lazy_take) 103 | 104 | 105 | def extend_set(): 106 | """ 107 | Extends the set and frozenset built-in type with methods. 108 | """ 109 | curse(set, "map", pfcset.map) 110 | curse(set, "filter", pfcset.filter) 111 | curse(set, "filter_not", pfcset.filter_not) 112 | curse(set, "flat_map", pfcset.flat_map) 113 | curse(set, "contains", pfcset.contains) 114 | curse(set, "foreach", pfcset.foreach) 115 | curse(set, "group_by", pfcset.group_by) 116 | curse(set, "is_empty", pfcset.is_empty) 117 | curse(set, "size", pfcset.size) 118 | curse(set, "find", pfcset.find) 119 | curse(set, "fold_left", pfcset.fold_left) 120 | curse(set, "fold_right", pfcset.fold_right) 121 | curse(set, "forall", pfcset.forall) 122 | curse(set, "length", pfcset.length) 123 | curse(set, "to_iterator", pfcset.to_iterator) 124 | 125 | curse(frozenset, "map", pfcset.map) 126 | curse(frozenset, "filter", pfcset.filter) 127 | curse(frozenset, "filter_not", pfcset.filter_not) 128 | curse(frozenset, "flat_map", pfcset.flat_map) 129 | curse(frozenset, "contains", pfcset.contains) 130 | curse(frozenset, "foreach", pfcset.foreach) 131 | curse(frozenset, "group_by", pfcset.group_by) 132 | curse(frozenset, "is_empty", pfcset.is_empty) 133 | curse(frozenset, "size", pfcset.size) 134 | curse(frozenset, "find", pfcset.find) 135 | curse(frozenset, "fold_left", pfcset.fold_left) 136 | curse(frozenset, "fold_right", pfcset.fold_right) 137 | curse(frozenset, "forall", pfcset.forall) 138 | curse(frozenset, "length", pfcset.length) 139 | curse(frozenset, "to_iterator", pfcset.to_iterator) 140 | 141 | # Parallel operations 142 | curse(set, "par_map", pfcset.par_map) 143 | curse(set, "par_filter", pfcset.par_filter) 144 | curse(set, "par_filter_not", pfcset.par_filter_not) 145 | curse(set, "par_flat_map", pfcset.par_flat_map) 146 | 147 | curse(frozenset, "par_map", pfcset.par_map) 148 | curse(frozenset, "par_filter", pfcset.par_filter) 149 | curse(frozenset, "par_filter_not", pfcset.par_filter_not) 150 | curse(frozenset, "par_flat_map", pfcset.par_flat_map) 151 | 152 | # Pure operations 153 | curse(set, "pure_map", pfcset.pure_map) 154 | curse(set, "pure_flat_map", pfcset.pure_flat_map) 155 | curse(set, "pure_filter", pfcset.pure_filter) 156 | curse(set, "pure_filter_not", pfcset.pure_filter_not) 157 | 158 | curse(frozenset, "pure_map", pfcset.pure_map) 159 | curse(frozenset, "pure_flat_map", pfcset.pure_flat_map) 160 | curse(frozenset, "pure_filter", pfcset.pure_filter) 161 | curse(frozenset, "pure_filter_not", pfcset.pure_filter_not) 162 | 163 | # Lazy operations 164 | curse(set, "lazy_map", pfcset.lazy_map) 165 | curse(set, "lazy_flat_map", pfcset.lazy_flat_map) 166 | curse(set, "lazy_filter", pfcset.lazy_filter) 167 | curse(set, "lazy_filter_not", pfcset.lazy_filter_not) 168 | 169 | curse(frozenset, "lazy_map", pfcset.lazy_map) 170 | curse(frozenset, "lazy_flat_map", pfcset.lazy_flat_map) 171 | curse(frozenset, "lazy_filter", pfcset.lazy_filter) 172 | curse(frozenset, "lazy_filter_not", pfcset.lazy_filter_not) 173 | 174 | 175 | if EXTEND_BUILTINS: 176 | extend_dict() 177 | extend_list() 178 | extend_set() 179 | -------------------------------------------------------------------------------- /pyfuncol/set.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from typing import Callable, Dict, Optional, TypeVar, Set, cast, Iterator 3 | import functools 4 | import dask 5 | 6 | A = TypeVar("A") 7 | B = TypeVar("B") 8 | K = TypeVar("K") 9 | U = TypeVar("U") 10 | 11 | 12 | def map(self: Set[A], f: Callable[[A], B]) -> Set[B]: 13 | """ 14 | Builds a new set by applying a function to all elements of this set. 15 | 16 | Args: 17 | f: The function to apply to all elements. 18 | 19 | Returns: 20 | The new set. 21 | """ 22 | return cast(Set[B], type(self)(f(x) for x in self)) 23 | 24 | 25 | def filter(self: Set[A], p: Callable[[A], bool]) -> Set[A]: 26 | """ 27 | Selects all elements of this set which satisfy a predicate. 28 | 29 | Args: 30 | p: The predicate to satisfy. 31 | 32 | Returns: 33 | The filtered set. 34 | """ 35 | return type(self)(x for x in self if p(x)) 36 | 37 | 38 | def filter_not(self: Set[A], p: Callable[[A], bool]) -> Set[A]: 39 | """ 40 | Selects all elements of this set which do not satisfy a predicate. 41 | 42 | Args: 43 | p: The predicate to not satisfy. 44 | 45 | Returns: 46 | The filtered set. 47 | """ 48 | return type(self)(x for x in self if not p(x)) 49 | 50 | 51 | def flat_map(self: Set[A], f: Callable[[A], Set[B]]) -> Set[B]: 52 | """ 53 | Builds a new set by applying a function to all elements of this set and using the elements of the resulting collections. 54 | 55 | Args: 56 | f: The function to apply to all elements. 57 | 58 | Returns: 59 | The new set. 60 | """ 61 | return cast(Set[B], type(self)(y for x in self for y in f(x))) 62 | 63 | 64 | def contains(self: Set[A], elem: A) -> bool: 65 | """ 66 | Tests whether this set contains a given value as element. 67 | 68 | Args: 69 | elem: The element to look for. 70 | 71 | Returns: 72 | True if the set contains the element, False otherwise. 73 | """ 74 | return elem in self 75 | 76 | 77 | def foreach(self: Set[A], f: Callable[[A], U]) -> None: 78 | """ 79 | Apply f to each element of the set for its side effects. 80 | 81 | Args: 82 | f: The function to apply to all elements for its side effects. 83 | """ 84 | for x in self: 85 | f(x) 86 | 87 | 88 | def group_by(self: Set[A], f: Callable[[A], K]) -> Dict[K, Set[A]]: 89 | """ 90 | Partitions this set into a dict of sets according to some discriminator function. 91 | 92 | Args: 93 | f: The grouping function. 94 | 95 | Returns: 96 | A dictionary where elements are grouped according to the grouping function. 97 | """ 98 | # frozenset does not have `add` 99 | d = defaultdict(set if isinstance(self, frozenset) else type(self)) 100 | for x in self: 101 | k = f(x) 102 | d[k].add(x) 103 | return d 104 | 105 | 106 | def is_empty(self: Set[A]) -> bool: 107 | """ 108 | Tests whether the set is empty. 109 | 110 | Returns: 111 | True if the set is empty, False otherwise. 112 | """ 113 | return len(self) == 0 114 | 115 | 116 | def size(self: Set[A]) -> int: 117 | """ 118 | Computes the size of this set. 119 | 120 | Returns: 121 | The size of the set. 122 | """ 123 | return len(self) 124 | 125 | 126 | def find(self: Set[A], p: Callable[[A], bool]) -> Optional[A]: 127 | """ 128 | Finds the first element of the set satisfying a predicate, if any. 129 | 130 | Args: 131 | p: The predicate to satisfy. 132 | 133 | Returns: 134 | The first element satisfying the predicate, otherwise None. 135 | """ 136 | for x in self: 137 | if p(x): 138 | return x 139 | return None 140 | 141 | 142 | def fold_left(self: Set[A], z: B, op: Callable[[B, A], B]) -> B: 143 | """ 144 | Applies a binary operator to a start value and all elements of this set, going left to right. 145 | 146 | Note: might return different results for different runs, unless the underlying collection type is ordered or the operator is associative and commutative. 147 | 148 | Args: 149 | z: The start value. 150 | op: The binary operation. 151 | 152 | Returns: 153 | The result of inserting op between consecutive elements of this set, going left to right with the start value z on the left: 154 | op(...op(z, x_1), x_2, ..., x_n) 155 | where x1, ..., xn are the elements of this set. Returns z if this set is empty. 156 | """ 157 | acc = z 158 | for x in self: 159 | acc = op(acc, x) 160 | return acc 161 | 162 | 163 | def fold_right(self: Set[A], z: B, op: Callable[[A, B], B]) -> B: 164 | """ 165 | Applies a binary operator to all elements of this set and a start value, going right to left. 166 | 167 | Note: might return different results for different runs, unless the underlying collection type is ordered or the operator is associative and commutative. 168 | 169 | Args: 170 | z: The start value. 171 | op: The binary operation. 172 | 173 | Returns: 174 | The result of inserting op between consecutive elements of this set, going right to left with the start value z on the right: 175 | op(x_1, op(x_2, ... op(x_n, z)...)) 176 | where x1, ..., xn are the elements of this set. Returns z if this set is empty. 177 | """ 178 | 179 | acc = z 180 | for x in self: 181 | acc = op(x, acc) 182 | return acc 183 | 184 | 185 | def forall(self: Set[A], p: Callable[[A], bool]) -> bool: 186 | """ 187 | Tests whether a predicate holds for all elements of this set. 188 | 189 | Args: 190 | p: The predicate used to test elements. 191 | 192 | Returns: 193 | True if this set is empty or the given predicate p holds for all elements of this set, otherwise False. 194 | """ 195 | for x in self: 196 | if not p(x): 197 | return False 198 | 199 | return True 200 | 201 | 202 | def length(self: Set[A]) -> int: 203 | """ 204 | Returns the length (number of elements) of the set. `size` is an alias for length. 205 | 206 | Returns: 207 | The length of the set 208 | """ 209 | return len(self) 210 | 211 | 212 | def to_iterator(self: Set[A]) -> Iterator[A]: 213 | """ 214 | Converts this set to an iterator. 215 | 216 | Returns: 217 | An iterator 218 | """ 219 | return (x for x in self) 220 | 221 | # Parallel operations 222 | 223 | 224 | def par_map(self: Set[A], f: Callable[[A], B]) -> Set[B]: 225 | """ 226 | Builds a new set by applying in parallel a function to all elements of this set. 227 | 228 | Args: 229 | f: The function to apply to all elements. 230 | 231 | Returns: 232 | The new set. 233 | """ 234 | return cast(Set[B], type(self)((dask.compute(*(dask.delayed(f)(x) for x in self))))) 235 | 236 | 237 | def par_filter(self: Set[A], p: Callable[[A], bool]) -> Set[A]: 238 | """ 239 | Selects in parallel all elements of this set which satisfy a predicate. 240 | 241 | Args: 242 | p: The predicate to satisfy. 243 | 244 | Returns: 245 | The filtered set. 246 | """ 247 | preds = dask.compute(*(dask.delayed(p)(x) for x in self)) 248 | return type(self)(x for i, x in enumerate(self) if preds[i]) 249 | 250 | 251 | def par_filter_not(self: Set[A], p: Callable[[A], bool]) -> Set[A]: 252 | """ 253 | Selects in parallel all elements of this set which do not satisfy a predicate. 254 | 255 | Args: 256 | p: The predicate to not satisfy. 257 | 258 | Returns: 259 | The filtered set. 260 | """ 261 | preds = dask.compute(*(dask.delayed(p)(x) for x in self)) 262 | return type(self)(x for i, x in enumerate(self) if not preds[i]) 263 | 264 | 265 | def par_flat_map(self: Set[A], f: Callable[[A], Set[B]]) -> Set[B]: 266 | """ 267 | Builds a new set by applying in parallel a function to all elements of this set and using the elements of the resulting collections. 268 | 269 | Args: 270 | f: The function to apply to all elements. 271 | 272 | Returns: 273 | The new set. 274 | """ 275 | applications = dask.compute(*(dask.delayed(f)(x) for x in self)) 276 | return cast(Set[B], type(self)(x for y in applications for x in y)) 277 | 278 | 279 | # Pure operations 280 | 281 | 282 | def pure_map(self: Set[A], f: Callable[[A], B]) -> Set[B]: 283 | """ 284 | Builds a new set by applying a function to all elements of this set using memoization to improve performance. 285 | 286 | WARNING: f must be a PURE function i.e., calling f on the same input must always lead to the same result! 287 | 288 | Type A must be hashable using `hash()` function. 289 | 290 | Args: 291 | f: The PURE function to apply to all elements. 292 | 293 | Returns: 294 | The new set. 295 | """ 296 | f_cache = functools.cache(f) 297 | return cast(Set[B], type(self)(f_cache(x) for x in self)) 298 | 299 | 300 | def pure_flat_map(self: Set[A], f: Callable[[A], Set[B]]) -> Set[B]: 301 | """ 302 | Builds a new set by applying a function to all elements of this set and using the elements of the resulting collections using memoization to improve performance. 303 | 304 | WARNING: f must be a PURE function i.e., calling f on the same input must always lead to the same result! 305 | 306 | Type A must be hashable using `hash()` function. 307 | 308 | Args: 309 | f: The function to apply to all elements. 310 | 311 | Returns: 312 | The new set. 313 | """ 314 | f_cache = functools.cache(f) 315 | return cast(Set[B], type(self)(y for x in self for y in f_cache(x))) 316 | 317 | 318 | def pure_filter(self: Set[A], p: Callable[[A], bool]) -> Set[A]: 319 | """ 320 | Selects all elements of this set which satisfy a predicate using memoization to improve performance. 321 | 322 | WARNING: p must be a PURE function i.e., calling p on the same input must always lead to the same result! 323 | 324 | Type A must be hashable using `hash()` function. 325 | 326 | Args: 327 | p: The predicate to satisfy. 328 | 329 | Returns: 330 | The filtered set. 331 | """ 332 | p_cache = functools.cache(p) 333 | return type(self)(x for x in self if p_cache(x)) 334 | 335 | 336 | def pure_filter_not(self: Set[A], p: Callable[[A], bool]) -> Set[A]: 337 | """ 338 | Selects all elements of this set which do not satisfy a predicate using memoization to improve performance. 339 | 340 | WARNING: p must be a PURE function i.e., calling p on the same input must always lead to the same result! 341 | 342 | Type A must be hashable using `hash()` function. 343 | 344 | 345 | Args: 346 | p: The predicate not to satisfy. 347 | 348 | Returns: 349 | The filtered set. 350 | """ 351 | p_cache = functools.cache(p) 352 | return type(self)(x for x in self if not p_cache(x)) 353 | 354 | 355 | def lazy_map(self: Set[A], f: Callable[[A], B]) -> Iterator[B]: 356 | """ 357 | Builds a new set by applying a function to all elements of this set, lazily. 358 | 359 | Args: 360 | f: The function to apply to all elements. 361 | 362 | Returns: 363 | The new lazy set, as an iterator. 364 | """ 365 | for x in self: 366 | yield f(x) 367 | 368 | 369 | def lazy_filter(self: Set[A], p: Callable[[A], bool]) -> Iterator[A]: 370 | """ 371 | Selects all elements of this set which satisfy a predicate, lazily. 372 | 373 | Args: 374 | p: The predicate to satisfy. 375 | 376 | Returns: 377 | The filtered lazy set, as an iterator. 378 | """ 379 | for x in self: 380 | if p(x): 381 | yield x 382 | 383 | 384 | def lazy_filter_not(self: Set[A], p: Callable[[A], bool]) -> Iterator[A]: 385 | """ 386 | Selects all elements of this set which do not satisfy a predicate, lazily. 387 | 388 | Args: 389 | p: The predicate to not satisfy. 390 | 391 | Returns: 392 | The filtered lazy set, as an iterator. 393 | """ 394 | for x in self: 395 | if not p(x): 396 | yield x 397 | 398 | 399 | def lazy_flat_map(self: Set[A], f: Callable[[A], Set[B]]) -> Iterator[B]: 400 | """ 401 | Builds a new lazy set by applying a function to all elements of this set and using the elements of the resulting collections. 402 | 403 | Args: 404 | f: The function to apply to all elements. 405 | 406 | Returns: 407 | The new lazy set, as an iterator. 408 | """ 409 | return (y for x in self for y in f(x)) 410 | -------------------------------------------------------------------------------- /pyfuncol/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didactic-meme/pyfuncol/60af7017629b7578fe04aefeecbcdfbd2b2ecb49/pyfuncol/tests/__init__.py -------------------------------------------------------------------------------- /pyfuncol/tests/test_dict.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | import pyfuncol 3 | 4 | d = {"a": 1, "b": 2, "c": 3} 5 | di: OrderedDict[str, int] = OrderedDict(d) 6 | 7 | 8 | def test_contains(): 9 | assert d.contains("a") == True 10 | assert d.contains("z") == False 11 | 12 | 13 | def test_size(): 14 | assert d.size() == 3 15 | 16 | 17 | def test_filter(): 18 | assert d.filter(lambda kv: kv[1] > 1) == {"b": 2, "c": 3} 19 | 20 | # Test that type is preserved 21 | di: OrderedDict[str, int] = OrderedDict(d) 22 | assert di.filter(lambda kv: True) == di 23 | 24 | 25 | def test_filter_not(): 26 | assert d.filter_not(lambda kv: kv[1] > 1) == {"a": 1} 27 | 28 | # Test that type is preserved 29 | di: OrderedDict[str, int] = OrderedDict(d) 30 | assert di.filter_not(lambda kv: False) == di 31 | 32 | 33 | def test_flat_map(): 34 | assert d.flat_map(lambda kv: {kv[0]: kv[1] ** 2}) == {"a": 1, "b": 4, "c": 9} 35 | 36 | # Test that type is preserved 37 | di: OrderedDict[str, int] = OrderedDict(d) 38 | assert di.flat_map(lambda kv: {kv[0]: kv[1]}) == di 39 | 40 | 41 | def test_foreach(): 42 | tester = [] 43 | d.foreach(lambda kv: tester.append(kv)) 44 | assert tester == [("a", 1), ("b", 2), ("c", 3)] 45 | 46 | 47 | def test_is_empty(): 48 | assert d.is_empty() == False 49 | assert {}.is_empty() == True 50 | 51 | 52 | def test_map(): 53 | assert d.map(lambda kv: (kv[0], kv[1] ** 2)) == {"a": 1, "b": 4, "c": 9} 54 | 55 | # Test that type is preserved 56 | di: OrderedDict[str, int] = OrderedDict(d) 57 | assert di.map(lambda kv: kv) == di 58 | 59 | 60 | def test_to_list(): 61 | assert d.to_list() == [("a", 1), ("b", 2), ("c", 3)] 62 | 63 | 64 | def test_to_iterator(): 65 | it = d.to_iterator() 66 | assert next(it) == ("a", 1) 67 | assert list(it) == [("b", 2), ("c", 3)] 68 | 69 | 70 | def test_count(): 71 | assert d.count(lambda kv: (kv[0] == "a" or kv[0] == "b") and kv[1] <= 3) == 2 72 | 73 | 74 | def test_fold_left(): 75 | assert d.fold_left("", lambda acc, kv: acc + kv[0] + str(kv[1])) == "a1b2c3" 76 | 77 | 78 | def test_fold_right(): 79 | assert d.fold_right("", lambda kv, acc: acc + kv[0] + str(kv[1])) == "c3b2a1" 80 | 81 | 82 | def test_forall(): 83 | assert d.forall(lambda kv: kv[1] <= 3) == True 84 | 85 | 86 | def test_forall_false(): 87 | assert d.forall(lambda kv: kv[1] < 2) == False 88 | 89 | 90 | def test_find(): 91 | assert d.find(lambda kv: kv[1] == 2) == ("b", 2) 92 | 93 | 94 | def test_find_none(): 95 | assert d.find(lambda kv: kv[1] == 5) == None 96 | 97 | 98 | # Parallel operations 99 | 100 | 101 | def test_par_filter(): 102 | assert d.par_filter(lambda kv: kv[1] > 1) == {"b": 2, "c": 3} 103 | 104 | # Test that type is preserved 105 | assert di.par_filter(lambda kv: True) == di 106 | 107 | 108 | def test_par_filter_not(): 109 | assert d.par_filter_not(lambda kv: kv[1] <= 1) == {"b": 2, "c": 3} 110 | 111 | # Test that type is preserved 112 | assert di.par_filter_not(lambda kv: False) == di 113 | 114 | 115 | def test_par_flat_map(): 116 | assert d.par_flat_map(lambda kv: {kv[0]: kv[1] ** 2}) == {"a": 1, "b": 4, "c": 9} 117 | 118 | # Test that type is preserved 119 | assert di.par_flat_map(lambda kv: {kv[0]: kv[1]}) == di 120 | 121 | 122 | def test_par_map(): 123 | assert d.par_map(lambda kv: (kv[0], kv[1] ** 2)) == {"a": 1, "b": 4, "c": 9} 124 | 125 | # Test that type is preserved 126 | assert di.par_map(lambda kv: kv) == di 127 | 128 | 129 | # Pure operations 130 | 131 | 132 | def test_pure_flat_map(): 133 | assert d.pure_flat_map(lambda kv: {kv[0]: kv[1] ** 2}) == {"a": 1, "b": 4, "c": 9} 134 | 135 | # Test that type is preserved 136 | assert di.pure_flat_map(lambda kv: {kv[0]: kv[1] ** 2}) == {"a": 1, "b": 4, "c": 9} 137 | 138 | 139 | def test_pure_map(): 140 | assert d.pure_map(lambda kv: (kv[0], kv[1] ** 2)) == {"a": 1, "b": 4, "c": 9} 141 | 142 | # Test that type is preserved 143 | assert di.pure_map(lambda kv: (kv[0], kv[1] ** 2)) == {"a": 1, "b": 4, "c": 9} 144 | 145 | 146 | def test_pure_filter(): 147 | assert d.pure_filter(lambda kv: kv[1] > 1) == {"b": 2, "c": 3} 148 | 149 | # Test that type is preserved 150 | assert di.pure_filter(lambda kv: kv[1] > 1) == {"b": 2, "c": 3} 151 | 152 | 153 | def test_pure_filter_not(): 154 | assert d.pure_filter_not(lambda kv: kv[1] > 1) == {"a": 1} 155 | 156 | # Test that type is preserved 157 | assert d.pure_filter_not(lambda kv: kv[1] > 1) == {"a": 1} 158 | 159 | 160 | # Lazy operations 161 | 162 | 163 | def test_lazy_flat_map(): 164 | res = d.lazy_flat_map(lambda kv: {kv[0]: kv[1] ** 2}) 165 | assert next(res) == ("a", 1) 166 | assert list(res) == [("b", 4), ("c", 9)] 167 | 168 | 169 | def test_lazy_map(): 170 | res = d.lazy_map(lambda kv: (kv[0], kv[1] ** 2)) 171 | assert next(res) == ("a", 1) 172 | assert list(res) == [("b", 4), ("c", 9)] 173 | 174 | 175 | def test_lazy_filter(): 176 | res = d.lazy_filter(lambda kv: kv[1] > 1) 177 | assert next(res) == ("b", 2) 178 | assert list(res) == [("c", 3)] 179 | 180 | 181 | def test_lazy_filter_not(): 182 | res = d.lazy_filter_not(lambda kv: kv[1] <= 1) 183 | assert next(res) == ("b", 2) 184 | assert list(res) == [("c", 3)] 185 | -------------------------------------------------------------------------------- /pyfuncol/tests/test_list.py: -------------------------------------------------------------------------------- 1 | import pyfuncol 2 | import pytest 3 | 4 | 5 | class Lst(list): 6 | pass 7 | 8 | 9 | l = [1, 2, 3] 10 | lst = Lst(l) 11 | 12 | 13 | def test_map(): 14 | assert l.map(lambda x: x * 2) == [2, 4, 6] 15 | 16 | # Test that type is preserved 17 | assert lst.map(lambda x: x) == lst 18 | 19 | 20 | def test_filter(): 21 | assert l.filter(lambda x: x >= 2) == [2, 3] 22 | 23 | # Test that type is preserved 24 | assert lst.filter(lambda x: True) == lst 25 | 26 | 27 | def test_filter_not(): 28 | assert l.filter_not(lambda x: x < 2) == [2, 3] 29 | 30 | # Test that type is preserved 31 | assert lst.filter_not(lambda x: False) == lst 32 | 33 | 34 | def test_flat_map(): 35 | assert l.flat_map(lambda x: [x ** 2]) == [1, 4, 9] 36 | 37 | # Test that type is preserved 38 | assert lst.flat_map(lambda x: [x]) == lst 39 | 40 | 41 | def test_flatten(): 42 | assert [[1, 2], [3]].flatten() == l 43 | 44 | # Test that type is preserved 45 | lst = Lst([[1, 2], [3]]) 46 | assert lst.flatten() == Lst(l) 47 | 48 | 49 | def test_contains(): 50 | assert l.contains(2) == True 51 | 52 | 53 | def test_distinct(): 54 | assert [1, 1, 2, 2, 3].distinct() == l 55 | 56 | # Test that type is preserved 57 | lst = Lst([1, 1, 2, 2, 3]) 58 | assert lst.distinct() == Lst(l) 59 | 60 | 61 | def test_group_by(): 62 | assert ["abc", "def", "e"].group_by(lambda s: len(s)) == { 63 | 3: ["abc", "def"], 64 | 1: ["e"], 65 | } 66 | 67 | 68 | def test_is_empty(): 69 | assert l.is_empty() == False 70 | assert [].is_empty() == True 71 | 72 | 73 | def test_size(): 74 | assert l.size() == 3 75 | 76 | 77 | def test_find(): 78 | assert l.find(lambda x: x >= 3) == 3 79 | assert l.find(lambda x: x < 0) == None 80 | 81 | 82 | def test_index_of(): 83 | assert l.index_of(3) == 2 84 | assert l.index_of(42) == -1 85 | 86 | 87 | def test_foreach(): 88 | tester = [] 89 | l.foreach(lambda x: tester.append(x)) 90 | assert tester == l 91 | 92 | 93 | def test_fold_left_plus(): 94 | a = l.fold_left(0, lambda acc, n: acc + n) 95 | assert a == 6 96 | 97 | 98 | def test_fold_left_concat(): 99 | a = l.fold_left("", lambda acc, n: acc + str(n)) 100 | assert a == "123" 101 | 102 | 103 | def test_fold_right_plus(): 104 | a = l.fold_right(0, lambda n, acc: acc + n) 105 | assert a == 6 106 | 107 | 108 | def test_fold_right_concat(): 109 | a = l.fold_right("", lambda n, acc: acc + str(n)) 110 | assert a == "321" 111 | 112 | 113 | def test_forall_gt_zero(): 114 | a = l.forall(lambda n: n > 0) 115 | assert a == True 116 | 117 | 118 | def test_forall_gt_two(): 119 | a = l.forall(lambda n: n > 2) 120 | assert a == False 121 | 122 | 123 | def test_head(): 124 | h = l.head() 125 | assert h == 1 126 | 127 | 128 | def test_head_empty(): 129 | l = [] 130 | with pytest.raises(IndexError): 131 | l.head() 132 | 133 | 134 | def test_tail(): 135 | t = l.tail() 136 | assert t == [2, 3] 137 | 138 | 139 | def test_tail_empty(): 140 | l = [] 141 | with pytest.raises(IndexError): 142 | l.tail() 143 | 144 | 145 | def test_take_neg(): 146 | a = l.take(-1) 147 | assert a == [] 148 | 149 | # Test that type is preserved 150 | assert lst.take(-1) == Lst() 151 | 152 | 153 | def test_take_greater_len(): 154 | a = l.take(4) 155 | assert a == l 156 | 157 | # Test that type is preserved 158 | assert lst.take(4) == lst 159 | 160 | 161 | def test_take_smaller_len(): 162 | a = l.take(2) 163 | assert a == [1, 2] 164 | 165 | # Test that type is preserved 166 | assert lst.take(2) == Lst(l.take(2)) 167 | 168 | 169 | def test_length(): 170 | a = l.length() 171 | assert a == 3 172 | 173 | 174 | def test_length_equal_size(): 175 | assert l.size() == l.length() 176 | 177 | 178 | def test_to_iterator(): 179 | it = l.to_iterator() 180 | assert next(it) == l[0] 181 | assert list(it) == l[1:] 182 | 183 | 184 | # Parallel operations 185 | 186 | 187 | def test_par_map(): 188 | assert l.par_map(lambda x: x * 2) == [2, 4, 6] 189 | 190 | # Test that type is preserved 191 | assert lst.par_map(lambda x: x) == lst 192 | 193 | 194 | def test_par_filter(): 195 | assert l.par_filter(lambda x: x >= 2) == [2, 3] 196 | 197 | # Test that type is preserved 198 | assert lst.par_filter(lambda x: True) == lst 199 | 200 | 201 | def test_par_filter_not(): 202 | assert l.par_filter_not(lambda x: x < 2) == [2, 3] 203 | 204 | # Test that type is preserved 205 | assert lst.par_filter_not(lambda x: False) == lst 206 | 207 | 208 | def test_par_flat_map(): 209 | assert l.par_flat_map(lambda x: [x ** 2]) == [1, 4, 9] 210 | 211 | # Test that type is preserved 212 | assert lst.par_flat_map(lambda x: [x]) == lst 213 | 214 | 215 | # Pure operations 216 | 217 | 218 | def test_pure_map(): 219 | assert l.pure_map(lambda x: x * 2) == [2, 4, 6] 220 | 221 | # Test that type is preserved 222 | assert lst.pure_map(lambda x: x * 2) == [2, 4, 6] 223 | 224 | 225 | def test_pure_flat_map(): 226 | assert l.pure_flat_map(lambda x: [x ** 2]) == [1, 4, 9] 227 | 228 | # Test that type is preserved 229 | assert lst.pure_flat_map(lambda x: [x ** 2]) == [1, 4, 9] 230 | 231 | 232 | def test_pure_filter(): 233 | assert l.pure_filter(lambda x: x >= 2) == [2, 3] 234 | 235 | # Test that type is preserved 236 | assert lst.pure_filter(lambda x: x >= 2) == [2, 3] 237 | 238 | 239 | def test_pure_filter_not(): 240 | assert l.pure_filter_not(lambda x: x >= 2) == [1] 241 | 242 | # Test that type is preserved 243 | assert lst.pure_filter_not(lambda x: x >= 2) == [1] 244 | 245 | 246 | # Lazy operations 247 | 248 | 249 | def test_lazy_map(): 250 | res = l.lazy_map(lambda x: x * 2) 251 | assert next(res) == 2 252 | assert list(res) == [4, 6] 253 | 254 | 255 | def test_lazy_flat_map(): 256 | res = l.lazy_flat_map(lambda x: [x * 2]) 257 | assert next(res) == 2 258 | assert list(res) == [4, 6] 259 | 260 | 261 | def test_lazy_filter(): 262 | res = l.lazy_filter(lambda x: x >= 2) 263 | assert next(res) == 2 264 | assert list(res) == [3] 265 | 266 | 267 | def test_lazy_filter_not(): 268 | res = l.lazy_filter_not(lambda x: x < 2) 269 | assert next(res) == 2 270 | assert list(res) == [3] 271 | 272 | 273 | def test_lazy_flatten(): 274 | res = [[1, 2], [3]].lazy_flatten() 275 | assert next(res) == 1 276 | assert list(res) == [2, 3] 277 | 278 | 279 | def test_lazy_distinct(): 280 | res = [1, 1, 2, 2, 3].lazy_distinct() 281 | assert next(res) == 1 282 | assert list(res) == [2, 3] 283 | 284 | 285 | def test_lazy_take_neg(): 286 | a = l.lazy_take(-1) 287 | assert list(a) == [] 288 | 289 | 290 | def test_lazy_take_greater_len(): 291 | a = l.lazy_take(4) 292 | assert next(a) == l[0] 293 | assert list(a) == l[1:] 294 | 295 | 296 | def test_lazy_take_smaller_len(): 297 | a = l.lazy_take(2) 298 | assert next(a) == 1 299 | assert list(a) == [2] 300 | -------------------------------------------------------------------------------- /pyfuncol/tests/test_no_forbiddenfruit.py: -------------------------------------------------------------------------------- 1 | from .. import dict as pfcdict 2 | from .. import list as pfclist 3 | from .. import set as pfcset 4 | 5 | def test_dict(): 6 | assert pfcdict.map({'a': 1, 'b': 2, 'c': 3}, lambda x: (x[0], x[1] * 2)) == {'a': 2, 'b': 4, 'c': 6} 7 | 8 | def test_list(): 9 | assert pfclist.map([1, 2, 3], lambda x: x * 2) == [2, 4, 6] 10 | 11 | def test_set(): 12 | assert pfcset.map({1, 2, 3}, lambda x: x * 2) == {2, 4, 6} -------------------------------------------------------------------------------- /pyfuncol/tests/test_set.py: -------------------------------------------------------------------------------- 1 | from operator import ne 2 | import pyfuncol 3 | 4 | s = {1, 2, 3} 5 | st = frozenset(s) 6 | 7 | 8 | def test_map(): 9 | s = {1, 2, 3} 10 | st = frozenset(s) 11 | assert s.map(lambda x: x * 2) == {2, 4, 6} 12 | assert st.map(lambda x: x * 2) == frozenset({2, 4, 6}) 13 | 14 | 15 | def test_filter(): 16 | s = {1, 2, 3} 17 | st = frozenset(s) 18 | assert s.filter(lambda x: x >= 2) == {2, 3} 19 | assert st.filter(lambda x: x >= 2) == frozenset({2, 3}) 20 | 21 | 22 | def test_filter_not(): 23 | s = {1, 2, 3} 24 | st = frozenset(s) 25 | assert s.filter_not(lambda x: x < 2) == {2, 3} 26 | assert st.filter_not(lambda x: x < 2) == frozenset({2, 3}) 27 | 28 | 29 | def test_flat_map(): 30 | s = {1, 2, 3} 31 | st = frozenset(s) 32 | assert s.flat_map(lambda x: {x**2}) == {1, 4, 9} 33 | assert st.flat_map(lambda x: {x**2}) == frozenset({1, 4, 9}) 34 | 35 | 36 | def test_contains(): 37 | s = {1, 2, 3} 38 | st = frozenset(s) 39 | assert s.contains(2) == True 40 | assert st.contains(2) == True 41 | 42 | 43 | def test_group_by(): 44 | s = {1, 2, 3} 45 | st = frozenset(s) 46 | assert {"abc", "def", "e"}.group_by(lambda s: len(s)) == { 47 | 3: {"abc", "def"}, 48 | 1: {"e"}, 49 | } 50 | assert frozenset({"abc", "def", "e"}).group_by(lambda s: len(s)) == { 51 | 3: {"abc", "def"}, 52 | 1: {"e"}, 53 | } 54 | 55 | 56 | def test_is_empty(): 57 | s = {1, 2, 3} 58 | st = frozenset(s) 59 | assert s.is_empty() == False 60 | empty = set() 61 | assert empty.is_empty() == True 62 | 63 | assert st.is_empty() == False 64 | frozen_empty = frozenset() 65 | assert frozen_empty.is_empty() == True 66 | 67 | 68 | def test_size(): 69 | s = {1, 2, 3} 70 | st = frozenset(s) 71 | assert s.size() == 3 72 | assert st.size() == 3 73 | 74 | 75 | def test_find(): 76 | s = {1, 2, 3} 77 | st = frozenset(s) 78 | assert s.find(lambda x: x >= 3) == 3 79 | assert s.find(lambda x: x < 0) == None 80 | 81 | assert st.find(lambda x: x >= 3) == 3 82 | assert st.find(lambda x: x < 0) == None 83 | 84 | 85 | def test_foreach(): 86 | s = {1, 2, 3} 87 | st = frozenset(s) 88 | tester = set() 89 | s.foreach(lambda x: tester.add(x)) 90 | assert tester == s 91 | 92 | frozen_tester = set() 93 | st.foreach(lambda x: frozen_tester.add(x)) 94 | assert frozen_tester == s 95 | 96 | 97 | def test_fold_left_plus(): 98 | s = {1, 2, 3} 99 | st = frozenset(s) 100 | assert s.fold_left(0, lambda acc, n: acc + n) == 6 101 | assert st.fold_left(0, lambda acc, n: acc + n) == 6 102 | 103 | 104 | def test_fold_left_concat(): 105 | s = {1, 2, 3} 106 | st = frozenset(s) 107 | a = s.fold_left("", lambda acc, n: acc + str(n)) 108 | assert ( 109 | a == "123" or a == "321" or a == "132" or a == "213" or a == "231" or a == "312" 110 | ) 111 | 112 | frozen_a = st.fold_left("", lambda acc, n: acc + str(n)) 113 | assert ( 114 | frozen_a == "123" 115 | or frozen_a == "321" 116 | or frozen_a == "132" 117 | or frozen_a == "213" 118 | or frozen_a == "231" 119 | or frozen_a == "312" 120 | ) 121 | 122 | 123 | def test_fold_right_plus(): 124 | s = {1, 2, 3} 125 | st = frozenset(s) 126 | assert s.fold_right(0, lambda n, acc: acc + n) == 6 127 | assert st.fold_right(0, lambda n, acc: acc + n) == 6 128 | 129 | 130 | def test_fold_right_concat(): 131 | s = {1, 2, 3} 132 | st = frozenset(s) 133 | a = s.fold_right("", lambda n, acc: acc + str(n)) 134 | assert ( 135 | a == "321" or a == "123" or a == "132" or a == "213" or a == "231" or a == "312" 136 | ) 137 | 138 | frozen_a = st.fold_right("", lambda n, acc: acc + str(n)) 139 | assert ( 140 | frozen_a == "321" 141 | or frozen_a == "123" 142 | or frozen_a == "132" 143 | or frozen_a == "213" 144 | or frozen_a == "231" 145 | or frozen_a == "312" 146 | ) 147 | 148 | 149 | def test_forall_gt_zero(): 150 | s = {1, 2, 3} 151 | st = frozenset(s) 152 | assert s.forall(lambda n: n > 0) == True 153 | assert st.forall(lambda n: n > 0) == True 154 | 155 | 156 | def test_forall_gt_two(): 157 | s = {1, 2, 3} 158 | st = frozenset(s) 159 | assert s.forall(lambda n: n > 2) == False 160 | assert st.forall(lambda n: n > 2) == False 161 | 162 | 163 | def test_length(): 164 | s = {1, 2, 3} 165 | st = frozenset(s) 166 | assert s.length() == 3 167 | assert st.length() == 3 168 | 169 | 170 | def test_length_equal_size(): 171 | s = {1, 2, 3} 172 | st = frozenset(s) 173 | assert s.size() == s.length() 174 | assert st.size() == st.length() 175 | 176 | 177 | def test_to_iterator(): 178 | s = {1, 2, 3} 179 | st = frozenset(s) 180 | sbis = s.copy() 181 | 182 | it = s.to_iterator() 183 | sit = st.to_iterator() 184 | 185 | n = next(it) 186 | ns = next(sit) 187 | assert n in s 188 | assert ns in st 189 | 190 | remaining = set(it) 191 | remainings = set(sit) 192 | s.remove(n) 193 | sbis.remove(ns) 194 | assert remaining == s 195 | assert remainings == sbis 196 | 197 | 198 | # Parallel operations 199 | 200 | 201 | def test_par_map(): 202 | s = {1, 2, 3} 203 | st = frozenset(s) 204 | assert s.par_map(lambda x: x * 2) == {2, 4, 6} 205 | assert st.par_map(lambda x: x * 2) == frozenset({2, 4, 6}) 206 | 207 | 208 | def test_par_filter(): 209 | s = {1, 2, 3} 210 | st = frozenset(s) 211 | assert s.par_filter(lambda x: x >= 2) == {2, 3} 212 | assert st.par_filter(lambda x: x >= 2) == frozenset({2, 3}) 213 | 214 | 215 | def test_par_filter_not(): 216 | s = {1, 2, 3} 217 | st = frozenset(s) 218 | assert s.par_filter_not(lambda x: x < 2) == {2, 3} 219 | assert st.par_filter_not(lambda x: x < 2) == frozenset({2, 3}) 220 | 221 | 222 | def test_par_flat_map(): 223 | s = {1, 2, 3} 224 | st = frozenset(s) 225 | assert s.par_flat_map(lambda x: [x**2]) == {1, 4, 9} 226 | assert st.par_flat_map(lambda x: {x**2}) == frozenset({1, 4, 9}) 227 | 228 | 229 | # Pure operations 230 | 231 | 232 | def test_pure_map(): 233 | s = {1, 2, 3} 234 | st = frozenset(s) 235 | assert s.pure_map(lambda x: x * 2) == {2, 4, 6} 236 | assert st.pure_map(lambda x: x * 2) == {2, 4, 6} 237 | 238 | 239 | def test_pure_flat_map(): 240 | s = {1, 2, 3} 241 | st = frozenset(s) 242 | assert s.pure_flat_map(lambda x: [x**2]) == {1, 4, 9} 243 | assert st.pure_flat_map(lambda x: [x**2]) == {1, 4, 9} 244 | 245 | 246 | def test_pure_filter(): 247 | s = {1, 2, 3} 248 | st = frozenset(s) 249 | assert s.pure_filter(lambda x: x >= 2) == {2, 3} 250 | assert st.pure_filter(lambda x: x >= 2) == {2, 3} 251 | 252 | 253 | def test_pure_filter_not(): 254 | s = {1, 2, 3} 255 | st = frozenset(s) 256 | assert s.pure_filter_not(lambda x: x >= 2) == {1} 257 | assert st.pure_filter_not(lambda x: x >= 2) == {1} 258 | 259 | 260 | # Lazy operations 261 | 262 | 263 | def test_lazy_map(): 264 | s = {1, 2, 3} 265 | st = frozenset(s) 266 | res = s.lazy_map(lambda x: x * 2) 267 | 268 | assert next(res) == 2 269 | assert set(res) == {4, 6} 270 | 271 | 272 | def test_lazy_flat_map(): 273 | s = {1, 2, 3} 274 | st = frozenset(s) 275 | res = s.lazy_flat_map(lambda x: [x * 2]) 276 | 277 | assert next(res) == 2 278 | assert set(res) == {4, 6} 279 | 280 | 281 | def test_lazy_filter(): 282 | s = {1, 2, 3} 283 | st = frozenset(s) 284 | res = s.lazy_filter(lambda x: x >= 2) 285 | 286 | assert next(res) == 2 287 | assert set(res) == {3} 288 | 289 | 290 | def test_lazy_filter_not(): 291 | s = {1, 2, 3} 292 | st = frozenset(s) 293 | res = s.lazy_filter_not(lambda x: x < 2) 294 | assert next(res) == 2 295 | assert set(res) == {3} 296 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | dask==2023.3.1 2 | forbiddenfruit==0.1.4 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | 4 | [wheel] 5 | universal = 0 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="pyfuncol", 5 | description="Functional collections extension functions for Python", 6 | long_description=open("README.md").read(), 7 | long_description_content_type="text/markdown", 8 | url="https://github.com/didactic-meme/pyfuncol", 9 | author="Andrea Veneziano", 10 | author_email="andrea.veneziano@icloud.com", 11 | maintainer="Andrea Veneziano", 12 | maintainer_email="andrea.veneziano@icloud.com", 13 | license="MIT", 14 | keywords="functional pipeline data collection chain parallel", 15 | packages=find_packages(exclude=["contrib", "docs", "*tests*", "test"]), 16 | version="1.3.1", 17 | install_requires=["forbiddenfruit", "dask"], 18 | extras_requires={ 19 | "dev": [ 20 | "pytest", 21 | "pytest-cov", 22 | "myst-parser", 23 | "black", 24 | "Sphinx", 25 | "sphinx-rtd-theme", 26 | ] 27 | }, 28 | classifiers=[ 29 | "Development Status :: 4 - Beta", 30 | "Intended Audience :: Developers", 31 | "License :: OSI Approved :: MIT License", 32 | "Programming Language :: Python :: 3.6", 33 | "Programming Language :: Python :: 3.7", 34 | "Programming Language :: Python :: 3.8", 35 | "Programming Language :: Python :: 3.9", 36 | "Programming Language :: Python :: Implementation :: CPython", 37 | "Natural Language :: English", 38 | "Operating System :: OS Independent", 39 | "Topic :: Software Development :: Libraries :: Python Modules", 40 | ], 41 | ) 42 | --------------------------------------------------------------------------------