├── tests ├── __init__.py ├── reference_test.py └── pipe_test.py ├── docs ├── index.md ├── images │ ├── logo.jpg │ ├── logo.png │ └── favicon.ico ├── stylesheets │ └── extra.css ├── similar-tools.md └── reference.md ├── .gitignore ├── Makefile ├── LICENSE ├── mkdocs.yml ├── .pre-commit-config.yaml ├── .github └── workflows │ └── ci.yml ├── pyproject.toml ├── README.md └── pipe21.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.ipynb 2 | site 3 | -------------------------------------------------------------------------------- /docs/images/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tandav/pipe21/HEAD/docs/images/logo.jpg -------------------------------------------------------------------------------- /docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tandav/pipe21/HEAD/docs/images/logo.png -------------------------------------------------------------------------------- /docs/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tandav/pipe21/HEAD/docs/images/favicon.ico -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: 3 | pytest 4 | python -m doctest docs/reference.md 5 | 6 | .PHONY: coverage 7 | coverage: 8 | pytest --cov=pipe21 --cov-report=html 9 | open htmlcov/index.html 10 | 11 | .PHONY: bumpver 12 | bumpver: 13 | # usage: make bumpver PART=minor 14 | bumpver update --no-fetch --$(PART) 15 | 16 | .PHONY: mkdocs 17 | mkdocs: 18 | mkdocs build 19 | mkdocs serve 20 | -------------------------------------------------------------------------------- /tests/reference_test.py: -------------------------------------------------------------------------------- 1 | import re 2 | from pathlib import Path 3 | 4 | import pipe21 as P 5 | 6 | 7 | def all_ops(): 8 | out = [] 9 | for k, v in vars(P).items(): 10 | if not isinstance(v, type): 11 | continue 12 | if not issubclass(v, P.B): 13 | continue 14 | if v is P.B: 15 | continue 16 | out.append(k) 17 | return out 18 | 19 | 20 | def test_all_operators_have_reference_docs(): 21 | reference = Path('docs/reference.md').read_text() 22 | assert re.findall(r'## (\w+)', reference) == all_ops() 23 | -------------------------------------------------------------------------------- /docs/stylesheets/extra.css: -------------------------------------------------------------------------------- 1 | code { 2 | border-radius: 20px!important; 3 | } 4 | 5 | :root > * { 6 | --md-text-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 7 | --md-code-font: "Menlo", 'SF Mono', "Monaco", "Roboto Mono", "Consolas", "Liberation Mono", "Courier New", monospace; 8 | --md-primary-fg-color:#366355; 9 | --md-accent-fg-color: #72D1B2; 10 | } 11 | 12 | .md-typeset h1, .md-typeset h2, .md-typeset h3, .md-typeset h4, .md-typeset h5, .md-typeset h6 { 13 | font-weight: 500; 14 | } 15 | 16 | .md-nav__source { 17 | background-color: var(--md-primary-fg-color); 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Alexander Rodionov 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 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: pipe21 2 | site_url: https://tandav.github.io/pipe21 3 | repo_url: https://github.com/tandav/pipe21 4 | repo_name: tandav/pipe21 5 | edit_uri: edit/master/docs/ 6 | extra_css: 7 | - stylesheets/extra.css 8 | theme: 9 | name: material 10 | favicon: images/favicon.ico 11 | logo: images/logo.png 12 | features: 13 | - navigation.instant 14 | - content.action.edit 15 | - content.action.view 16 | - content.code.copy 17 | palette: 18 | # Palette toggle for light mode 19 | - media: "(prefers-color-scheme: light)" 20 | scheme: default 21 | primary: custom 22 | accent: teal 23 | toggle: 24 | icon: material/brightness-7 25 | name: Switch to dark mode 26 | 27 | # Palette toggle for dark mode 28 | - media: "(prefers-color-scheme: dark)" 29 | scheme: slate 30 | primary: custom 31 | accent: custom 32 | toggle: 33 | icon: material/brightness-4 34 | name: Switch to system preference 35 | markdown_extensions: 36 | - pymdownx.highlight: 37 | anchor_linenums: true 38 | line_spans: __span 39 | pygments_lang_class: true 40 | - pymdownx.inlinehilite 41 | - pymdownx.snippets 42 | - pymdownx.superfences 43 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | autofix_prs: false 3 | autoupdate_schedule: quarterly 4 | repos: 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: v4.6.0 7 | hooks: 8 | - id: check-added-large-files 9 | - id: check-yaml 10 | - id: check-json 11 | - id: check-ast 12 | - id: check-byte-order-marker 13 | - id: check-builtin-literals 14 | - id: check-case-conflict 15 | - id: check-docstring-first 16 | - id: debug-statements 17 | - id: end-of-file-fixer 18 | - id: mixed-line-ending 19 | - id: trailing-whitespace 20 | - id: check-merge-conflict 21 | - id: detect-private-key 22 | - id: double-quote-string-fixer 23 | - id: name-tests-test 24 | - id: requirements-txt-fixer 25 | 26 | - repo: https://github.com/asottile/add-trailing-comma 27 | rev: v3.1.0 28 | hooks: 29 | - id: add-trailing-comma 30 | 31 | - repo: https://github.com/asottile/pyupgrade 32 | rev: v3.15.2 33 | hooks: 34 | - id: pyupgrade 35 | args: [--py311-plus] 36 | 37 | - repo: https://github.com/charliermarsh/ruff-pre-commit 38 | rev: v0.3.7 39 | hooks: 40 | - id: ruff 41 | args: [--fix, --exit-non-zero-on-fix] 42 | 43 | - repo: https://github.com/PyCQA/pylint 44 | rev: v3.1.0 45 | hooks: 46 | - id: pylint 47 | additional_dependencies: ["pylint-per-file-ignores"] 48 | 49 | # - repo: https://github.com/hhatto/autopep8 50 | # rev: v2.0.4 51 | # hooks: 52 | # - id: autopep8 53 | 54 | - repo: https://github.com/PyCQA/flake8 55 | rev: 7.0.0 56 | hooks: 57 | - id: flake8 58 | additional_dependencies: [Flake8-pyproject] 59 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: push 4 | 5 | permissions: 6 | contents: write 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | env: 12 | MINIMUM_COVERAGE_PERCENTAGE: 100 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | python_version: ['3.8', '3.9', '3.10', '3.11', '3.12'] 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ matrix.python_version }} 22 | 23 | - name: install dependencies 24 | run: python3 -m pip install .[dev] 25 | 26 | # - name: reformat # neccessary to fix 100% coverage 27 | # run: | 28 | # autopep8 --in-place --aggressive --aggressive pipe21.py 29 | # black pipe21.py 30 | 31 | - name: test 32 | # run: pytest --cov pipe21 --cov-fail-under=${{ env.MINIMUM_COVERAGE_PERCENTAGE }} 33 | run: pytest 34 | 35 | - name: doctest 36 | run: python3 -m doctest docs/reference.md 37 | 38 | - name: pre-commit 39 | run: pre-commit run --all-files 40 | 41 | # - name: Upload coverage data to coveralls.io 42 | # run: coveralls --service=github 43 | # env: 44 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | # COVERALLS_FLAG_NAME: ${{ matrix.python_version }} 46 | # COVERALLS_PARALLEL: true 47 | 48 | # coveralls: 49 | # name: Indicate completion to coveralls.io 50 | # needs: test 51 | # runs-on: ubuntu-latest 52 | # container: python:3-slim 53 | # steps: 54 | # - name: Finished 55 | # run: | 56 | # pip3 install --upgrade coveralls 57 | # coveralls --service=github --finish 58 | # env: 59 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | 61 | publish-to-pypi-and-github-release: 62 | if: "startsWith(github.ref, 'refs/tags/')" 63 | runs-on: ubuntu-latest 64 | needs: test 65 | steps: 66 | - uses: actions/checkout@master 67 | - name: Set up Python 68 | uses: actions/setup-python@v5 69 | with: 70 | python-version: '3.12' 71 | 72 | - name: Install pypa/build 73 | run: python -m pip install --upgrade setuptools build twine 74 | 75 | - name: Build a source tarball and wheel 76 | run: python -m build . 77 | 78 | - name: Upload to PyPI 79 | env: 80 | TWINE_USERNAME: __token__ 81 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 82 | run: python -m twine upload dist/* 83 | 84 | - name: Github Release 85 | uses: softprops/action-gh-release@v2 86 | 87 | mkdocs-github-pages: 88 | if: "!startsWith(github.ref, 'refs/tags/')" 89 | needs: test 90 | runs-on: ubuntu-latest 91 | steps: 92 | - uses: actions/checkout@v4 93 | - uses: actions/setup-python@v5 94 | with: 95 | python-version: 3.12 96 | - uses: actions/cache@v4 97 | with: 98 | key: ${{ github.ref }} 99 | path: .cache 100 | - run: pip install mkdocs-material 101 | - run: mkdocs gh-deploy --force 102 | -------------------------------------------------------------------------------- /docs/similar-tools.md: -------------------------------------------------------------------------------- 1 | # similar tools 2 | - [pytoolz/toolz: A functional standard library for Python.](https://github.com/pytoolz/toolz/) 3 | - [Арсений Сапелкин — Функциональное программирование в Python: ежедневные рецепты - YouTube](https://www.youtube.com/watch?v=eFkp1e4ex5s) 4 | - [more-itertools/more-itertools: More routines for operating on iterables, beyond itertools](https://github.com/more-itertools/more-itertools) 5 | - [nekitdev/iters.py: Rich Iterators for Python.](https://github.com/nekitdev/iters.py) 6 | - [R adds native pipe and lambda syntax | Hacker News](https://news.ycombinator.com/item?id=25316608) 7 | - [mpypl - Google Search](https://www.google.com/search?q=mpypl&oq=mpypl&aqs=chrome..69i57j0l6j69i60.1343j0j7&sourceid=chrome&ie=UTF-8) 8 | - [Дмитрий Сошников — mPyPl: функциональный способ организовать обработку данных в Python - YouTube](https://www.youtube.com/watch?v=UO2NBluBG_g) 9 | - [JulienPalard/Pipe: A Python library to use infix notation in Python](https://github.com/JulienPalard/Pipe) 10 | - [Pydash: A Kitchen Sink of Missing Python Utilities | by Khuyen Tran | Towards Data Science](https://towardsdatascience.com/pydash-a-bucket-of-missing-python-utilities-5d10365be4fc) 11 | - [Write Clean Python Code Using Pipes | by Khuyen Tran | Oct, 2021 | Towards Data Science](https://towardsdatascience.com/write-clean-python-code-using-pipes-1239a0f3abf5) 12 | - [A trick to have arbitrary infix operators in Python | Hacker News](https://news.ycombinator.com/item?id=30057048) 13 | - [PySpark - RDD API](https://spark.apache.org/docs/latest/api/python/reference/pyspark.html#rdd-apis) 14 | - [altimin/fcollections](https://github.com/altimin/fcollections) 15 | - [InvestmentSystems/function-pipe: Tools for extended function composition and pipelines in Python](https://github.com/InvestmentSystems/function-pipe) 16 | - [kachayev/fn.py: Functional programming in Python: implementation of missing features to enjoy FP](https://github.com/kachayev/fn.py) 17 | - [man-group/mdf: Data-flow programming toolkit for Python](https://github.com/man-group/mdf) 18 | - [0101/pipetools: Functional plumbing for Python](https://github.com/0101/pipetools) 19 | - [EntilZha/PyFunctional: Python library for creating data pipelines with chain functional programming](https://github.com/EntilZha/PyFunctional) 20 | - [jasondelaat/pymonad: ](https://github.com/jasondelaat/pymonad) 21 | - [machow/siuba: Python library for using dplyr like syntax with pandas and SQL](https://github.com/machow/siuba) 22 | - [pipeop · PyPI](https://pypi.org/project/pipeop/) 23 | - [Suor/funcy: A fancy and practical functional tools](https://github.com/Suor/funcy) 24 | - [igrishaev/f: Functional stuff for Python](https://github.com/igrishaev/f) 25 | - [Language Integrated Query (LINQ) in C# | Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/csharp/linq/) 26 | - [chain-ops-python: Simple chaining of operations (a.k.a. pipe operator) in python](https://sr.ht/~tpapastylianou/chain-ops-python/) 27 | - [sammyrulez/typed-monads: Easy functional monads for your python code](https://github.com/sammyrulez/typed-monads) 28 | - [PRQL/prql: PRQL is a modern language for transforming data — a simple, powerful, pipelined SQL replacement](https://github.com/PRQL/prql) 29 | - [viralogic/py-enumerable: A Python module used for interacting with collections of objects using LINQ syntax](https://github.com/viralogic/py-enumerable)- [jmfernandes/pyLINQ: a simple and easy way to filter and sort lists.](https://github.com/jmfernandes/pyLINQ) 30 | - [evhub/coconut: Simple, elegant, Pythonic functional programming.](https://github.com/evhub/coconut) 31 | - [petl-developers/petl: Python Extract Transform and Load Tables of Data](https://github.com/petl-developers/petl) 32 | - [Show HN: Pypipe – A Python command-line tool for pipeline processing | Hacker News](https://news.ycombinator.com/item?id=37981683) 33 | - [Marcel the Shell | Hacker News](https://news.ycombinator.com/item?id=37991746) 34 | - [cgarciae/pypeln: Concurrent data pipelines in Python >>>](https://github.com/cgarciae/pypeln) 35 | - [sfermigier/awesome-functional-python: A curated list of awesome things related to functional programming in Python.](https://github.com/sfermigier/awesome-functional-python) 36 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "pipe21" 3 | version = "1.23.0" 4 | authors = [ 5 | {name = "Alexander Rodionov", email = "tandav@tandav.me"}, 6 | ] 7 | description = "simple functional pipes" 8 | readme = "README.md" 9 | requires-python = ">=3.8" 10 | dependencies = [] 11 | 12 | [project.optional-dependencies] 13 | dev = [ 14 | "bumpver", 15 | # "black", 16 | # "autopep8", 17 | "pre-commit", 18 | "hypothesis", 19 | "pytest", 20 | # "pytest-cov", 21 | # "coveralls", 22 | "mkdocs", 23 | "mkdocs-material", 24 | ] 25 | 26 | [project.urls] 27 | source = "https://github.com/tandav/pipe21" 28 | docs = "https://tandav.github.io/pipe21/" 29 | issues = "https://github.com/tandav/pipe21/issues" 30 | "release notes" = "https://github.com/tandav/pipe21/releases" 31 | 32 | # ============================================================================== 33 | 34 | [build-system] 35 | requires = ["setuptools"] 36 | build-backend = "setuptools.build_meta" 37 | 38 | # ============================================================================== 39 | 40 | [tool.bumpver] 41 | current_version = "v1.23.0" 42 | version_pattern = "vMAJOR.MINOR.PATCH" 43 | commit_message = "bump version {old_version} -> {new_version}" 44 | commit = true 45 | tag = true 46 | 47 | [tool.bumpver.file_patterns] 48 | "pyproject.toml" = [ 49 | '^version = "{pep440_version}"', 50 | '^current_version = "{version}"', 51 | ] 52 | "pipe21.py" = [ 53 | "^__version__ = '{pep440_version}'", 54 | ] 55 | 56 | # ============================================================================== 57 | 58 | [tool.mypy] 59 | # todo: review this 60 | pretty = true 61 | show_traceback = true 62 | color_output = true 63 | allow_redefinition = false 64 | check_untyped_defs = true 65 | disallow_any_generics = true 66 | disallow_incomplete_defs = true 67 | disallow_untyped_defs = true 68 | ignore_missing_imports = true 69 | implicit_reexport = false 70 | no_implicit_optional = true 71 | show_column_numbers = true 72 | show_error_codes = true 73 | show_error_context = true 74 | strict_equality = true 75 | strict_optional = true 76 | warn_no_return = true 77 | warn_redundant_casts = true 78 | warn_return_any = true 79 | warn_unreachable = true 80 | warn_unused_configs = true 81 | warn_unused_ignores = true 82 | 83 | [[tool.mypy.overrides]] 84 | module = ["tests.*"] 85 | disallow_untyped_defs = false 86 | 87 | # ============================================================================== 88 | 89 | [tool.ruff] 90 | select = ["ALL"] 91 | ignore = [ 92 | "E501", # line too long 93 | "E731", # lambda assignment 94 | "E701", # multiple statements 95 | "E702", # multiple statements 96 | "F403", # star imports 97 | "F405", # star imports 98 | "B008", # function-call-in-default-argument 99 | "PLR0913", # too-many-arguments 100 | "TCH003", # typing-only-standard-library-import 101 | "ANN", # type annotations 102 | "D", #docstrings 103 | "Q", # quotes 104 | "ARG005", # Unused lambda argument 105 | "PTH123", # `open()` should be replaced by `Path.open()` 106 | "N812", # lowercase imported as non lowercase 107 | "SIM115", 108 | ] 109 | 110 | [tool.ruff.per-file-ignores] 111 | "examples/*" = ["INP001"] 112 | "tests/*" = [ 113 | "S101", 114 | "PLR2004", 115 | "PT001", 116 | ] 117 | 118 | [tool.ruff.isort] 119 | force-single-line = true 120 | 121 | # ============================================================================== 122 | 123 | [tool.pylint.MASTER] 124 | load-plugins=[ 125 | "pylint_per_file_ignores", 126 | ] 127 | 128 | [tool.pylint.messages-control] 129 | disable = [ 130 | "invalid-name", 131 | "missing-function-docstring", 132 | "missing-class-docstring", 133 | "missing-module-docstring", 134 | "unnecessary-lambda-assignment", 135 | "multiple-statements", 136 | "line-too-long", 137 | "unspecified-encoding", 138 | "wildcard-import", 139 | "unused-wildcard-import", 140 | "keyword-arg-before-vararg", 141 | "too-few-public-methods", 142 | "consider-using-with", 143 | ] 144 | 145 | [tool.pylint-per-file-ignores] 146 | "/tests/" = "import-error,redefined-outer-name" 147 | 148 | # ============================================================================== 149 | 150 | [tool.autopep8] 151 | ignore="E501,E701" 152 | recursive = true 153 | aggressive = 3 154 | 155 | # ============================================================================== 156 | 157 | [tool.flake8] 158 | ignore = ['F405', 'F403', 'E501', 'E701'] 159 | 160 | # ============================================================================== 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PyPI version](https://img.shields.io/pypi/v/pipe21.svg?logo=pypi&logoColor=FFE873)](https://pypi.org/project/pipe21/) 2 | [![Coverage Status](https://coveralls.io/repos/github/tandav/pipe21/badge.svg?branch=coveralls-bage)](https://coveralls.io/github/tandav/pipe21?branch=coveralls-bage) 3 | 4 | 5 | 6 | # pipe21 - simple functional pipes [[docs]](https://tandav.github.io/pipe21) 7 | 8 | ## Basic version 9 | just copy-paste it! 10 | 11 | Most frequently used operators. It's often easier to copypaste than install and import. 12 | 13 | ```py 14 | class B: 15 | def __init__(self, f): self.f = f 16 | class Pipe (B): __ror__ = lambda self, x: self.f(x) 17 | class Map (B): __ror__ = lambda self, x: map (self.f, x) 18 | class Filter(B): __ror__ = lambda self, x: filter(self.f, x) 19 | ``` 20 | 21 | or install using pip: 22 | 23 | ```py 24 | pip install pipe21 25 | ``` 26 | 27 | ## Examples 28 | 29 | #### little docs 30 | 31 | ```py 32 | from pipe21 import * 33 | 34 | x | Pipe(f) == f (x ) 35 | x | Map(f) == map (f, x) 36 | x | Filter(f) == filter(f, x) 37 | x | Reduce(f) == reduce(f, x) 38 | ``` 39 | 40 | --- 41 | 42 | #### simple pipes 43 | 44 | ```py 45 | range(5) | Pipe(list) # [0, 1, 2, 3, 4] 46 | range(5) | Map(str) | Pipe(''.join) # '01234' 47 | range(5) | Filter(lambda x: x % 2 == 0) | Pipe(list) # [0, 2, 4] 48 | range(5) | Reduce(lambda a, b: a + b) # 10 49 | ``` 50 | 51 | --- 52 | 53 | #### print digits 54 | 55 | ```py 56 | range(1_000_000) | Map(chr) | Filter(str.isdigit) | Pipe(''.join) 57 | ``` 58 | Output: 59 | 60 | ``` 61 | 0123456789²³¹٠١٢٣٤٥٦٧٨٩۰۱۲۳۴۵۶۷۸۹߀߁߂߃߄߅߆߇߈߉०१२३४५६७८९০১২৩৪৫৬৭৮৯੦੧੨੩੪੫੬੭੮੯૦૧૨૩૪૫૬૭૮૯୦୧୨୩୪୫୬୭୮୯௦௧௨௩௪௫௬௭௮௯౦౧౨౩౪౫౬౭౮౯೦೧೨೩೪೫೬೭೮೯൦൧൨൩൪൫൬൭൮൯෦෧෨෩෪෫෬෭෮෯๐๑๒๓๔๕๖๗๘๙໐໑໒໓໔໕໖໗໘໙༠༡༢༣༤༥༦༧༨༩၀၁၂၃၄၅၆၇၈၉႐႑႒႓႔႕႖႗႘႙፩፪፫፬፭፮፯፰፱០១២៣៤៥៦៧៨៩᠐᠑᠒᠓᠔᠕᠖᠗᠘᠙᥆᥇᥈᥉᥊᥋᥌᥍᥎᥏᧐᧑᧒᧓᧔᧕᧖᧗᧘᧙᧚᪀᪁᪂᪃᪄᪅᪆᪇᪈᪉᪐᪑᪒᪓᪔᪕᪖᪗᪘᪙᭐᭑᭒᭓᭔᭕᭖᭗᭘᭙᮰᮱᮲᮳᮴᮵᮶᮷᮸᮹᱀᱁᱂᱃᱄᱅᱆᱇᱈᱉᱐᱑᱒᱓᱔᱕᱖᱗᱘᱙⁰⁴⁵⁶⁷⁸⁹₀₁₂₃₄₅₆₇₈₉①②③④⑤⑥⑦⑧⑨⑴⑵⑶⑷⑸⑹⑺⑻⑼⒈⒉⒊⒋⒌⒍⒎⒏⒐⓪⓵⓶⓷⓸⓹⓺⓻⓼⓽⓿❶❷❸❹❺❻❼❽❾➀➁➂➃➄➅➆➇➈➊➋➌➍➎➏➐➑➒꘠꘡꘢꘣꘤꘥꘦꘧꘨꘩꣐꣑꣒꣓꣔꣕꣖꣗꣘꣙꤀꤁꤂꤃꤄꤅꤆꤇꤈꤉꧐꧑꧒꧓꧔꧕꧖꧗꧘꧙꧰꧱꧲꧳꧴꧵꧶꧷꧸꧹꩐꩑꩒꩓꩔꩕꩖꩗꩘꩙꯰꯱꯲꯳꯴꯵꯶꯷꯸꯹0123456789𐒠𐒡𐒢𐒣𐒤𐒥𐒦𐒧𐒨𐒩𐩀𐩁𐩂𐩃𐴰𐴱𐴲𐴳𐴴𐴵𐴶𐴷𐴸𐴹𐹠𐹡𐹢𐹣𐹤𐹥𐹦𐹧𐹨𑁒𑁓𑁔𑁕𑁖𑁗𑁘𑁙𑁚𑁦𑁧𑁨𑁩𑁪𑁫𑁬𑁭𑁮𑁯𑃰𑃱𑃲𑃳𑃴𑃵𑃶𑃷𑃸𑃹𑄶𑄷𑄸𑄹𑄺𑄻𑄼𑄽𑄾𑄿𑇐𑇑𑇒𑇓𑇔𑇕𑇖𑇗𑇘𑇙𑋰𑋱𑋲𑋳𑋴𑋵𑋶𑋷𑋸𑋹𑑐𑑑𑑒𑑓𑑔𑑕𑑖𑑗𑑘𑑙𑓐𑓑𑓒𑓓𑓔𑓕𑓖𑓗𑓘𑓙𑙐𑙑𑙒𑙓𑙔𑙕𑙖𑙗𑙘𑙙𑛀𑛁𑛂𑛃𑛄𑛅𑛆𑛇𑛈𑛉𑜰𑜱𑜲𑜳𑜴𑜵𑜶𑜷𑜸𑜹𑣠𑣡𑣢𑣣𑣤𑣥𑣦𑣧𑣨𑣩𑱐𑱑𑱒𑱓𑱔𑱕𑱖𑱗𑱘𑱙𑵐𑵑𑵒𑵓𑵔𑵕𑵖𑵗𑵘𑵙𑶠𑶡𑶢𑶣𑶤𑶥𑶦𑶧𑶨𑶩𖩠𖩡𖩢𖩣𖩤𖩥𖩦𖩧𖩨𖩩𖭐𖭑𖭒𖭓𖭔𖭕𖭖𖭗𖭘𖭙𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡𝟢𝟣𝟤𝟥𝟦𝟧𝟨𝟩𝟪𝟫𝟬𝟭𝟮𝟯𝟰𝟱𝟲𝟳𝟴𝟵𝟶𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾𝟿𞅀𞅁𞅂𞅃𞅄𞅅𞅆𞅇𞅈𞅉𞋰𞋱𞋲𞋳𞋴𞋵𞋶𞋷𞋸𞋹𞥐𞥑𞥒𞥓𞥔𞥕𞥖𞥗𞥘𞥙🄀🄁🄂🄃🄄🄅🄆🄇🄈🄉🄊' 62 | ``` 63 | 64 | #### chunked 65 | 66 | ```py 67 | >>> range(5) | Chunked(2) | Pipe(list) 68 | [(0, 1), (2, 3), (4,)] 69 | ``` 70 | 71 | --- 72 | 73 | ## Extended version 74 | ```py 75 | import pipe21 as P 76 | ``` 77 | 78 | #### FizzBuzz 79 | 80 | ```py 81 | ( 82 | range(1, 100) 83 | | P.MapSwitch([ 84 | (lambda i: i % 3 == i % 5 == 0, lambda x: 'FizzBuzz'), 85 | (lambda i: i % 3 == 0, lambda x: 'Fizz'), 86 | (lambda i: i % 5 == 0, lambda x: 'Buzz'), 87 | ]) 88 | | P.Pipe(list) 89 | ) 90 | 91 | [1, 2, 'Fizz', 4, 'Buzz', 'Fizz', 7, 8, 'Fizz', 'Buzz', 11, 'Fizz', 13, 14, 'FizzBuzz', 16, 17, 'Fizz', 19, 'Buzz', 'Fizz', 22, 23, 'Fizz', 'Buzz', 26, 'Fizz', 28, 29, 'FizzBuzz', 31, 32, 'Fizz', 34, 'Buzz', 'Fizz', 37, 38, 'Fizz', 'Buzz', 41, 'Fizz', 43, 44, 'FizzBuzz', 46, 47, 'Fizz', 49, 'Buzz', 'Fizz', 52, 53, 'Fizz', 'Buzz', 56, 'Fizz', 58, 59, 'FizzBuzz', 61, 62, 'Fizz', 64, 'Buzz', 'Fizz', 67, 68, 'Fizz', 'Buzz', 71, 'Fizz', 73, 74, 'FizzBuzz', 76, 77, 'Fizz', 79, 'Buzz', 'Fizz', 82, 83, 'Fizz', 'Buzz', 86, 'Fizz', 88, 89, 'FizzBuzz', 91, 92, 'Fizz', 94, 'Buzz', 'Fizz', 97, 98, 'Fizz'] 92 | ``` 93 | 94 | --- 95 | 96 | #### play random music from youtube links in markdown files: 97 | 98 | ```py 99 | import pathlib 100 | import random 101 | import itertools 102 | import re 103 | import operator 104 | import webbrowser 105 | import pipe21 as P 106 | 107 | 108 | ( 109 | pathlib.Path.home() / 'docs/knowledge/music' # take a directory 110 | | P.MethodCaller('rglob', '*.md') # find all markdown files 111 | | P.FlatMap(lambda p: p | P.IterLines()) # read all lines from all files and flatten into a single iterable 112 | | P.FlatMap(lambda l: re.findall(r'\[(.+)\]\((.+)\)', l)) # keep only lines with a markdown link 113 | | P.Map(operator.itemgetter(1)) # extract a link 114 | | P.Pipe(list) # convert iterable of links into a list 115 | | P.Pipe(random.choice) # choose random link 116 | | P.Pipe(webbrowser.open) # open link in browser 117 | ) 118 | ``` 119 | 120 | --- 121 | 122 | - [all available methods reference](reference.md) 123 | - [review of similar tools / alternatives](similar-tools.md) 124 | - written in pure python, no dependencies 125 | -------------------------------------------------------------------------------- /pipe21.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import itertools 3 | import operator 4 | import re 5 | import sys 6 | 7 | __version__ = '1.23.0' 8 | 9 | 10 | class B: 11 | def __init__(self, f=None, *args, **kw): 12 | self.f = f 13 | self.args = args 14 | self.kw = kw 15 | 16 | 17 | class Pipe (B): __ror__ = lambda self, x: self.f(x, *self.args, **self.kw) 18 | class Map (B): __ror__ = lambda self, it: (self.f(x) for x in it) 19 | class Filter (B): __ror__ = lambda self, it: (x for x in it if self.f(x)) 20 | class Reduce (B): __ror__ = lambda self, it: functools.reduce(self.f, it, *self.args) 21 | class MapKeys (B): __ror__ = lambda self, it: ((self.f(k), v) for k, v in it) 22 | class MapValues (B): __ror__ = lambda self, it: ((k, self.f(v)) for k, v in it) 23 | class FilterFalse (B): __ror__ = lambda self, it: itertools.filterfalse(self.f, it) 24 | class FilterKeys (B): __ror__ = lambda self, it: (kv for kv in it if (self.f or bool)(kv[0])) 25 | class FilterValues (B): __ror__ = lambda self, it: (kv for kv in it if (self.f or bool)(kv[1])) 26 | class FlatMap (B): __ror__ = lambda self, it: itertools.chain.from_iterable(self.f(x) for x in it) 27 | class FlatMapValues(B): __ror__ = lambda self, it: ((k, v) for k, vs in it for v in self.f(vs)) 28 | class KeyBy (B): __ror__ = lambda self, it: ((self.f(x), x) for x in it) 29 | class ValueBy (B): __ror__ = lambda self, it: ((x, self.f(x)) for x in it) 30 | class Append (B): __ror__ = lambda self, it: ((*x, self.f(x)) for x in it) 31 | class Keys (B): __ror__ = lambda self, it: (k for k, v in it) 32 | class Values (B): __ror__ = lambda self, it: (v for k, v in it) 33 | class SwapKV (B): __ror__ = lambda self, it: it | Map(operator.itemgetter(1, 0)) 34 | class Grep (B): __ror__ = lambda self, it: it | (FilterFalse if self.kw.get('v', False) else Filter)(re.compile(self.f, flags=re.IGNORECASE if self.kw.get('i', False) else 0).search) 35 | class IterLines (B): __ror__ = lambda self, f: (x.strip() if self.kw.get('strip', True) else x for x in open(f)) 36 | class Count (B): __ror__ = lambda self, it: sum(1 for _ in it) 37 | class Slice (B): __ror__ = lambda self, it: itertools.islice(it, self.f, *self.args) 38 | class Take (B): __ror__ = lambda self, it: it | Slice(self.f) | Pipe(list) 39 | class Sorted (B): __ror__ = lambda self, it: sorted(it, **self.kw) 40 | class GroupBy (B): __ror__ = lambda self, it: itertools.groupby(sorted(it, key=self.f), key=self.f) 41 | class ReduceByKey (B): __ror__ = lambda self, it: it | GroupBy(lambda kv: kv[0]) | MapValues(lambda kv: kv | Values() | Reduce(self.f)) | Pipe(list) 42 | class Apply (B): __ror__ = lambda self, x: x | Exec(self.f, x) 43 | class StarPipe (B): __ror__ = lambda self, x: self.f(*x) 44 | class StarMap (B): __ror__ = lambda self, x: itertools.starmap(self.f, x) 45 | class StarFlatMap (B): __ror__ = lambda self, x: itertools.starmap(self.f, x) | Pipe(itertools.chain.from_iterable) 46 | class MapApply (B): __ror__ = lambda self, it: (x | Apply(self.f) for x in it) 47 | class Switch (B): __ror__ = lambda self, x: next((v(x) for k, v in self.f if k(x)), x) 48 | class MapSwitch (B): __ror__ = lambda self, it: (x | Switch(self.f) for x in it) 49 | class YieldIf (B): __ror__ = lambda self, it: ((self.f or (lambda y: y))(x) for x in it if self.kw.get('key', bool)(x)) 50 | class Join (B): __ror__ = lambda self, it: it | FlatMap(lambda x: ((x, y) for y in self.f if self.kw.get('key', operator.eq)(x, y))) 51 | 52 | 53 | class GetItem (B): __ror__ = lambda self, x: operator.getitem(x, self.f) 54 | class SetItem (B): __ror__ = lambda self, x: x | Exec(operator.setitem, x, self.f, self.args[0]) 55 | class DelItem (B): __ror__ = lambda self, x: x | Exec(operator.delitem, x, self.f) 56 | class GetAttr (B): __ror__ = lambda self, x: getattr(x, self.f) 57 | class SetAttr (B): __ror__ = lambda self, x: x | Exec(setattr, x, self.f, self.args[0]) 58 | class DelAttr (B): __ror__ = lambda self, x: x | Exec(delattr, x, self.f) 59 | class MapGetItem (B): __ror__ = lambda self, it: (kv | GetItem(self.f) for kv in it) 60 | class MapSetItem (B): __ror__ = lambda self, it: (kv | SetItem(self.f, self.args[0]) for kv in it) 61 | class MapDelItem (B): __ror__ = lambda self, it: (kv | DelItem(self.f) for kv in it) 62 | class MapGetAttr (B): __ror__ = lambda self, it: (kv | GetAttr(self.f) for kv in it) 63 | class MapSetAttr (B): __ror__ = lambda self, it: (kv | SetAttr(self.f, self.args[0]) for kv in it) 64 | class MapDelAttr (B): __ror__ = lambda self, it: (kv | DelAttr(self.f) for kv in it) 65 | class MethodCaller (B): __ror__ = lambda self, x: operator.methodcaller(self.f, *self.args, **self.kw)(x) 66 | class MapMethodCaller(B): __ror__ = lambda self, it: (x | MethodCaller(self.f, *self.args, **self.kw) for x in it) 67 | 68 | 69 | class Unique(B): 70 | def __ror__(self, it): 71 | key = self.f or (lambda x: x) 72 | seen = set() 73 | for item in it: 74 | k = key(item) 75 | if k in seen: 76 | continue 77 | seen.add(k) 78 | yield item 79 | 80 | 81 | class Exec(B): 82 | def __ror__(self, x): 83 | self.f(*self.args, **self.kw) 84 | return x 85 | 86 | 87 | if sys.version_info >= (3, 12): # pragma: no cover 88 | class Chunked(B): __ror__ = lambda self, it: itertools.batched(it, self.f) # pylint: disable=no-member 89 | else: # pragma: no cover 90 | class Chunked(B): __ror__ = lambda self, it: iter(functools.partial(lambda n, i: tuple(i | Take(n)), self.f, iter(it)), ()) 91 | -------------------------------------------------------------------------------- /docs/reference.md: -------------------------------------------------------------------------------- 1 | # reference 2 | 3 | ```py 4 | >>> from pipe21 import * 5 | 6 | ``` 7 | 8 | ## Pipe 9 | Put a value into a function as 1st argument 10 | 11 | ```py 12 | >>> range(5) | Pipe(list) 13 | [0, 1, 2, 3, 4] 14 | 15 | >>> 2 | Pipe(pow, 8) 16 | 256 17 | 18 | >>> 'FF' | Pipe(int, base=16) 19 | 255 20 | 21 | >>> b'\x02\x00' | Pipe(int.from_bytes, byteorder='big') 22 | 512 23 | 24 | >>> 'ab' | Pipe(enumerate, start=0) | Pipe(list) 25 | [(0, 'a'), (1, 'b')] 26 | 27 | >>> import math 28 | >>> 5.01 | Pipe(math.isclose, 5, abs_tol=0.01) 29 | True 30 | 31 | >>> import random 32 | >>> random.seed(44) 33 | >>> [0, 1, 2] | Pipe(random.choices, [0.8, 0.15, 0.05], k=20) 34 | [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 1, 0, 0] 35 | 36 | >>> import itertools 37 | >>> [0, 1, 2] | Pipe(itertools.zip_longest, 'ab', fillvalue=None) | Pipe(list) 38 | [(0, 'a'), (1, 'b'), (2, None)] 39 | 40 | >>> import operator 41 | >>> [0, 1, 2] | Pipe(itertools.accumulate, operator.add, initial=100) | Pipe(list) 42 | [100, 100, 101, 103] 43 | 44 | ``` 45 | 46 | ## Map 47 | 48 | ```py 49 | >>> range(5) | Map(str) | Pipe(''.join) 50 | '01234' 51 | 52 | ``` 53 | 54 | ## Filter 55 | 56 | ```py 57 | >>> range(5) | Filter(lambda x: x % 2 == 0) | Pipe(list) 58 | [0, 2, 4] 59 | 60 | ``` 61 | 62 | ## Reduce 63 | 64 | ```py 65 | >>> import operator 66 | >>> range(5) | Reduce(operator.add) 67 | 10 68 | 69 | >>> range(5) | Reduce(operator.add, 5) # with initial value 70 | 15 71 | 72 | >>> [{1, 2}, {2, 3, 4}, {4, 5}] | Reduce(operator.or_) 73 | {1, 2, 3, 4, 5} 74 | 75 | ``` 76 | 77 | ## MapKeys 78 | 79 | ```py 80 | >>> [(1, 10), (2, 20)] | MapKeys(str) | Pipe(list) 81 | [('1', 10), ('2', 20)] 82 | 83 | ``` 84 | 85 | ## MapValues 86 | 87 | ```py 88 | >>> [(1, 10), (2, 20)] | MapValues(str) | Pipe(list) 89 | [(1, '10'), (2, '20')] 90 | 91 | ``` 92 | 93 | ## FilterFalse 94 | Same as `Filter` but negative 95 | 96 | ```py 97 | >>> range(5) | FilterFalse(lambda x: x % 2 == 0) | Pipe(list) 98 | [1, 3] 99 | 100 | ``` 101 | 102 | ## FilterKeys 103 | 104 | Take `(k, v)` pairs iterable and keep only elements for which `predicate(k) == True`. If no predicate function is provided - default function `bool` will be used. 105 | 106 | ```py 107 | >>> [(0, 2), (3, 0)] | FilterKeys() | Pipe(list) 108 | [(3, 0)] 109 | 110 | >>> [(0, 2), (3, 0)] | FilterKeys(lambda x: x % 2 == 0) | Pipe(list) 111 | [(0, 2)] 112 | 113 | ``` 114 | 115 | ## FilterValues 116 | 117 | Same as `FilterKeys` but for `v` in `(k, v)` pairs 118 | 119 | ```py 120 | >>> [(0, 2), (3, 0)] | FilterValues() | Pipe(list) 121 | [(0, 2)] 122 | 123 | >>> [(0, 2), (3, 0)] | FilterValues(lambda x: x % 2 == 0) | Pipe(list) 124 | [(0, 2), (3, 0)] 125 | 126 | ``` 127 | 128 | ## FlatMap 129 | 130 | ```py 131 | >>> [0, 2, 3, 0, 4] | FlatMap(range) | Pipe(list) 132 | [0, 1, 0, 1, 2, 0, 1, 2, 3] 133 | 134 | >>> [2, 3, 4] | FlatMap(lambda x: [(x, x), (x, x)]) | Pipe(list) 135 | [(2, 2), (2, 2), (3, 3), (3, 3), (4, 4), (4, 4)] 136 | 137 | >>> def yield_even(it): 138 | ... for x in it: 139 | ... if x % 2 == 0: 140 | ... yield x 141 | >>> [range(0, 5), range(100, 105)] | FlatMap(yield_even) | Pipe(list) 142 | [0, 2, 4, 100, 102, 104] 143 | 144 | >>> [range(0, 5), range(100, 105)] | FlatMap(lambda it: (x for x in it if x % 2 == 0)) | Pipe(list) 145 | [0, 2, 4, 100, 102, 104] 146 | 147 | >>> [range(0, 5), range(100, 105)] | FlatMap(lambda it: it | Filter(lambda x: x % 2 == 0)) | Pipe(list) 148 | [0, 2, 4, 100, 102, 104] 149 | 150 | ``` 151 | 152 | ## FlatMapValues 153 | 154 | ```py 155 | >>> [("a", ["x", "y", "z"]), ("b", ["p", "r"])] | FlatMapValues(lambda x: x) | Pipe(list) 156 | [('a', 'x'), ('a', 'y'), ('a', 'z'), ('b', 'p'), ('b', 'r')] 157 | 158 | >>> [('a', [0, 1, 2]), ('b', [3, 4])] | FlatMapValues(yield_even) | Pipe(list) 159 | [('a', 0), ('a', 2), ('b', 4)] 160 | 161 | ``` 162 | 163 | ## KeyBy 164 | 165 | ```py 166 | >>> range(2) | KeyBy(str) | Pipe(list) 167 | [('0', 0), ('1', 1)] 168 | 169 | ``` 170 | 171 | ## ValueBy 172 | 173 | ```py 174 | >>> range(2) | ValueBy(str) | Pipe(list) 175 | [(0, '0'), (1, '1')] 176 | 177 | ``` 178 | 179 | ## Append 180 | 181 | ```py 182 | >>> [(0,), (1,)] | Append(lambda x: str(x[0])) | Pipe(list) 183 | [(0, '0'), (1, '1')] 184 | 185 | >>> [(0, '0'), (1, '1')] | Append(lambda x: str(x[0] * 10)) | Pipe(list) 186 | [(0, '0', '0'), (1, '1', '10')] 187 | 188 | ``` 189 | 190 | ## Keys 191 | 192 | ```py 193 | >>> [(0, 'a'), (1, 'b')] | Keys() | Pipe(list) 194 | [0, 1] 195 | 196 | ``` 197 | 198 | ## Values 199 | 200 | ```py 201 | >>> [(0, 'a'), (1, 'b')] | Values() | Pipe(list) 202 | ['a', 'b'] 203 | 204 | ``` 205 | 206 | ## SwapKV 207 | 208 | ```py 209 | >>> [(0, 1), (2, 3)] | SwapKV() | Pipe(list) 210 | [(1, 0), (3, 2)] 211 | 212 | ``` 213 | 214 | ## Grep 215 | 216 | ```py 217 | >>> ['hello foo', 'world', 'awesome FOo'] | Grep('foo') | Pipe(list) 218 | ['hello foo'] 219 | 220 | # regex is supported (passed to re.search) 221 | >>> ['foo1', 'foo2', '3foo', 'bar1'] | Grep('^foo.*') | Pipe(list) 222 | ['foo1', 'foo2'] 223 | 224 | # case-insensitive 225 | >>> ['hello foo', 'world', 'awesome FOo'] | Grep('foo', i=True) | Pipe(list) 226 | ['hello foo', 'awesome FOo'] 227 | >>> ['hello foo', 'world', 'awesome FOo'] | Grep('Foo', i=True) | Pipe(list) 228 | ['hello foo', 'awesome FOo'] 229 | 230 | # invert match 231 | >>> ['hello foo', 'world', 'awesome FOo'] | Grep('foo', v=True) | Pipe(list) 232 | ['world', 'awesome FOo'] 233 | >>> ['foo1', 'foo2', '3foo', 'bar1'] | Grep('^foo.*', v=True) | Pipe(list) 234 | ['3foo', 'bar1'] 235 | 236 | # invert match and case-insensitive 237 | >>> ['hello foo', 'world', 'awesome FOo'] | Grep('foo', v=True, i=True) | Pipe(list) 238 | ['world'] 239 | >>> ['hello foo', 'world', 'awesome FOo'] | Grep('Foo', v=True, i=True) | Pipe(list) 240 | ['world'] 241 | 242 | ``` 243 | 244 | ## IterLines 245 | 246 | ```py 247 | >>> import tempfile 248 | >>> f = tempfile.NamedTemporaryFile('w+') 249 | >>> f.write('hello\nworld\n') 250 | 12 251 | >>> f.seek(0) 252 | 0 253 | >>> f.name | IterLines() | Pipe(list) 254 | ['hello', 'world'] 255 | 256 | >>> f.name | IterLines(strip=False) | Pipe(list) 257 | ['hello\n', 'world\n'] 258 | 259 | ``` 260 | 261 | ## Count 262 | 263 | useful for objects that don't have `__len__` method: 264 | 265 | ```py 266 | >>> iter(range(3)) | Count() 267 | 3 268 | 269 | ``` 270 | 271 | ## Slice 272 | 273 | ```py 274 | >>> range(5) | Slice(2) | Pipe(list) 275 | [0, 1] 276 | >>> range(5) | Slice(2, 4) | Pipe(list) 277 | [2, 3] 278 | >>> range(5) | Slice(2, None) | Pipe(list) 279 | [2, 3, 4] 280 | >>> range(5) | Slice(0, None, 2) | Pipe(list) 281 | [0, 2, 4] 282 | 283 | ``` 284 | 285 | ## Take 286 | 287 | ```py 288 | >>> range(5) | Take(3) 289 | [0, 1, 2] 290 | 291 | ``` 292 | 293 | ``` 294 | 295 | ## Sorted 296 | 297 | ```py 298 | >>> '3510' | Sorted() 299 | ['0', '1', '3', '5'] 300 | 301 | >>> '3510' | Sorted(reverse=True) 302 | ['5', '3', '1', '0'] 303 | 304 | >>> '!*&)#' | Sorted(key=ord) 305 | ['!', '#', '&', ')', '*'] 306 | 307 | >>> '!*&)#' | Sorted(key=ord, reverse=True) 308 | ['*', ')', '&', '#', '!'] 309 | 310 | ``` 311 | 312 | ## GroupBy 313 | 314 | Note: `GroupBy` sorts iterable before grouping. If you pass key function, eg `GroupBy(len)`, it also will be used as sorting key. 315 | 316 | ```py 317 | >>> import operator 318 | >>> [(0, 'a'), (1, 'c'), (0, 'b'), (2, 'd')] | GroupBy(operator.itemgetter(0)) | MapValues(list) | Pipe(list) 319 | [(0, [(0, 'a'), (0, 'b')]), (1, [(1, 'c')]), (2, [(2, 'd')])] 320 | 321 | >>> ['ab', 'cd', 'e', 'f', 'gh', 'ij'] | GroupBy(len) | MapValues(list) | Pipe(list) 322 | [(1, ['e', 'f']), (2, ['ab', 'cd', 'gh', 'ij'])] 323 | 324 | ``` 325 | 326 | ## ReduceByKey 327 | 328 | ```py 329 | >>> import operator 330 | >>> [('a', 1), ('b', 1), ('a', 1)] | ReduceByKey(operator.add) 331 | [('a', 2), ('b', 1)] 332 | 333 | ``` 334 | 335 | ## Apply 336 | 337 | ```py 338 | >>> import random 339 | >>> random.seed(42) 340 | >>> range(5) | Pipe(list) | Apply(random.shuffle) 341 | [3, 1, 2, 4, 0] 342 | 343 | ``` 344 | 345 | ## StarPipe 346 | 347 | ```py 348 | >>> (1, 2) | StarPipe(operator.add) 349 | 3 350 | 351 | >>> ('FF', 16) | StarPipe(int) 352 | 255 353 | 354 | >>> ([1, 2], 'A') | StarPipe(dict.fromkeys) 355 | {1: 'A', 2: 'A'} 356 | 357 | >>> ({1, 2}, {3, 4, 5}) | StarPipe(set.union) 358 | {1, 2, 3, 4, 5} 359 | 360 | ``` 361 | 362 | ## StarMap 363 | 364 | ```py 365 | >>> [(2, 5), (3, 2), (10, 3)] | StarMap(pow) | Pipe(list) 366 | [32, 9, 1000] 367 | >>> [('00', 16), ('A5', 16), ('FF', 16)] | StarMap(int) | Pipe(list) 368 | [0, 165, 255] 369 | 370 | ``` 371 | 372 | ## StarFlatMap 373 | 374 | ```py 375 | >>> range(2, 10) | Pipe(itertools.permutations, r=2) | StarFlatMap(lambda a, b: [(a, b)] if a % b == 0 else []) | Pipe(list) 376 | [(4, 2), (6, 2), (6, 3), (8, 2), (8, 4), (9, 3)] 377 | 378 | ``` 379 | 380 | ## MapApply 381 | 382 | ```py 383 | >>> import random 384 | >>> random.seed(42) 385 | >>> range(3, 5) | Map(range) | Map(list) | MapApply(random.shuffle) | Pipe(list) 386 | [[1, 0, 2], [3, 1, 2, 0]] 387 | 388 | >>> def setitem(key, value): 389 | ... def inner(x): 390 | ... x[key] = value 391 | ... return inner 392 | >>> [{'hello': 'world'}] | MapApply(setitem('foo', 'bar')) | Pipe(list) 393 | [{'hello': 'world', 'foo': 'bar'}] 394 | 395 | ``` 396 | 397 | ## Switch 398 | 399 | ```py 400 | >>> cases = [ 401 | ... (lambda i: i % 3 == i % 5 == 0, lambda x: 'FizzBuzz'), 402 | ... (lambda i: i % 3 == 0, lambda x: 'Fizz'), 403 | ... (lambda i: i % 5 == 0, lambda x: 'Buzz'), 404 | ... (lambda i: i > 100, lambda x: f'{x} is large'), 405 | ... ] 406 | >>> 1 | Switch(cases) 407 | 1 408 | >>> 3 | Switch(cases) 409 | 'Fizz' 410 | >>> 5 | Switch(cases) 411 | 'Buzz' 412 | >>> 15 | Switch(cases) 413 | 'FizzBuzz' 414 | >>> 101 | Switch(cases) 415 | '101 is large' 416 | 417 | ``` 418 | 419 | ## MapSwitch 420 | 421 | ```py 422 | >>> range(1, 20) | MapSwitch(cases) | Pipe(list) 423 | [1, 2, 'Fizz', 4, 'Buzz', 'Fizz', 7, 8, 'Fizz', 'Buzz', 11, 'Fizz', 13, 14, 'FizzBuzz', 16, 17, 'Fizz', 19] 424 | >>> range(5) | MapSwitch([(lambda x: x % 2 == 0, lambda x: x * 100)]) | Pipe(list) 425 | [0, 1, 200, 3, 400] 426 | 427 | ``` 428 | 429 | ## YieldIf 430 | Takes a function to map values (optional, by default there's no mapping) and a key. If key is false, value will not be yielded. Key is optional, default is `bool` 431 | 432 | ```py 433 | >>> range(5) | YieldIf(lambda x: x * 100) | Pipe(list) 434 | [100, 200, 300, 400] 435 | >>> range(5) | YieldIf(lambda x: x * 100, key=lambda x: x % 2 == 0) | Pipe(list) 436 | [0, 200, 400] 437 | >>> range(5) | YieldIf(key=lambda x: x % 2 == 0) | Pipe(list) 438 | [0, 2, 4] 439 | >>> range(5) | YieldIf() | Pipe(list) 440 | [1, 2, 3, 4] 441 | 442 | ``` 443 | 444 | ## Join 445 | ```py 446 | >>> range(5) | Join(range(2, 5)) | Pipe(list) 447 | [(2, 2), (3, 3), (4, 4)] 448 | 449 | >>> range(1, 7) | Join(range(2, 6), key=lambda x, y: x % y == 0) | Pipe(list) 450 | [(2, 2), (3, 3), (4, 2), (4, 4), (5, 5), (6, 2), (6, 3)] 451 | 452 | ``` 453 | 454 | ## GetItem 455 | 456 | ```py 457 | >>> {'a': 'b'} | GetItem('a') 458 | 'b' 459 | 460 | ``` 461 | 462 | ## SetItem 463 | 464 | ```py 465 | >>> {'a': 'b'} | SetItem('foo', 'bar') 466 | {'a': 'b', 'foo': 'bar'} 467 | 468 | ``` 469 | 470 | ## DelItem 471 | 472 | ```py 473 | >>> {'a': 'b'} | DelItem('a') 474 | {} 475 | 476 | ``` 477 | 478 | ## GetAttr 479 | 480 | ```py 481 | >>> from types import SimpleNamespace 482 | >>> SimpleNamespace(a='b') | GetAttr('a') 483 | 'b' 484 | 485 | ``` 486 | 487 | ## SetAttr 488 | 489 | ```py 490 | >>> SimpleNamespace(a='b') | SetAttr('foo', 'bar') 491 | namespace(a='b', foo='bar') 492 | 493 | ``` 494 | 495 | ## DelAttr 496 | 497 | ```py 498 | >>> SimpleNamespace(a='b') | DelAttr('a') 499 | namespace() 500 | 501 | ``` 502 | 503 | ## MapGetItem 504 | 505 | ```py 506 | >>> [{'a': 'b'}] | MapGetItem('a') | Pipe(list) 507 | ['b'] 508 | 509 | ``` 510 | 511 | ## MapSetItem 512 | 513 | ```py 514 | >>> [{'a': 'b'}] | MapSetItem('foo', 'bar') | Pipe(list) 515 | [{'a': 'b', 'foo': 'bar'}] 516 | 517 | ``` 518 | 519 | ## MapDelItem 520 | 521 | ```py 522 | >>> [{'a': 'b'}] | MapDelItem('a') | Pipe(list) 523 | [{}] 524 | 525 | ``` 526 | 527 | ## MapGetAttr 528 | 529 | ```py 530 | >>> [SimpleNamespace(a='b')] | MapGetAttr('a') | Pipe(list) 531 | ['b'] 532 | 533 | ``` 534 | 535 | ## MapSetAttr 536 | 537 | ```py 538 | >>> [SimpleNamespace(a='b')] | MapSetAttr('foo', 'bar') | Pipe(list) 539 | [namespace(a='b', foo='bar')] 540 | 541 | ``` 542 | 543 | ## MapDelAttr 544 | 545 | ```py 546 | >>> [SimpleNamespace(a='b')] | MapDelAttr('a') | Pipe(list) 547 | [namespace()] 548 | 549 | ``` 550 | 551 | ## MethodCaller 552 | 553 | ```py 554 | >>> class K: 555 | ... def hello(self): 556 | ... return 'hello' 557 | ... def increment(self, i, add=1): 558 | ... return i + add 559 | >>> k = K() 560 | >>> k | MethodCaller('hello') 561 | 'hello' 562 | >>> k | MethodCaller('increment', 1) 563 | 2 564 | >>> k | MethodCaller('increment', 1, add=2) 565 | 3 566 | 567 | ``` 568 | 569 | ## MapMethodCaller 570 | 571 | ```py 572 | >>> [k] | MapMethodCaller('hello') | Pipe(list) 573 | ['hello'] 574 | 575 | ``` 576 | 577 | ## Unique 578 | 579 | ```py 580 | >>> ['a', 'cd', 'cd', 'e', 'fgh'] | Unique() | Pipe(list) 581 | ['a', 'cd', 'e', 'fgh'] 582 | 583 | >>> ['a', 'cd', 'cd', 'e', 'fgh'] | Unique(len) | Pipe(list) 584 | ['a', 'cd', 'fgh'] 585 | 586 | >>> [{'a': 1}, {'a': 2}, {'a': 1}] | Unique(operator.itemgetter('a')) | Pipe(list) 587 | [{'a': 1}, {'a': 2}] 588 | 589 | ``` 590 | 591 | ## Exec 592 | 593 | ```py 594 | >>> v = 42 595 | >>> random.seed(42) 596 | >>> x = [0, 1, 2] 597 | 598 | >>> v | Exec(lambda: random.shuffle(x)) 599 | 42 600 | >>> x 601 | [1, 0, 2] 602 | 603 | >>> random.seed(42) 604 | >>> x = [0, 1, 2] 605 | >>> v | Exec(random.shuffle, x) 606 | 42 607 | >>> x 608 | [1, 0, 2] 609 | >>> u = [] 610 | >>> v | Exec(lambda: u.append(1)) 611 | 42 612 | >>> u 613 | [1] 614 | >>> v | Exec(u.append, 2) 615 | 42 616 | >>> u 617 | [1, 2] 618 | >>> x = [2, 0, 1] 619 | >>> x | Exec(x.sort, reverse=True) 620 | [2, 1, 0] 621 | 622 | ``` 623 | 624 | ## Chunked 625 | 626 | ```py 627 | >>> range(5) | Chunked(2) | Pipe(list) 628 | [(0, 1), (2, 3), (4,)] 629 | 630 | >>> range(5) | Chunked(3) | Pipe(list) 631 | [(0, 1, 2), (3, 4)] 632 | 633 | ``` 634 | -------------------------------------------------------------------------------- /tests/pipe_test.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import itertools 3 | import math 4 | import operator 5 | import random 6 | from types import SimpleNamespace 7 | 8 | import hypothesis.strategies as st 9 | import pytest 10 | from hypothesis import given 11 | 12 | from pipe21 import * 13 | 14 | 15 | def is_even(x): 16 | return x % 2 == 0 17 | 18 | 19 | def yield_even(it): 20 | for x in it: 21 | if x % 2 == 0: 22 | yield x 23 | 24 | 25 | @given(st.lists(st.integers() | st.characters() | st.floats() | st.booleans() | st.binary())) 26 | def test_pipe(it): 27 | assert it | Pipe(list) == list(it) 28 | 29 | 30 | def test_pipe_args_kwargs(): 31 | assert 2 | Pipe(pow, 8) == 256 32 | assert 'FF' | Pipe(int, base=16) == 255 33 | assert b'\x02\x00' | Pipe(int.from_bytes, byteorder='big') == 512 34 | assert 'ab' | Pipe(enumerate, start=0) | Pipe(list) == [(0, 'a'), (1, 'b')] 35 | assert 5.01 | Pipe(math.isclose, 5, abs_tol=0.01) is True 36 | random.seed(44) 37 | assert [0, 1, 2] | Pipe(random.choices, [0.8, 0.15, 0.05], k=20) == [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 1, 0, 0] 38 | assert [0, 1, 2] | Pipe(itertools.zip_longest, 'ab', fillvalue=None) | Pipe(list) == [(0, 'a'), (1, 'b'), (2, None)] 39 | assert [0, 1, 2] | Pipe(itertools.accumulate, operator.add, initial=100) | Pipe(list) == [100, 100, 101, 103] 40 | 41 | 42 | @given(st.lists(st.integers() | st.characters() | st.floats() | st.booleans() | st.binary())) 43 | def test_map(it): 44 | assert it | Map(str) | Pipe(list) == list(map(str, it)) 45 | 46 | 47 | @given(st.lists(st.integers())) 48 | def test_filter(it): 49 | assert it | Filter(is_even) | Pipe(list) == list(filter(is_even, it)) 50 | 51 | 52 | @pytest.mark.parametrize( 53 | ('it', 'f', 'initializer'), [ 54 | ([], operator.add, 0), 55 | ([], operator.add, None), 56 | ([1, 2, 3], operator.add, 0), 57 | ([1, 2, 3], operator.add, 1), 58 | ([1, 2, 3], operator.add, None), 59 | (range(0), operator.add, 0), 60 | (range(10), operator.add, 0), 61 | (range(10), operator.add, 1), 62 | (range(10), operator.add, None), 63 | (list('abc'), operator.add, ''), 64 | (list('abc'), operator.add, 'd'), 65 | (list('abc'), operator.add, None), 66 | (list(''), operator.add, ''), 67 | (list(''), operator.add, 'd'), 68 | (list(''), operator.add, None), 69 | ([{1, 2}, {3, 4, 5}, {4, 5}], operator.or_, {1, 2, 3, 4, 5}), 70 | ], 71 | ) 72 | def test_reduce(it, f, initializer): 73 | if len(it) == 0 and initializer is None: 74 | with pytest.raises(TypeError): 75 | it | Reduce(f) # pylint: disable=W0106 76 | return 77 | 78 | if initializer is None: 79 | assert it | Reduce(f) == functools.reduce(f, it) 80 | return 81 | assert it | Reduce(f, initializer) == functools.reduce(f, it, initializer) 82 | 83 | 84 | def test_map_keys_map_values(): 85 | assert [(1, 10), (2, 20)] | MapKeys(str) | Pipe(list) == [('1', 10), ('2', 20)] 86 | assert [(1, 10), (2, 20)] | MapValues(str) | Pipe(list) == [(1, '10'), (2, '20')] 87 | 88 | 89 | @given(st.lists(st.integers())) 90 | def test_filter_false(it): 91 | assert it | FilterFalse(is_even) | Pipe(list) == list(filter(lambda x: not is_even(x), it)) 92 | 93 | 94 | @pytest.mark.parametrize( 95 | ('it', 'f', 'expected'), [ 96 | ([(0, 2), (3, 0)], None, [(3, 0)]), 97 | ([(0, 2), (3, 0)], lambda x: x % 2 == 0, [(0, 2)]), 98 | ], 99 | ) 100 | def test_filter_keys(it, f, expected): 101 | assert it | FilterKeys(f) | Pipe(list) == expected 102 | 103 | 104 | @pytest.mark.parametrize( 105 | ('it', 'f', 'expected'), [ 106 | ([(0, 2), (3, 0)], None, [(0, 2)]), 107 | ([(0, 2), (3, 0)], lambda x: x % 2 == 0, [(0, 2), (3, 0)]), 108 | ], 109 | ) 110 | def test_filter_values(it, f, expected): 111 | assert it | FilterValues(f) | Pipe(list) == expected 112 | 113 | 114 | @pytest.mark.parametrize( 115 | ('it', 'f', 'expected'), [ 116 | ([0, 2, 3, 0, 4], range, [0, 1, 0, 1, 2, 0, 1, 2, 3]), 117 | ([2, 3, 4], lambda x: [(x, x), (x, x)], [(2, 2), (2, 2), (3, 3), (3, 3), (4, 4), (4, 4)]), 118 | ([range(5), range(100, 105)], yield_even, [0, 2, 4, 100, 102, 104]), 119 | ([range(5), range(100, 105)], lambda it: (x for x in it if x % 2 == 0), [0, 2, 4, 100, 102, 104]), 120 | ], 121 | ) 122 | def test_flat_map(it, f, expected): 123 | assert it | FlatMap(f) | Pipe(list) == expected 124 | 125 | 126 | @pytest.mark.parametrize( 127 | ('it', 'f', 'expected'), [ 128 | ([('a', ['x', 'y', 'z']), ('b', ['p', 'r'])], lambda x: x, [('a', 'x'), ('a', 'y'), ('a', 'z'), ('b', 'p'), ('b', 'r')]), 129 | ([('a', [0, 1, 2]), ('b', [3, 4])], yield_even, [('a', 0), ('a', 2), ('b', 4)]), 130 | ], 131 | ) 132 | def test_flat_map_values(it, f, expected): 133 | assert it | FlatMapValues(f) | Pipe(list) == expected 134 | 135 | 136 | def test_key_by_value_by(): 137 | assert range(2) | KeyBy(str) | Pipe(list) == [('0', 0), ('1', 1)] 138 | assert range(2) | ValueBy(str) | Pipe(list) == [(0, '0'), (1, '1')] 139 | 140 | 141 | @pytest.mark.parametrize( 142 | ('it', 'f', 'expected'), [ 143 | ([(0,), (1,)], lambda x: str(x[0]), [(0, '0'), (1, '1')]), 144 | ([(0, '0'), (1, '1')], lambda x: str(x[0] * 10), [(0, '0', '0'), (1, '1', '10')]), 145 | ], 146 | ) 147 | def test_append(it, f, expected): 148 | assert it | Append(f) | Pipe(list) == expected 149 | assert it | Append(f) | Pipe(list) == expected 150 | 151 | 152 | @pytest.mark.parametrize( 153 | ('it', 'expected'), [ 154 | ([(0, 'a'), (1, 'b')], [0, 1]), 155 | ], 156 | ) 157 | def test_keys(it, expected): 158 | assert it | Keys() | Pipe(list) == expected 159 | 160 | 161 | @pytest.mark.parametrize( 162 | ('it', 'expected'), [ 163 | ([(0, 'a'), (1, 'b')], ['a', 'b']), 164 | ], 165 | ) 166 | def test_values(it, expected): 167 | assert it | Values() | Pipe(list) == expected 168 | 169 | 170 | @pytest.mark.parametrize( 171 | ('it', 'expected'), [ 172 | ([(0, 1), (2, 3)], [(1, 0), (3, 2)]), 173 | ], 174 | ) 175 | def test_swap_kv(it, expected): 176 | assert it | SwapKV() | Pipe(list) == expected 177 | 178 | 179 | @pytest.mark.parametrize( 180 | ('it', 'grep', 'expected'), [ 181 | (['hello foo', 'world', 'awesome FOo'], 'foo', ['hello foo']), 182 | (['foo1', 'foo2', '3foo', 'bar1'], '^foo.*', ['foo1', 'foo2']), 183 | ], 184 | ) 185 | def test_grep(it, grep, expected): 186 | assert it | Grep(grep) | Pipe(list) == expected 187 | 188 | 189 | @pytest.mark.parametrize( 190 | ('it', 'grep', 'expected'), [ 191 | (['hello foo', 'world', 'awesome FOo'], 'foo', ['world', 'awesome FOo']), 192 | (['foo1', 'foo2', '3foo', 'bar1'], '^foo.*', ['3foo', 'bar1']), 193 | ], 194 | ) 195 | def test_grep_v(it, grep, expected): 196 | assert it | Grep(grep, v=True) | Pipe(list) == expected 197 | 198 | 199 | @pytest.mark.parametrize( 200 | ('it', 'grep', 'v', 'i', 'expected'), [ 201 | (['hello foo', 'world', 'awesome FOo'], 'foo', False, False, ['hello foo']), 202 | (['hello foo', 'world', 'awesome FOo'], 'foo', False, True, ['hello foo', 'awesome FOo']), 203 | (['hello foo', 'world', 'awesome FOo'], 'Foo', False, True, ['hello foo', 'awesome FOo']), 204 | (['hello foo', 'world', 'awesome FOo'], 'foo', True, False, ['world', 'awesome FOo']), 205 | (['hello foo', 'world', 'awesome FOo'], 'foo', True, True, ['world']), 206 | (['hello foo', 'world', 'awesome FOo'], 'Foo', True, True, ['world']), 207 | ], 208 | ) 209 | def test_grep_i(it, v, grep, i, expected): 210 | assert it | Grep(grep, v=v, i=i) | Pipe(list) == expected 211 | 212 | 213 | def test_iter_lines(tmp_path): 214 | file = tmp_path / 'file.txt' 215 | file.write_text('hello\nworld\n') 216 | assert file | IterLines() | Pipe(list) == ['hello', 'world'] 217 | assert file | IterLines(strip=False) | Pipe(list) == ['hello\n', 'world\n'] 218 | 219 | 220 | @pytest.mark.parametrize( 221 | ('it', 'expected'), [ 222 | (range(3), 3), 223 | (iter(range(3)), 3), 224 | ('abc', 3), 225 | ({1, 2, 3}, 3), 226 | ({'a': 1, 'b': 2}, 2), 227 | ], 228 | ) 229 | def test_count(it, expected): 230 | assert it | Count() == expected 231 | 232 | 233 | @pytest.mark.parametrize( 234 | ('it', 'args', 'expected'), [ 235 | (range(5), (2,), [0, 1]), 236 | (range(5), (2, 4), [2, 3]), 237 | (range(5), (2, None), [2, 3, 4]), 238 | (range(5), (0, None, 2), [0, 2, 4]), 239 | ], 240 | ) 241 | def test_slice(it, args, expected): 242 | assert it | Slice(*args) | Pipe(list) == expected 243 | 244 | 245 | @pytest.mark.parametrize( 246 | ('it', 'n', 'expected'), [ 247 | (range(5), 3, [0, 1, 2]), 248 | (range(5), 1, [0]), 249 | (range(5), 0, []), 250 | (range(5), 10, [0, 1, 2, 3, 4]), 251 | ], 252 | ) 253 | def test_take(it, n, expected): 254 | assert it | Take(n) == expected 255 | 256 | 257 | @pytest.mark.parametrize( 258 | ('it', 'n', 'expected'), [ 259 | (range(5), 5, [(0, 1, 2, 3, 4)]), 260 | (range(5), 4, [(0, 1, 2, 3), (4,)]), 261 | (range(5), 3, [(0, 1, 2), (3, 4)]), 262 | (range(5), 2, [(0, 1), (2, 3), (4,)]), 263 | (range(5), 1, [(0,), (1,), (2,), (3,), (4,)]), 264 | ], 265 | ) 266 | def test_chunked(it, n, expected): 267 | assert it | Chunked(n) | Pipe(list) == expected 268 | 269 | 270 | @pytest.mark.skipif(sys.version_info >= (3, 12), reason='pre itertools.batched implementation for python<3.12') 271 | def test_chunked_zero_without_itertools_batched(): 272 | assert range(5) | Chunked(0) | Pipe(list) == [] # pylint: disable=unsupported-binary-operation 273 | 274 | 275 | @pytest.mark.skipif(sys.version_info < (3, 12), reason='itertools.batched implementation for python>=3.12') 276 | def test_chunked_zero_itertools_batched(): 277 | with pytest.raises(ValueError, match='n must be at least one'): 278 | assert range(5) | Chunked(0) | Pipe(list) == [] # pylint: disable=unsupported-binary-operation 279 | 280 | 281 | @pytest.mark.parametrize( 282 | ('it', 'f', 'expected'), [ 283 | ([(0, 'a'), (1, 'c'), (0, 'b'), (2, 'd')], operator.itemgetter(0), [(0, [(0, 'a'), (0, 'b')]), (1, [(1, 'c')]), (2, [(2, 'd')])]), 284 | (['ab', 'cd', 'e', 'f', 'gh', 'ij'], len, [(1, ['e', 'f']), (2, ['ab', 'cd', 'gh', 'ij'])]), 285 | ], 286 | ) 287 | def test_groupby(it, f, expected): 288 | assert it | GroupBy(f) | MapValues(list) | Pipe(list) == expected 289 | 290 | 291 | @pytest.mark.parametrize( 292 | ('it', 'f', 'expected'), [ 293 | ((1, 2), operator.add, 3), 294 | (('FF', 16), int, 255), 295 | (([1, 2], 'A'), dict.fromkeys, {1: 'A', 2: 'A'}), 296 | (({1, 2}, {3, 4, 5}), set.union, {1, 2, 3, 4, 5}), 297 | ], 298 | ) 299 | def test_star_pipe(it, f, expected): 300 | assert it | StarPipe(f) == expected 301 | 302 | 303 | @pytest.mark.parametrize( 304 | ('it', 'f', 'expected'), [ 305 | ([(2, 5), (3, 2), (10, 3)], pow, [32, 9, 1000]), 306 | ([('00', 16), ('A5', 16), ('FF', 16)], int, [0, 165, 255]), 307 | ], 308 | ) 309 | def test_star_map(it, f, expected): 310 | assert it | StarMap(f) | Pipe(list) == expected 311 | 312 | 313 | @pytest.mark.parametrize( 314 | ('it', 'f', 'expected'), [ 315 | (range(2, 10) | Pipe(itertools.permutations, r=2), lambda a, b: [(a, b)] if a % b == 0 else [], [(4, 2), (6, 2), (6, 3), (8, 2), (8, 4), (9, 3)]), 316 | ], 317 | ) 318 | def test_star_flatmap(it, f, expected): 319 | assert it | StarFlatMap(f) | Pipe(list) == expected 320 | 321 | 322 | @pytest.mark.parametrize( 323 | ('it', 'kw'), [ 324 | ([3, 5, 1, 0], {}), 325 | ([3, 5, 1, 0], {'reverse': True}), 326 | ('3510', {'key': int}), 327 | ('3510', {'key': int, 'reverse': True}), 328 | ('9j8xy2m#98g%^xd$', {'key': ord, 'reverse': True}), 329 | ('9j8xy2m#98g%^xd$', {'key': ord, 'reverse': False}), 330 | ], 331 | ) 332 | def test_sorted(it, kw): 333 | assert it | Sorted(**kw) == sorted(it, **kw) 334 | 335 | 336 | def test_map_apply(): 337 | random.seed(42) 338 | assert range(3, 5) | Map(range) | Map(list) | MapApply(random.shuffle) | Pipe(list) == [[1, 0, 2], [3, 1, 2, 0]] 339 | 340 | 341 | cases = [ 342 | (lambda i: i % 3 == i % 5 == 0, lambda x: 'FizzBuzz'), 343 | (lambda i: i % 3 == 0, lambda x: 'Fizz'), 344 | (lambda i: i % 5 == 0, lambda x: 'Buzz'), 345 | (lambda i: i > 100, lambda x: f'{x} is large'), 346 | ] 347 | 348 | 349 | @pytest.mark.parametrize( 350 | ('x', 'expected'), [ 351 | (1, 1), 352 | (3, 'Fizz'), 353 | (5, 'Buzz'), 354 | (15, 'FizzBuzz'), 355 | (101, '101 is large'), 356 | ], 357 | ) 358 | def test_switch(x, expected): 359 | assert x | Switch(cases) == expected 360 | 361 | 362 | @pytest.mark.parametrize( 363 | ('it', 'cases', 'expected'), [ 364 | (range(1, 100), cases, [1, 2, 'Fizz', 4, 'Buzz', 'Fizz', 7, 8, 'Fizz', 'Buzz', 11, 'Fizz', 13, 14, 'FizzBuzz', 16, 17, 'Fizz', 19, 'Buzz', 'Fizz', 22, 23, 'Fizz', 'Buzz', 26, 'Fizz', 28, 29, 'FizzBuzz', 31, 32, 'Fizz', 34, 'Buzz', 'Fizz', 37, 38, 'Fizz', 'Buzz', 41, 'Fizz', 43, 44, 'FizzBuzz', 46, 47, 'Fizz', 49, 'Buzz', 'Fizz', 52, 53, 'Fizz', 'Buzz', 56, 'Fizz', 58, 59, 'FizzBuzz', 61, 62, 'Fizz', 64, 'Buzz', 'Fizz', 67, 68, 'Fizz', 'Buzz', 71, 'Fizz', 73, 74, 'FizzBuzz', 76, 77, 'Fizz', 79, 'Buzz', 'Fizz', 82, 83, 'Fizz', 'Buzz', 86, 'Fizz', 88, 89, 'FizzBuzz', 91, 92, 'Fizz', 94, 'Buzz', 'Fizz', 97, 98, 'Fizz']), 365 | (range(5), [(lambda x: x % 2 == 0, lambda x: x * 100)], [0, 1, 200, 3, 400]), 366 | ], 367 | ) 368 | def test_map_switch(it, cases, expected): 369 | assert it | MapSwitch(cases) | Pipe(list) == expected 370 | 371 | 372 | @pytest.mark.parametrize( 373 | ('it', 'f', 'key', 'expected'), [ 374 | (range(5), lambda x: x * 100, None, [100, 200, 300, 400]), 375 | (range(5), lambda x: x * 100, lambda x: x % 2 == 0, [0, 200, 400]), 376 | (range(5), None, lambda x: x % 2 == 0, [0, 2, 4]), 377 | (range(5), None, None, [1, 2, 3, 4]), 378 | ], 379 | ) 380 | def test_yield_if(it, f, key, expected): 381 | y = YieldIf(f) if key is None else YieldIf(f, key=key) 382 | assert it | y | Pipe(list) == expected 383 | 384 | 385 | @pytest.mark.parametrize( 386 | ('a', 'b', 'key', 'expected'), [ 387 | (range(5), range(2, 5), None, [(2, 2), (3, 3), (4, 4)]), 388 | (range(1, 7), range(2, 6), lambda x, y: x % y == 0, [(2, 2), (3, 3), (4, 2), (4, 4), (5, 5), (6, 2), (6, 3)]), 389 | ], 390 | ) 391 | def test_join(a, b, key, expected): 392 | j = Join(b) if key is None else Join(b, key=key) 393 | assert a | j | Pipe(list) == expected 394 | 395 | 396 | @pytest.mark.parametrize( 397 | ('it', 'f', 'expected'), [ 398 | ([('a', 1), ('b', 1), ('a', 1)], operator.add, [('a', 2), ('b', 1)]), 399 | ], 400 | ) 401 | def test_reduce_by_key(it, f, expected): 402 | assert it | ReduceByKey(f) == expected 403 | 404 | 405 | def test_apply(): 406 | random.seed(42) 407 | assert range(5) | Pipe(list) | Apply(random.shuffle) == [3, 1, 2, 4, 0] 408 | 409 | 410 | def test_descriptors(): 411 | assert {'a': 'b'} | GetItem('a') == 'b' 412 | assert {'a': 'b'} | SetItem('foo', 'bar') == {'a': 'b', 'foo': 'bar'} 413 | assert {'a': 'b'} | DelItem('a') == {} 414 | assert [{'a': 'b'}] | MapGetItem('a') | Pipe(list) == ['b'] 415 | assert [{'a': 'b'}] | MapSetItem('foo', 'bar') | Pipe(list) == [{'a': 'b', 'foo': 'bar'}] 416 | assert [{'a': 'b'}] | MapDelItem('a') | Pipe(list) == [{}] 417 | assert SimpleNamespace(a='b') | GetAttr('a') == 'b' 418 | assert SimpleNamespace(a='b') | SetAttr('foo', 'bar') == SimpleNamespace(a='b', foo='bar') 419 | assert SimpleNamespace(a='b') | DelAttr('a') == SimpleNamespace() 420 | assert [SimpleNamespace(a='b')] | MapGetAttr('a') | Pipe(list) == ['b'] 421 | assert [SimpleNamespace(a='b')] | MapSetAttr('foo', 'bar') | Pipe(list) == [SimpleNamespace(a='b', foo='bar')] 422 | assert [SimpleNamespace(a='b')] | MapDelAttr('a') | Pipe(list) == [SimpleNamespace()] 423 | 424 | 425 | class K: 426 | def hello(self): 427 | return 'hello' 428 | 429 | def increment(self, i, add=1): 430 | return i + add 431 | 432 | 433 | def test_methodcaller(): 434 | k = K() 435 | assert k | MethodCaller('hello') == 'hello' 436 | assert k | MethodCaller('increment', 1) == 2 437 | assert k | MethodCaller('increment', 1, add=2) == 3 438 | assert [k] | MapMethodCaller('hello') | Pipe(list) == ['hello'] 439 | 440 | 441 | @pytest.mark.parametrize( 442 | ('seq', 'key', 'expected'), [ 443 | ([0, 1, 1, 2], None, [0, 1, 2]), 444 | ('0112', int, ['0', '1', '2']), 445 | (range(10), lambda x: x % 3, [0, 1, 2]), 446 | (['a', 'cd', 'cd', 'e', 'fgh'], None, ['a', 'cd', 'e', 'fgh']), 447 | (['a', 'cd', 'cd', 'e', 'fgh'], len, ['a', 'cd', 'fgh']), 448 | ([{'a': 1}, {'a': 2}, {'a': 1}], operator.itemgetter('a'), [{'a': 1}, {'a': 2}]), 449 | ], 450 | ) 451 | def test_unique(seq, key, expected): 452 | assert seq | Unique(key) | Pipe(list) == expected 453 | 454 | 455 | def test_exec(): 456 | v = 42 457 | 458 | random.seed(42) 459 | x = [0, 1, 2] 460 | assert v | Exec(lambda: random.shuffle(x)) == v 461 | assert x == [1, 0, 2] 462 | 463 | random.seed(42) 464 | x = [0, 1, 2] 465 | assert v | Exec(random.shuffle, x) == v 466 | assert x == [1, 0, 2] 467 | 468 | u = [] 469 | assert v | Exec(lambda: u.append(1)) == v 470 | assert u == [1] 471 | 472 | assert v | Exec(u.append, 2) == v 473 | assert u == [1, 2] 474 | 475 | x = [2, 0, 1] 476 | assert x | Exec(x.sort, reverse=True) == [2, 1, 0] 477 | --------------------------------------------------------------------------------