├── .coveragerc ├── .github └── workflows │ ├── pypi.yml │ ├── pyright.yml │ └── pytest.yml ├── .gitignore ├── .readthedocs.yaml ├── LICENSE ├── README.md ├── RunTest.ps1 ├── _config.yml ├── doc ├── Makefile ├── _static │ └── override.css ├── api │ ├── more │ │ ├── types_linq.more.extrema_enumerable.md │ │ ├── types_linq.more.more_enumerable.md │ │ ├── types_linq.more.more_enums.md │ │ └── types_linq.more.more_error.md │ ├── types_linq.cached_enumerable.md │ ├── types_linq.enumerable.md │ ├── types_linq.grouping.md │ ├── types_linq.lookup.md │ ├── types_linq.more_typing.md │ ├── types_linq.ordered_enumerable.md │ └── types_linq.types_linq_error.md ├── api_spec.py ├── conf.py ├── gen_api_doc.py ├── index.rst ├── make.bat ├── requirements.txt └── to-start │ ├── changelog.rst │ ├── differences.rst │ ├── examples.rst │ └── installing.rst ├── pyrightconfig.json ├── setup.py ├── tests ├── test_more_usage.py ├── test_tricky.py └── test_usage.py └── types_linq ├── __init__.py ├── cached_enumerable.py ├── enumerable.py ├── enumerable.pyi ├── grouping.py ├── lookup.py ├── more ├── __init__.py ├── extrema_enumerable.py ├── extrema_enumerable.pyi ├── more_enumerable.py ├── more_enumerable.pyi ├── more_enums.py └── more_error.py ├── more_typing.py ├── ordered_enumerable.py ├── ordered_enumerable.pyi ├── py.typed ├── types_linq_error.py └── util.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = true 3 | source = types_linq 4 | omit = types_linq/more_typing.py 5 | 6 | [report] 7 | exclude_lines = 8 | pragma: no cover 9 | if TYPE_CHECKING: 10 | fail_under = 100 11 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | name: pypi 2 | 3 | on: 4 | push: 5 | tags: [v*] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | environment: pypi 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Python 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: 3.8 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install twine 22 | - name: Build sdist 23 | run: | 24 | python setup.py sdist 25 | - name: Publish to Pypi 26 | env: 27 | TWINE_REPOSITORY: pypi 28 | TWINE_NON_INTERACTIVE: true 29 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 30 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 31 | run: | 32 | twine upload dist/* 33 | -------------------------------------------------------------------------------- /.github/workflows/pyright.yml: -------------------------------------------------------------------------------- 1 | name: pyright 2 | 3 | on: 4 | schedule: 5 | - cron: 0 9 * * 4 6 | push: 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | on-ubuntu-latest-py37: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up Python 3.7 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: 3.7 20 | - name: Set up npm 21 | uses: actions/setup-node@v2 22 | with: 23 | node-version: 14 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install pytest 28 | - name: Check types with Pyright 29 | run: | 30 | npx pyright 31 | -------------------------------------------------------------------------------- /.github/workflows/pytest.yml: -------------------------------------------------------------------------------- 1 | name: pytest 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest, macos-latest, windows-latest] 15 | python-version: [3.7, 3.8, 3.9] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install pytest 27 | - name: Install project 28 | run: | 29 | pip install . 30 | #- name: Lint with flake8 31 | # run: | 32 | # # stop the build if there are Python syntax errors or undefined names 33 | # flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 34 | # # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 35 | # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 36 | - name: Test with pytest 37 | run: | 38 | pytest 39 | 40 | coverage: 41 | needs: build 42 | runs-on: ubuntu-latest 43 | 44 | steps: 45 | - uses: actions/checkout@v2 46 | - name: Set up Python 3.8 47 | uses: actions/setup-python@v2 48 | with: 49 | python-version: 3.8 50 | - name: Install dependencies 51 | run: | 52 | python -m pip install --upgrade pip 53 | pip install pytest coverage 54 | - name: Install project 55 | run: | 56 | pip install . 57 | - name: Coverage report 58 | run: | 59 | coverage run -m pytest 60 | coverage report -m 61 | - uses: codecov/codecov-action@v1 62 | name: Upload coverage to Codecov 63 | with: 64 | fail_ci_if_error: true 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # venv 132 | [Ii]nclude/ 133 | [Ss]cripts/ 134 | pyvenv.cfg 135 | 136 | # sphinx 137 | doc/_build 138 | 139 | # editor 140 | .vscode 141 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 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 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | builder: html 11 | configuration: doc/conf.py 12 | 13 | # Optionally build your docs in additional formats such as PDF 14 | formats: 15 | - pdf 16 | - epub 17 | 18 | # Optionally set the version of Python and requirements required to build your docs 19 | python: 20 | version: 3 21 | install: 22 | - requirements: doc/requirements.txt 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2021, cleoold 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # types-linq 2 | 3 | ![Python](https://img.shields.io/badge/python-3.7%2B-blue.svg) [![pypi](https://img.shields.io/pypi/v/types-linq)](https://pypi.org/project/types-linq/) [![pytest](https://github.com/cleoold/types-linq/workflows/pytest/badge.svg)](https://github.com/cleoold/types-linq/actions?query=workflow%3Apytest) [![codecov](https://codecov.io/gh/cleoold/types-linq/branch/main/graph/badge.svg?token=HTUKZ0SQJ3)](https://codecov.io/gh/cleoold/types-linq) [![Documentation Status](https://readthedocs.org/projects/types-linq/badge/?version=latest)](https://types-linq.readthedocs.io/en/latest/?badge=latest) 4 | 5 | types-linq is a lightweight Python library that attempts to implement LINQ (Language Integrated Query) features seen in .NET languages. 6 | 7 | Usage, guide and API references can be found in the [documentation](https://types-linq.readthedocs.io/en/latest/) page. 8 | -------------------------------------------------------------------------------- /RunTest.ps1: -------------------------------------------------------------------------------- 1 | coverage run -m pytest @args 2 | if ($?) 3 | { 4 | coverage report -m 5 | } 6 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /doc/_static/override.css: -------------------------------------------------------------------------------- 1 | .rst-content code, 2 | .rst-content tt, 3 | code { 4 | font-size: 90%; 5 | border: none; 6 | border-bottom: 1px solid rgba(61, 110, 110, 0.288); 7 | background: none; 8 | padding-left: 0px; 9 | padding-right: 0px; 10 | } 11 | 12 | .rst-content a code, 13 | .rst-content a tt, 14 | a code { 15 | border-bottom: 1px solid rgba(61, 110, 110, 0.938); 16 | } 17 | 18 | .rst-content a code:hover, 19 | .rst-content a tt:hover, 20 | a code:hover { 21 | border-width: 2px; 22 | } 23 | 24 | span.pre { 25 | color: rgb(21, 77, 78); 26 | white-space: normal; 27 | } 28 | 29 | .wy-menu .toctree-l1 span.pre { 30 | color: unset; 31 | } 32 | 33 | .wy-nav-content { 34 | max-width: 1100px; 35 | } 36 | 37 | .rst-content .linenodiv pre, 38 | .rst-content div[class^="highlight"] pre, 39 | .rst-content pre.literal-block { 40 | font-size: 14px; 41 | } 42 | -------------------------------------------------------------------------------- /doc/api/more/types_linq.more.extrema_enumerable.md: -------------------------------------------------------------------------------- 1 | # module ``types_linq.more.extrema_enumerable`` 2 | 3 | (apiref.ExtremaEnumerable)= 4 | ## class `ExtremaEnumerable[TSource_co, TKey]` 5 | 6 | ```py 7 | from types_linq.more.extrema_enumerable import ExtremaEnumerable 8 | ``` 9 | 10 | Specialization for manipulating extrema. 11 | 12 | Users should not construct instances of this class directly. Use `MoreEnumerable.maxima_by()` 13 | instead. 14 | 15 | Revisions 16 | ~ v0.2.0: New. 17 | 18 | ### Bases 19 | 20 | - [`MoreEnumerable`](apiref.MoreEnumerable)`[`[`TSource_co`](apiref.TSource_co)`]` 21 | - `Generic[`[`TSource_co`](apiref.TSource_co)`, `[`TKey`](apiref.TKey)`]` 22 | 23 | ### Members 24 | 25 | #### instancemethod `take(count)` 26 | 27 | Parameters 28 | ~ *count*: `int` 29 | 30 | Returns 31 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[`[`TSource_co`](apiref.TSource_co)`]` 32 | 33 | Returns a specified number of contiguous elements from the start of the sequence. 34 | 35 | --- 36 | 37 | #### instancemethod `take(__index)` 38 | 39 | Parameters 40 | ~ *__index*: `slice` 41 | 42 | Returns 43 | ~ [`Enumerable`](apiref.Enumerable)`[`[`TSource_co`](apiref.TSource_co)`]` 44 | 45 | Identical to parent. 46 | 47 | Revisions 48 | ~ v1.1.0: Fixed incorrect override of `Enumerable.take()` when it takes a slice. 49 | 50 | --- 51 | 52 | #### instancemethod `take_last(count)` 53 | 54 | Parameters 55 | ~ *count*: `int` 56 | 57 | Returns 58 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[`[`TSource_co`](apiref.TSource_co)`]` 59 | 60 | Returns a new sequence that contains the last `count` elements. 61 | 62 | -------------------------------------------------------------------------------- /doc/api/more/types_linq.more.more_enumerable.md: -------------------------------------------------------------------------------- 1 | # module ``types_linq.more.more_enumerable`` 2 | 3 | (apiref.MoreEnumerable)= 4 | ## class `MoreEnumerable[TSource_co]` 5 | 6 | ```py 7 | from types_linq.more import MoreEnumerable 8 | ``` 9 | 10 | MoreEnumerable provides more query methods. Instances of this class can be created by directly 11 | constructing, using `as_more()`, or invoking MoreEnumerable methods that return MoreEnumerable 12 | instead of Enumerable. 13 | 14 | These APIs may have breaking changes more frequently than those in Enumerable class because updates 15 | in .NET are happening and sometimes ones of these APIs could be moved to Enumerable with modification, 16 | or changed to accommodate changes to Enumerable. 17 | 18 | Revisions 19 | ~ v0.2.0: New. 20 | 21 | ### Bases 22 | 23 | - [`Enumerable`](apiref.Enumerable)`[`[`TSource_co`](apiref.TSource_co)`]` 24 | 25 | ### Members 26 | 27 | #### instancemethod `aggregate_right[TAccumulate, TResult](__seed, __func, __result_selector)` 28 | 29 | Parameters 30 | ~ *__seed*: [`TAccumulate`](apiref.TAccumulate) 31 | ~ *__func*: `Callable[[`[`TSource_co`](apiref.TSource_co)`, `[`TAccumulate`](apiref.TAccumulate)`], `[`TAccumulate`](apiref.TAccumulate)`]` 32 | ~ *__result_selector*: `Callable[[`[`TAccumulate`](apiref.TAccumulate)`], `[`TResult`](apiref.TResult)`]` 33 | 34 | Returns 35 | ~ [`TResult`](apiref.TResult) 36 | 37 | Applies a right-associative accumulator function over the sequence. The seed is used as 38 | the initial accumulator value, and the result_selector is used to select the result value. 39 | 40 | Revisions 41 | ~ v1.2.0: Fixed annotation for __func. 42 | 43 | --- 44 | 45 | #### instancemethod `aggregate_right[TAccumulate](__seed, __func)` 46 | 47 | Parameters 48 | ~ *__seed*: [`TAccumulate`](apiref.TAccumulate) 49 | ~ *__func*: `Callable[[`[`TSource_co`](apiref.TSource_co)`, `[`TAccumulate`](apiref.TAccumulate)`], `[`TAccumulate`](apiref.TAccumulate)`]` 50 | 51 | Returns 52 | ~ [`TAccumulate`](apiref.TAccumulate) 53 | 54 | Applies a right-associative accumulator function over the sequence. The seed is used as the 55 | initial accumulator value. 56 | 57 | Example 58 | ~ ```py 59 | >>> values = [9, 4, 2] 60 | >>> MoreEnumerable(values).aggregate_right('null', lambda e, rr: f'(cons {e} {rr})') 61 | '(cons 9 (cons 4 (cons 2 null)))' 62 | ``` 63 | 64 | Revisions 65 | ~ v1.2.0: Fixed annotation for __func. 66 | 67 | --- 68 | 69 | #### instancemethod `aggregate_right(__func)` 70 | 71 | Parameters 72 | ~ *__func*: `Callable[[`[`TSource_co`](apiref.TSource_co)`, `[`TSource_co`](apiref.TSource_co)`], `[`TSource_co`](apiref.TSource_co)`]` 73 | 74 | Returns 75 | ~ [`TSource_co`](apiref.TSource_co) 76 | 77 | Applies a right-associative accumulator function over the sequence. Raises [`InvalidOperationError`](apiref.InvalidOperationError) 78 | if there is no value in the sequence. 79 | 80 | Example 81 | ~ ```py 82 | >>> values = ['9', '4', '2', '5'] 83 | >>> MoreEnumerable(values).aggregate_right(lambda e, rr: f'({e}+{rr})') 84 | '(9+(4+(2+5)))' 85 | ``` 86 | 87 | Revisions 88 | ~ v1.2.0: Fixed annotation for __func. 89 | 90 | --- 91 | 92 | #### instancemethod `as_more()` 93 | 94 | 95 | Returns 96 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[`[`TSource_co`](apiref.TSource_co)`]` 97 | 98 | Returns the original MoreEnumerable reference. 99 | 100 | --- 101 | 102 | #### instancemethod `consume()` 103 | 104 | 105 | Returns 106 | ~ `None` 107 | 108 | Consumes the sequence completely. This method iterates the sequence immediately and does not save 109 | any intermediate data. 110 | 111 | Revisions 112 | ~ v1.1.0: New. 113 | 114 | --- 115 | 116 | #### instancemethod `cycle(count=None)` 117 | 118 | Parameters 119 | ~ *count*: `Optional[int]` 120 | 121 | Returns 122 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[`[`TSource_co`](apiref.TSource_co)`]` 123 | 124 | Repeats the sequence `count` times. 125 | 126 | If `count` is `None`, the sequence is infinite. Raises [`InvalidOperationError`](apiref.InvalidOperationError) if `count` 127 | is negative. 128 | 129 | Example 130 | ~ ```py 131 | >>> MoreEnumerable([1, 2, 3]).cycle(3).to_list() 132 | [1, 2, 3, 1, 2, 3, 1, 2, 3] 133 | ``` 134 | 135 | Revisions 136 | ~ v1.1.0: New. 137 | 138 | --- 139 | 140 | #### instancemethod `enumerate(start=0)` 141 | 142 | Parameters 143 | ~ *start*: `int` 144 | 145 | Returns 146 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[Tuple[int, `[`TSource_co`](apiref.TSource_co)`]]` 147 | 148 | Returns a sequence of tuples containing the index and the value from the source sequence. `start` 149 | is used to specify the starting index. 150 | 151 | Example 152 | ~ ```py 153 | >>> ints = [2, 4, 6] 154 | >>> MoreEnumerable(ints).enumerate().to_list() 155 | [(0, 2), (1, 4), (2, 6)] 156 | ``` 157 | 158 | Revisions 159 | ~ v1.0.0: New. 160 | 161 | --- 162 | 163 | #### instancemethod `except_by2(second, key_selector)` 164 | 165 | Parameters 166 | ~ *second*: `Iterable[`[`TSource_co`](apiref.TSource_co)`]` 167 | ~ *key_selector*: `Callable[[`[`TSource_co`](apiref.TSource_co)`], object]` 168 | 169 | Returns 170 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[`[`TSource_co`](apiref.TSource_co)`]` 171 | 172 | Produces the set difference of two sequences: self - second, according to a key selector that 173 | determines "distinctness". Note the second iterable is homogenous to self. 174 | 175 | Example 176 | ~ ```py 177 | >>> first = [(16, 'x'), (9, 'y'), (12, 'd'), (16, 't')] 178 | >>> second = [(24, 'd'), (77, 'y')] 179 | >>> MoreEnumerable(first).except_by2(second, lambda x: x[1]).to_list() 180 | [(16, 'x'), (16, 't')] 181 | ``` 182 | 183 | Revisions 184 | ~ v1.0.0: Renamed from `except_by()` to this name to accommodate an update to Enumerable class. 185 | ~ v0.2.1: Added preliminary support for unhashable keys. 186 | 187 | --- 188 | 189 | #### instancemethod `flatten()` 190 | 191 | 192 | Returns 193 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[Any]` 194 | 195 | Flattens the sequence containing arbitrarily-nested subsequences. 196 | 197 | Note: the nested objects must be Iterable to be flatten. 198 | Instances of `str` or `bytes` are not flattened. 199 | 200 | Example 201 | ~ ```py 202 | >>> lst = ['apple', ['orange', ['juice', 'mango'], 'delta function']] 203 | >>> MoreEnumerable(lst).flatten().to_list() 204 | ['apple', 'orange', 'juice', 'mango', 'delta function'] 205 | ``` 206 | 207 | --- 208 | 209 | #### instancemethod `flatten(__predicate)` 210 | 211 | Parameters 212 | ~ *__predicate*: `Callable[[Iterable[Any]], bool]` 213 | 214 | Returns 215 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[Any]` 216 | 217 | Flattens the sequence containing arbitrarily-nested subsequences. A predicate function determines 218 | whether a nested iterable should be flattened or not. 219 | 220 | Note: the nested objects must be Iterable to be flatten. 221 | 222 | --- 223 | 224 | #### instancemethod `flatten2(selector)` 225 | 226 | Parameters 227 | ~ *selector*: `Callable[[Any], Optional[Iterable[object]]]` 228 | 229 | Returns 230 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[Any]` 231 | 232 | Flattens the sequence containing arbitrarily-nested subsequences. A selector is used to select a 233 | subsequence based on the object's properties. If the selector returns None, then the object is 234 | considered a leaf. 235 | 236 | --- 237 | 238 | #### instancemethod `for_each(action)` 239 | 240 | Parameters 241 | ~ *action*: `Callable[[`[`TSource_co`](apiref.TSource_co)`], object]` 242 | 243 | Returns 244 | ~ `None` 245 | 246 | Executes the given function on each element in the source sequence. The return values are discarded. 247 | 248 | Example 249 | ~ ```py 250 | >>> def gen(): 251 | ... yield 116; yield 35; yield -9 252 | 253 | >>> Enumerable(gen()).where(lambda x: x > 0).as_more().for_each(print) 254 | 116 255 | 35 256 | ``` 257 | 258 | --- 259 | 260 | #### instancemethod `for_each2(action)` 261 | 262 | Parameters 263 | ~ *action*: `Callable[[`[`TSource_co`](apiref.TSource_co)`, int], object]` 264 | 265 | Returns 266 | ~ `None` 267 | 268 | Executes the given function on each element in the source sequence. Each element's index is used in 269 | the logic of the function. The return values are discarded. 270 | 271 | --- 272 | 273 | #### instancemethod `interleave(*iters)` 274 | 275 | Parameters 276 | ~ **iters*: `Iterable[`[`TSource_co`](apiref.TSource_co)`]` 277 | 278 | Returns 279 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[`[`TSource_co`](apiref.TSource_co)`]` 280 | 281 | Interleaves the elements of two or more sequences into a single sequence, skipping sequences if they 282 | are consumed. 283 | 284 | Example 285 | ~ ```py 286 | >>> MoreEnumerable(['1', '2']).interleave(['4', '5', '6'], ['7', '8', '9']).to_list() 287 | ['1', '4', '7', '2', '5', '8', '6', '9'] 288 | ``` 289 | 290 | --- 291 | 292 | #### instancemethod `maxima_by[TSupportsLessThan](selector)` 293 | 294 | Parameters 295 | ~ *selector*: `Callable[[`[`TSource_co`](apiref.TSource_co)`], `[`TSupportsLessThan`](apiref.TSupportsLessThan)`]` 296 | 297 | Returns 298 | ~ [`ExtremaEnumerable`](apiref.ExtremaEnumerable)`[`[`TSource_co`](apiref.TSource_co)`, `[`TSupportsLessThan`](apiref.TSupportsLessThan)`]` 299 | 300 | Returns the maximal elements of the sequence based on the given selector. 301 | 302 | Example 303 | ~ ```py 304 | >>> strings = ['foo', 'bar', 'cheese', 'orange', 'baz', 'spam', 'egg', 'toasts', 'dish'] 305 | >>> MoreEnumerable(strings).maxima_by(len).to_list() 306 | ['cheese', 'orange', 'toasts'] 307 | >>> MoreEnumerable(strings).maxima_by(lambda x: x.count('e')).first() 308 | 'cheese' 309 | ``` 310 | 311 | --- 312 | 313 | #### instancemethod `maxima_by[TKey](selector, __comparer)` 314 | 315 | Parameters 316 | ~ *selector*: `Callable[[`[`TSource_co`](apiref.TSource_co)`], `[`TKey`](apiref.TKey)`]` 317 | ~ *__comparer*: `Callable[[`[`TKey`](apiref.TKey)`, `[`TKey`](apiref.TKey)`], int]` 318 | 319 | Returns 320 | ~ [`ExtremaEnumerable`](apiref.ExtremaEnumerable)`[`[`TSource_co`](apiref.TSource_co)`, `[`TKey`](apiref.TKey)`]` 321 | 322 | Returns the maximal elements of the sequence based on the given selector and the comparer. 323 | 324 | Such comparer takes two values and return positive ints when lhs > rhs, negative ints 325 | if lhs < rhs, and 0 if they are equal. 326 | 327 | --- 328 | 329 | #### instancemethod `minima_by[TSupportsLessThan](selector)` 330 | 331 | Parameters 332 | ~ *selector*: `Callable[[`[`TSource_co`](apiref.TSource_co)`], `[`TSupportsLessThan`](apiref.TSupportsLessThan)`]` 333 | 334 | Returns 335 | ~ [`ExtremaEnumerable`](apiref.ExtremaEnumerable)`[`[`TSource_co`](apiref.TSource_co)`, `[`TSupportsLessThan`](apiref.TSupportsLessThan)`]` 336 | 337 | Returns the minimal elements of the sequence based on the given selector. 338 | 339 | --- 340 | 341 | #### instancemethod `minima_by[TKey](selector, __comparer)` 342 | 343 | Parameters 344 | ~ *selector*: `Callable[[`[`TSource_co`](apiref.TSource_co)`], `[`TKey`](apiref.TKey)`]` 345 | ~ *__comparer*: `Callable[[`[`TKey`](apiref.TKey)`, `[`TKey`](apiref.TKey)`], int]` 346 | 347 | Returns 348 | ~ [`ExtremaEnumerable`](apiref.ExtremaEnumerable)`[`[`TSource_co`](apiref.TSource_co)`, `[`TKey`](apiref.TKey)`]` 349 | 350 | Returns the minimal elements of the sequence based on the given selector and the comparer. 351 | 352 | Such comparer takes two values and return positive ints when lhs > rhs, negative ints 353 | if lhs < rhs, and 0 if they are equal. 354 | 355 | --- 356 | 357 | #### instancemethod `pipe(action)` 358 | 359 | Parameters 360 | ~ *action*: `Callable[[`[`TSource_co`](apiref.TSource_co)`], object]` 361 | 362 | Returns 363 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[`[`TSource_co`](apiref.TSource_co)`]` 364 | 365 | Executes the given action on each element in the sequence and yields it. Return values of 366 | action are discarded. 367 | 368 | Example 369 | ~ ```py 370 | >>> store = set() 371 | >>> MoreEnumerable([1, 2, 2, 1]).pipe(store.add).where(lambda x: x % 2 == 0).to_list() 372 | [2, 2] 373 | >>> store 374 | {1, 2} 375 | ``` 376 | 377 | Revisions 378 | ~ v0.2.1: New. 379 | 380 | --- 381 | 382 | #### instancemethod `pre_scan[TAccumulate](identity, transformation)` 383 | 384 | Parameters 385 | ~ *identity*: [`TAccumulate`](apiref.TAccumulate) 386 | ~ *transformation*: `Callable[[`[`TAccumulate`](apiref.TAccumulate)`, `[`TSource_co`](apiref.TSource_co)`], `[`TAccumulate`](apiref.TAccumulate)`]` 387 | 388 | Returns 389 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[`[`TAccumulate`](apiref.TAccumulate)`]` 390 | 391 | Performs a pre-scan (exclusive prefix sum) over the sequence. Such scan returns an 392 | equal-length sequence where the first element is the identity, and i-th element (i>1) is 393 | the sum of the first i-1 (and identity) elements in the original sequence. 394 | 395 | Example 396 | ~ ```py 397 | >>> values = [9, 4, 2, 5, 7] 398 | >>> MoreEnumerable(values).pre_scan(0, lambda acc, e: acc + e).to_list() 399 | [0, 9, 13, 15, 20] 400 | >>> MoreEnumerable([]).pre_scan(0, lambda acc, e: acc + e).to_list() 401 | [] 402 | ``` 403 | 404 | Revisions 405 | ~ v1.2.0: New. 406 | 407 | --- 408 | 409 | #### instancemethod `rank[TSupportsLessThan](*, method=RankMethods.dense)` 410 | 411 | Constraint 412 | ~ *self*: [`MoreEnumerable`](apiref.MoreEnumerable)`[`[`TSupportsLessThan`](apiref.TSupportsLessThan)`]` 413 | 414 | Parameters 415 | ~ *method*: [`RankMethods`](apiref.RankMethods) 416 | 417 | Returns 418 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[int]` 419 | 420 | Ranks each item in the sequence in descending order using the method provided. 421 | 422 | Example 423 | ~ ```py 424 | >>> scores = [1, 4, 77, 23, 23, 4, 9, 0, -7, 101, 23] 425 | >>> MoreEnumerable(scores).rank().to_list() 426 | [6, 5, 2, 3, 3, 5, 4, 7, 8, 1, 3] # 101 is largest, so has rank of 1 427 | 428 | >>> MoreEnumerable(scores).rank(method=RankMethods.competitive).to_list() 429 | [9, 7, 2, 3, 3, 7, 6, 10, 11, 1, 3] # there are no 4th or 5th since there 430 | # are three 3rd's 431 | 432 | >>> MoreEnumerable(scores).rank(method=RankMethods.ordinal).to_list() 433 | [9, 7, 2, 3, 4, 8, 6, 10, 11, 1, 5] # as in sorting 434 | ``` 435 | 436 | Revisions 437 | ~ v1.2.1: Added method parameter to support more ranking methods. 438 | ~ v1.0.0: New. 439 | 440 | --- 441 | 442 | #### instancemethod `rank(__comparer, *, method=RankMethods.dense)` 443 | 444 | Parameters 445 | ~ *__comparer*: `Callable[[`[`TSource_co`](apiref.TSource_co)`, `[`TSource_co`](apiref.TSource_co)`], int]` 446 | ~ *method*: [`RankMethods`](apiref.RankMethods) 447 | 448 | Returns 449 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[int]` 450 | 451 | Ranks each item in the sequence in descending order using the given comparer and the 452 | method. 453 | 454 | Such comparer takes two values and return positive ints when lhs > rhs, negative ints 455 | if lhs < rhs, and 0 if they are equal. 456 | 457 | Revisions 458 | ~ v1.2.1: Added method parameter to support more ranking methods. 459 | ~ v1.0.0: New. 460 | 461 | --- 462 | 463 | #### instancemethod `rank_by[TSupportsLessThan](key_selector, *, method=RankMethods.dense)` 464 | 465 | Parameters 466 | ~ *key_selector*: `Callable[[`[`TSource_co`](apiref.TSource_co)`], `[`TSupportsLessThan`](apiref.TSupportsLessThan)`]` 467 | ~ *method*: [`RankMethods`](apiref.RankMethods) 468 | 469 | Returns 470 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[int]` 471 | 472 | Ranks each item in the sequence in descending order using the given selector and the 473 | method. 474 | 475 | Example 476 | ~ ```py 477 | >>> scores = [ 478 | ... {'name': 'Frank', 'score': 75}, 479 | ... {'name': 'Alica', 'score': 90}, 480 | ... {'name': 'Erika', 'score': 99}, 481 | ... {'name': 'Rogers', 'score': 90}, 482 | ... ] 483 | 484 | >>> MoreEnumerable(scores).rank_by(lambda x: x['score']) \ 485 | ... .zip(scores) \ 486 | ... .group_by(lambda t: t[0], lambda t: t[1]['name']) \ 487 | ... .to_dict(lambda g: g.key, lambda g: g.to_list()) 488 | {3: ['Frank'], 2: ['Alica', 'Rogers'], 1: ['Erika']} 489 | ``` 490 | 491 | Revisions 492 | ~ v1.2.1: Added method parameter to support more ranking methods. 493 | ~ v1.0.0: New. 494 | 495 | --- 496 | 497 | #### instancemethod `rank_by[TKey](key_selector, __comparer, *, method=RankMethods.dense)` 498 | 499 | Parameters 500 | ~ *key_selector*: `Callable[[`[`TSource_co`](apiref.TSource_co)`], `[`TKey`](apiref.TKey)`]` 501 | ~ *__comparer*: `Callable[[`[`TKey`](apiref.TKey)`, `[`TKey`](apiref.TKey)`], int]` 502 | ~ *method*: [`RankMethods`](apiref.RankMethods) 503 | 504 | Returns 505 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[int]` 506 | 507 | Ranks each item in the sequence in descending order using the given selector, comparer 508 | and the method. 509 | 510 | Such comparer takes two values and return positive ints when lhs > rhs, negative ints 511 | if lhs < rhs, and 0 if they are equal. 512 | 513 | Revisions 514 | ~ v1.2.1: Added method parameter to support more ranking methods. 515 | ~ v1.0.0: New. 516 | 517 | --- 518 | 519 | #### instancemethod `run_length_encode()` 520 | 521 | 522 | Returns 523 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[Tuple[`[`TSource_co`](apiref.TSource_co)`, int]]` 524 | 525 | Run-length encodes the sequence into a sequence of tuples where each tuple contains an 526 | (the first) element and its number of contingent occurrences, where equality is based on 527 | `==`. 528 | 529 | Example 530 | ~ ```py 531 | >>> MoreEnumerable('abbcaeeeaa').run_length_encode().to_list() 532 | [('a', 1), ('b', 2), ('c', 1), ('a', 1), ('e', 3), ('a', 2)] 533 | ``` 534 | 535 | Revisions 536 | ~ v1.1.0: New. 537 | 538 | --- 539 | 540 | #### instancemethod `run_length_encode(__comparer)` 541 | 542 | Parameters 543 | ~ *__comparer*: `Callable[[`[`TSource_co`](apiref.TSource_co)`, `[`TSource_co`](apiref.TSource_co)`], bool]` 544 | 545 | Returns 546 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[Tuple[`[`TSource_co`](apiref.TSource_co)`, int]]` 547 | 548 | Run-length encodes the sequence into a sequence of tuples where each tuple contains an 549 | (the first) element and its number of contingent occurrences, where equality is determined by 550 | the comparer. 551 | 552 | Example 553 | ~ ```py 554 | >>> MoreEnumerable('abBBbcaEeeff') \ 555 | >>> .run_length_encode(lambda x, y: x.lower() == y.lower()).to_list() 556 | [('a', 1), ('b', 4), ('c', 1), ('a', 1), ('E', 3), ('f', 2)] 557 | ``` 558 | 559 | Revisions 560 | ~ v1.1.0: New. 561 | 562 | --- 563 | 564 | #### instancemethod `scan(__transformation)` 565 | 566 | Parameters 567 | ~ *__transformation*: `Callable[[`[`TSource_co`](apiref.TSource_co)`, `[`TSource_co`](apiref.TSource_co)`], `[`TSource_co`](apiref.TSource_co)`]` 568 | 569 | Returns 570 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[`[`TSource_co`](apiref.TSource_co)`]` 571 | 572 | Performs a inclusive prefix sum over the sequence. Such scan returns an equal-length sequence 573 | where the i-th element is the sum of the first i elements in the original sequence. 574 | 575 | Example 576 | ~ ```py 577 | >>> values = [9, 4, 2, 5, 7] 578 | >>> MoreEnumerable(values).scan(lambda acc, e: acc + e).to_list() 579 | [9, 13, 15, 20, 27] 580 | >>> MoreEnumerable([]).scan(lambda acc, e: acc + e).to_list() 581 | [] 582 | ``` 583 | 584 | Example 585 | ~ ```py 586 | >>> # running max 587 | >>> fruits = ['apple', 'mango', 'orange', 'passionfruit', 'grape'] 588 | >>> MoreEnumerable(fruits).scan(lambda acc, e: e if len(e) > len(acc) else acc).to_list() 589 | ['apple', 'apple', 'orange', 'passionfruit', 'passionfruit'] 590 | ``` 591 | 592 | Revisions 593 | ~ v1.2.0: New. 594 | 595 | --- 596 | 597 | #### instancemethod `scan[TAccumulate](__seed, __transformation)` 598 | 599 | Parameters 600 | ~ *__seed*: [`TAccumulate`](apiref.TAccumulate) 601 | ~ *__transformation*: `Callable[[`[`TAccumulate`](apiref.TAccumulate)`, `[`TSource_co`](apiref.TSource_co)`], `[`TAccumulate`](apiref.TAccumulate)`]` 602 | 603 | Returns 604 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[`[`TAccumulate`](apiref.TAccumulate)`]` 605 | 606 | Like Enumerable.aggregate(seed, transformation) except that the intermediate results are 607 | included in the result sequence. 608 | 609 | Example 610 | ~ ```py 611 | >>> Enumerable.range(1, 5).as_more().scan(-1, lambda acc, e: acc * e).to_list() 612 | [-1, -1, -2, -6, -24, -120] 613 | ``` 614 | 615 | Revisions 616 | ~ v1.2.0: New. 617 | 618 | --- 619 | 620 | #### instancemethod `scan_right(__func)` 621 | 622 | Parameters 623 | ~ *__func*: `Callable[[`[`TSource_co`](apiref.TSource_co)`, `[`TSource_co`](apiref.TSource_co)`], `[`TSource_co`](apiref.TSource_co)`]` 624 | 625 | Returns 626 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[`[`TSource_co`](apiref.TSource_co)`]` 627 | 628 | Performs a right-associative inclusive prefix sum over the sequence. This is the 629 | right-associative version of MoreEnumerable.scan(func). 630 | 631 | Example 632 | ~ ```py 633 | >>> values = ['9', '4', '2', '5'] 634 | >>> MoreEnumerable(values).scan_right(lambda e, rr: f'({e}+{rr})').to_list() 635 | ['(9+(4+(2+5)))', '(4+(2+5))', '(2+5)', '5'] 636 | >>> MoreEnumerable([]).scan_right(lambda e, rr: e + rr).to_list() 637 | [] 638 | ``` 639 | 640 | Revisions 641 | ~ v1.2.0: New. 642 | 643 | --- 644 | 645 | #### instancemethod `scan_right[TAccumulate](__seed, __func)` 646 | 647 | Parameters 648 | ~ *__seed*: [`TAccumulate`](apiref.TAccumulate) 649 | ~ *__func*: `Callable[[`[`TSource_co`](apiref.TSource_co)`, `[`TAccumulate`](apiref.TAccumulate)`], `[`TAccumulate`](apiref.TAccumulate)`]` 650 | 651 | Returns 652 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[`[`TAccumulate`](apiref.TAccumulate)`]` 653 | 654 | The right-associative version of MoreEnumerable.scan(seed, func). 655 | 656 | Example 657 | ~ ```py 658 | >>> values = [9, 4, 2] 659 | >>> MoreEnumerable(values).scan_right('null', lambda e, rr: f'(cons {e} {rr})').to_list() 660 | ['(cons 9 (cons 4 (cons 2 null)))', '(cons 4 (cons 2 null))', '(cons 2 null)', 'null'] 661 | ``` 662 | 663 | Revisions 664 | ~ v1.2.0: New. 665 | 666 | --- 667 | 668 | #### instancemethod `segment(new_segment_predicate)` 669 | 670 | Parameters 671 | ~ *new_segment_predicate*: `Callable[[`[`TSource_co`](apiref.TSource_co)`], bool]` 672 | 673 | Returns 674 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[`[`MoreEnumerable`](apiref.MoreEnumerable)`[`[`TSource_co`](apiref.TSource_co)`]]` 675 | 676 | Splits the sequence into segments by using a detector function that returns True to signal a 677 | new segment. 678 | 679 | Example 680 | ~ ```py 681 | >>> values = [0, 1, 2, 4, -4, -2, 6, 2, -2] 682 | >>> MoreEnumerable(values).segment(lambda x: x < 0).select(lambda x: x.to_list()).to_list() 683 | [[0, 1, 2, 4], [-4], [-2, 6, 2], [-2]] 684 | ``` 685 | 686 | Revisions 687 | ~ v1.2.0: New. 688 | 689 | --- 690 | 691 | #### instancemethod `segment2(new_segment_predicate)` 692 | 693 | Parameters 694 | ~ *new_segment_predicate*: `Callable[[`[`TSource_co`](apiref.TSource_co)`, int], bool]` 695 | 696 | Returns 697 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[`[`MoreEnumerable`](apiref.MoreEnumerable)`[`[`TSource_co`](apiref.TSource_co)`]]` 698 | 699 | Splits the sequence into segments by using a detector function that returns True to signal a 700 | new segment. The element's index is used in the detector function. 701 | 702 | Example 703 | ~ ```py 704 | >>> values = [0, 1, 2, 4, -4, -2, 6, 2, -2] 705 | >>> MoreEnumerable(values).segment2(lambda x, i: x < 0 or i % 3 == 0) \ 706 | ... .select(lambda x: x.to_list()) \ 707 | ... .to_list() 708 | [[0, 1, 2], [4], [-4], [-2], [6, 2], [-2]] 709 | ``` 710 | 711 | Revisions 712 | ~ v1.2.0: New. 713 | 714 | --- 715 | 716 | #### instancemethod `segment3(new_segment_predicate)` 717 | 718 | Parameters 719 | ~ *new_segment_predicate*: `Callable[[`[`TSource_co`](apiref.TSource_co)`, `[`TSource_co`](apiref.TSource_co)`, int], bool]` 720 | 721 | Returns 722 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[`[`MoreEnumerable`](apiref.MoreEnumerable)`[`[`TSource_co`](apiref.TSource_co)`]]` 723 | 724 | Splits the sequence into segments by using a detector function that returns True to signal a 725 | new segment. The last element and the current element's index are used in the detector 726 | function. 727 | 728 | Example 729 | ~ ```py 730 | >>> values = [0, 1, 2, 4, -4, -2, 6, 2, -2] 731 | >>> MoreEnumerable(values).segment3(lambda curr, prev, i: curr * prev < 0) \ 732 | ... .select(lambda x: x.to_list()) \ 733 | ... .to_list() 734 | [[0, 1, 2, 4], [-4, -2], [6, 2], [-2]] 735 | ``` 736 | 737 | Revisions 738 | ~ v1.2.0: New. 739 | 740 | --- 741 | 742 | #### staticmethod `traverse_breath_first[TSource](root, children_selector)` 743 | 744 | Parameters 745 | ~ *root*: [`TSource`](apiref.TSource) 746 | ~ *children_selector*: `Callable[[`[`TSource`](apiref.TSource)`], Iterable[`[`TSource`](apiref.TSource)`]]` 747 | 748 | Returns 749 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[`[`TSource`](apiref.TSource)`]` 750 | 751 | Traverses the tree (graph) from the root node in a breath-first fashion. A selector is used to 752 | select children of each node. 753 | 754 | Graphs are not checked for cycles or duplicates visits. If the resulting sequence needs to be 755 | finite then it is the responsibility of children_selector to ensure that duplicate nodes are not 756 | visited. 757 | 758 | Example 759 | ~ ```py 760 | >>> tree = { 3: [1, 4], 1: [0, 2], 4: [5] } 761 | >>> MoreEnumerable.traverse_breath_first(3, lambda x: tree.get(x, [])) \ 762 | >>> .to_list() 763 | [3, 1, 4, 0, 2, 5] 764 | ``` 765 | 766 | --- 767 | 768 | #### staticmethod `traverse_depth_first[TSource](root, children_selector)` 769 | 770 | Parameters 771 | ~ *root*: [`TSource`](apiref.TSource) 772 | ~ *children_selector*: `Callable[[`[`TSource`](apiref.TSource)`], Iterable[`[`TSource`](apiref.TSource)`]]` 773 | 774 | Returns 775 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[`[`TSource`](apiref.TSource)`]` 776 | 777 | Traverses the tree (graph) from the root node in a depth-first fashion. A selector is used to 778 | select children of each node. 779 | 780 | Graphs are not checked for cycles or duplicates visits. If the resulting sequence needs to be 781 | finite then it is the responsibility of children_selector to ensure that duplicate nodes are not 782 | visited. 783 | 784 | Example 785 | ~ ```py 786 | >>> tree = { 3: [1, 4], 1: [0, 2], 4: [5] } 787 | >>> MoreEnumerable.traverse_depth_first(3, lambda x: tree.get(x, [])) \ 788 | >>> .to_list() 789 | [3, 1, 0, 2, 4, 5] 790 | ``` 791 | 792 | --- 793 | 794 | #### instancemethod `traverse_topological(children_selector)` 795 | 796 | Parameters 797 | ~ *children_selector*: `Callable[[`[`TSource_co`](apiref.TSource_co)`], Iterable[`[`TSource_co`](apiref.TSource_co)`]]` 798 | 799 | Returns 800 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[`[`TSource_co`](apiref.TSource_co)`]` 801 | 802 | Traverses the graph in topological order, A selector is used to select children of each 803 | node. The ordering created from this method is a variant of depth-first traversal and ensures 804 | duplicate nodes are output once. 805 | 806 | To invoke this method, the self sequence contains nodes with zero in-degrees to start the 807 | iteration. Passing a list of all nodes is allowed although not required. 808 | 809 | Raises [`DirectedGraphNotAcyclicError`](apiref.DirectedGraphNotAcyclicError) if the directed graph contains a cycle and the 810 | topological ordering cannot be produced. 811 | 812 | Example 813 | ~ ```py 814 | >>> adj = { 5: [2, 0], 4: [0, 1], 2: [3], 3: [1] } 815 | >>> MoreEnumerable([5, 4]).traverse_topological(lambda x: adj.get(x, [])) \ 816 | >>> .to_list() 817 | [5, 2, 3, 4, 0, 1] 818 | ``` 819 | 820 | Revisions 821 | ~ v1.2.1: New. 822 | 823 | --- 824 | 825 | #### instancemethod `traverse_topological2(children_selector, key_selector)` 826 | 827 | Parameters 828 | ~ *children_selector*: `Callable[[`[`TSource_co`](apiref.TSource_co)`], Iterable[`[`TSource_co`](apiref.TSource_co)`]]` 829 | ~ *key_selector*: `Callable[[`[`TSource_co`](apiref.TSource_co)`], object]` 830 | 831 | Returns 832 | ~ [`MoreEnumerable`](apiref.MoreEnumerable)`[`[`TSource_co`](apiref.TSource_co)`]` 833 | 834 | Traverses the graph in topological order, A selector is used to select children of each 835 | node. The ordering created from this method is a variant of depth-first traversal and 836 | ensures duplicate nodes are output once. A key selector is used to determine equality 837 | between nodes. 838 | 839 | To invoke this method, the self sequence contains nodes with zero in-degrees to start the 840 | iteration. Passing a list of all nodes is allowed although not required. 841 | 842 | Raises [`DirectedGraphNotAcyclicError`](apiref.DirectedGraphNotAcyclicError) if the directed graph contains a cycle and the 843 | topological ordering cannot be produced. 844 | 845 | Revisions 846 | ~ v1.2.1: New. 847 | 848 | -------------------------------------------------------------------------------- /doc/api/more/types_linq.more.more_enums.md: -------------------------------------------------------------------------------- 1 | # module ``types_linq.more.more_enums`` 2 | 3 | (apiref.RankMethods)= 4 | ## class `RankMethods` 5 | 6 | ```py 7 | from types_linq.more import RankMethods 8 | ``` 9 | 10 | Enumeration to select different methods of assigning rankings when breaking 11 | [ties](https://en.wikipedia.org/wiki/Ranking#Strategies_for_assigning_rankings). 12 | 13 | Revisions 14 | ~ v1.2.1: New. 15 | 16 | ### Bases 17 | 18 | - `Enum` 19 | 20 | ### Fields 21 | 22 | #### `dense` 23 | 24 | Equals 25 | ~ `auto()` 26 | 27 | Items that compare equally receive the same ranking, and the next items get the immediately 28 | following ranking. *(1223)* 29 | 30 | --- 31 | 32 | #### `competitive` 33 | 34 | Equals 35 | ~ `auto()` 36 | 37 | Items that compare equally receive the same highest ranking, and gaps are left out. *(1224)* 38 | 39 | --- 40 | 41 | #### `ordinal` 42 | 43 | Equals 44 | ~ `auto()` 45 | 46 | Each item receives unique rankings. *(1234)* 47 | 48 | -------------------------------------------------------------------------------- /doc/api/more/types_linq.more.more_error.md: -------------------------------------------------------------------------------- 1 | # module ``types_linq.more.more_error`` 2 | 3 | (apiref.DirectedGraphNotAcyclicError)= 4 | ## class `DirectedGraphNotAcyclicError` 5 | 6 | ```py 7 | from types_linq.more import DirectedGraphNotAcyclicError 8 | ``` 9 | 10 | Exception raised when a cycle exists in a graph. 11 | 12 | Revisions 13 | ~ v1.2.1: New. 14 | 15 | ### Bases 16 | 17 | - [`InvalidOperationError`](apiref.InvalidOperationError) 18 | 19 | ### Members 20 | 21 | #### instanceproperty `cycle` 22 | 23 | Returns 24 | ~ `Tuple[object, object]` 25 | 26 | The two elements (A, B) in this tuple are part of a cycle. There exists an edge from A to B, 27 | and a path from B back to A. A and B may be identical. 28 | 29 | Example 30 | ~ ```py 31 | >>> adj = { 5: [2, 0], 4: [0, 1], 2: [3], 3: [1, 5] } 32 | >>> try: 33 | >>> MoreEnumerable([5, 4]).traverse_topological(lambda x: adj.get(x, [])) \ 34 | >>> .consume() 35 | >>> except DirectedGraphNotAcyclicError as e: 36 | >>> print(e.cycle) 37 | (3, 5) # 3 -> 5 -> 2 -> 3 38 | ``` 39 | 40 | -------------------------------------------------------------------------------- /doc/api/types_linq.cached_enumerable.md: -------------------------------------------------------------------------------- 1 | # module ``types_linq.cached_enumerable`` 2 | 3 | (apiref.CachedEnumerable)= 4 | ## class `CachedEnumerable[TSource_co]` 5 | 6 | ```py 7 | from types_linq.cached_enumerable import CachedEnumerable 8 | ``` 9 | 10 | Enumerable that stores the enumerated results which can be accessed repeatedly. 11 | 12 | Users should not construct instances of this class directly. Use `Enumerable.as_cached()` instead. 13 | 14 | Revisions 15 | ~ v0.1.1: New. 16 | 17 | ### Bases 18 | 19 | - [`Enumerable`](apiref.Enumerable)`[`[`TSource_co`](apiref.TSource_co)`]` 20 | 21 | ### Members 22 | 23 | #### instancemethod `as_cached(*, cache_capacity=None)` 24 | 25 | Parameters 26 | ~ *cache_capacity*: `Optional[int]` 27 | 28 | Returns 29 | ~ [`CachedEnumerable`](apiref.CachedEnumerable)`[`[`TSource_co`](apiref.TSource_co)`]` 30 | 31 | Updates settings and returns the original CachedEnumerable reference. 32 | 33 | Raises [`InvalidOperationError`](apiref.InvalidOperationError) if cache_capacity is negative. 34 | 35 | -------------------------------------------------------------------------------- /doc/api/types_linq.grouping.md: -------------------------------------------------------------------------------- 1 | # module ``types_linq.grouping`` 2 | 3 | (apiref.Grouping)= 4 | ## class `Grouping[TValue_co, TKey_co]` 5 | 6 | ```py 7 | from types_linq.grouping import Grouping 8 | ``` 9 | 10 | Represents a collection of objects that have a common key. 11 | 12 | Users should not construct instances of this class directly. Use `Enumerable.group_by()` instead. 13 | 14 | ### Bases 15 | 16 | - [`Enumerable`](apiref.Enumerable)`[`[`TValue_co`](apiref.TValue_co)`]` 17 | - `Generic[`[`TKey_co`](apiref.TKey_co)`, `[`TValue_co`](apiref.TValue_co)`]` 18 | 19 | ### Members 20 | 21 | #### instanceproperty `key` 22 | 23 | Returns 24 | ~ [`TKey_co`](apiref.TKey_co) 25 | 26 | Gets the key of the grouping. 27 | 28 | -------------------------------------------------------------------------------- /doc/api/types_linq.lookup.md: -------------------------------------------------------------------------------- 1 | # module ``types_linq.lookup`` 2 | 3 | (apiref.Lookup)= 4 | ## class `Lookup[TKey_co, TValue_co]` 5 | 6 | ```py 7 | from types_linq.lookup import Lookup 8 | ``` 9 | 10 | A lookup is a one-to-many dictionary. It maps keys to Enumerable sequences of values. 11 | 12 | Users should not construct instances of this class directly. Use `Enumerable.to_lookup()` 13 | instead. 14 | 15 | ### Bases 16 | 17 | - [`Enumerable`](apiref.Enumerable)`[`[`Grouping`](apiref.Grouping)`[`[`TKey_co`](apiref.TKey_co)`, `[`TValue_co`](apiref.TValue_co)`]]` 18 | 19 | ### Members 20 | 21 | #### instanceproperty `count` 22 | 23 | Returns 24 | ~ `int` 25 | 26 | Gets the number of key-collection pairs. 27 | 28 | --- 29 | 30 | #### instancemethod `__contains__(value)` 31 | 32 | Parameters 33 | ~ *value*: `object` 34 | 35 | Returns 36 | ~ `bool` 37 | 38 | Tests whether key is in the lookup. 39 | 40 | --- 41 | 42 | #### instancemethod `__len__()` 43 | 44 | 45 | Returns 46 | ~ `int` 47 | 48 | Gets the number of key-collection pairs. 49 | 50 | --- 51 | 52 | #### instancemethod `__getitem__(key)` 53 | 54 | Parameters 55 | ~ *key*: [`TKey_co`](apiref.TKey_co) 56 | 57 | Returns 58 | ~ [`Enumerable`](apiref.Enumerable)`[`[`TValue_co`](apiref.TValue_co)`]` 59 | 60 | Gets the collection of values indexed by the specified key, or empty if no such key 61 | exists. 62 | 63 | --- 64 | 65 | #### instancemethod `apply_result_selector[TResult](result_selector)` 66 | 67 | Parameters 68 | ~ *result_selector*: `Callable[[`[`TKey_co`](apiref.TKey_co)`, `[`Enumerable`](apiref.Enumerable)`[`[`TValue_co`](apiref.TValue_co)`]], `[`TResult`](apiref.TResult)`]` 69 | 70 | Returns 71 | ~ [`Enumerable`](apiref.Enumerable)`[`[`TResult`](apiref.TResult)`]` 72 | 73 | Applies a transform function to each key and its associated values, then returns the 74 | results. 75 | 76 | --- 77 | 78 | #### instancemethod `contains(value)` 79 | 80 | Parameters 81 | ~ *value*: `object` 82 | 83 | Returns 84 | ~ `bool` 85 | 86 | Tests whether key is in the lookup. 87 | 88 | -------------------------------------------------------------------------------- /doc/api/types_linq.more_typing.md: -------------------------------------------------------------------------------- 1 | # module ``types_linq.more_typing`` 2 | 3 | Typing utilities used by methods's declarations across the library. For more details, see 4 | [`typing`](https://docs.python.org/3/library/typing.html). 5 | ```{note} Definitions in this module are for documenting purposes only. 6 | ``` 7 | 8 | ## Constants 9 | 10 | (apiref.TAccumulate)= 11 | ### `TAccumulate` 12 | 13 | Equals 14 | ~ `TypeVar('`[`TAccumulate`](apiref.TAccumulate)`')` 15 | 16 | A generic type parameter. 17 | 18 | --- 19 | 20 | (apiref.TAverage_co)= 21 | ### `TAverage_co` 22 | 23 | Equals 24 | ~ `TypeVar('`[`TAverage_co`](apiref.TAverage_co)`', covariant=True)` 25 | 26 | A generic covariant type parameter. 27 | 28 | --- 29 | 30 | (apiref.TCollection)= 31 | ### `TCollection` 32 | 33 | Equals 34 | ~ `TypeVar('`[`TCollection`](apiref.TCollection)`')` 35 | 36 | A generic type parameter. 37 | 38 | --- 39 | 40 | (apiref.TDefault)= 41 | ### `TDefault` 42 | 43 | Equals 44 | ~ `TypeVar('`[`TDefault`](apiref.TDefault)`')` 45 | 46 | A generic type parameter. 47 | 48 | --- 49 | 50 | (apiref.TInner)= 51 | ### `TInner` 52 | 53 | Equals 54 | ~ `TypeVar('`[`TInner`](apiref.TInner)`')` 55 | 56 | A generic type parameter. 57 | 58 | --- 59 | 60 | (apiref.TKey)= 61 | ### `TKey` 62 | 63 | Equals 64 | ~ `TypeVar('`[`TKey`](apiref.TKey)`')` 65 | 66 | A generic type parameter. 67 | 68 | --- 69 | 70 | (apiref.TKey2)= 71 | ### `TKey2` 72 | 73 | Equals 74 | ~ `TypeVar('`[`TKey2`](apiref.TKey2)`')` 75 | 76 | A generic type parameter. 77 | 78 | --- 79 | 80 | (apiref.TKey_co)= 81 | ### `TKey_co` 82 | 83 | Equals 84 | ~ `TypeVar('`[`TKey_co`](apiref.TKey_co)`', covariant=True)` 85 | 86 | A generic covariant type parameter. 87 | 88 | --- 89 | 90 | (apiref.TOther)= 91 | ### `TOther` 92 | 93 | Equals 94 | ~ `TypeVar('`[`TOther`](apiref.TOther)`')` 95 | 96 | A generic type parameter. 97 | 98 | --- 99 | 100 | (apiref.TOther2)= 101 | ### `TOther2` 102 | 103 | Equals 104 | ~ `TypeVar('`[`TOther2`](apiref.TOther2)`')` 105 | 106 | A generic type parameter. 107 | 108 | --- 109 | 110 | (apiref.TOther3)= 111 | ### `TOther3` 112 | 113 | Equals 114 | ~ `TypeVar('`[`TOther3`](apiref.TOther3)`')` 115 | 116 | A generic type parameter. 117 | 118 | --- 119 | 120 | (apiref.TOther4)= 121 | ### `TOther4` 122 | 123 | Equals 124 | ~ `TypeVar('`[`TOther4`](apiref.TOther4)`')` 125 | 126 | A generic type parameter. 127 | 128 | --- 129 | 130 | (apiref.TResult)= 131 | ### `TResult` 132 | 133 | Equals 134 | ~ `TypeVar('`[`TResult`](apiref.TResult)`')` 135 | 136 | A generic type parameter. 137 | 138 | --- 139 | 140 | (apiref.TSelf)= 141 | ### `TSelf` 142 | 143 | Equals 144 | ~ `TypeVar('`[`TSelf`](apiref.TSelf)`')` 145 | 146 | A generic type parameter. 147 | 148 | --- 149 | 150 | (apiref.TSource)= 151 | ### `TSource` 152 | 153 | Equals 154 | ~ `TypeVar('`[`TSource`](apiref.TSource)`')` 155 | 156 | A generic type parameter. 157 | 158 | --- 159 | 160 | (apiref.TSource_co)= 161 | ### `TSource_co` 162 | 163 | Equals 164 | ~ `TypeVar('`[`TSource_co`](apiref.TSource_co)`', covariant=True)` 165 | 166 | A generic covariant type parameter. 167 | 168 | --- 169 | 170 | (apiref.TValue)= 171 | ### `TValue` 172 | 173 | Equals 174 | ~ `TypeVar('`[`TValue`](apiref.TValue)`')` 175 | 176 | A generic type parameter. 177 | 178 | --- 179 | 180 | (apiref.TValue_co)= 181 | ### `TValue_co` 182 | 183 | Equals 184 | ~ `TypeVar('`[`TValue_co`](apiref.TValue_co)`', covariant=True)` 185 | 186 | A generic covariant type parameter. 187 | 188 | --- 189 | 190 | (apiref.TSupportsLessThan)= 191 | ### `TSupportsLessThan` 192 | 193 | Equals 194 | ~ `TypeVar('`[`TSupportsLessThan`](apiref.TSupportsLessThan)`', bound=`[`SupportsLessThan`](apiref.SupportsLessThan)`)` 195 | 196 | A generic type parameter that represents a type that [`SupportsLessThan`](apiref.SupportsLessThan). 197 | 198 | --- 199 | 200 | (apiref.TSupportsAdd)= 201 | ### `TSupportsAdd` 202 | 203 | Equals 204 | ~ `TypeVar('`[`TSupportsAdd`](apiref.TSupportsAdd)`', bound=`[`SupportsAdd`](apiref.SupportsAdd)`)` 205 | 206 | A generic type parameter that represents a type that [`SupportsAdd`](apiref.SupportsAdd). 207 | 208 | --- 209 | 210 | (apiref.SupportsAverage)= 211 | ## class `SupportsAverage[TAverage_co]` 212 | 213 | Instances of this protocol supports the averaging operation. that is, if `x` is such an instance, 214 | and `N` is an integer, then `(x + x + ...) / N` is allowed, and has the type [`TAverage_co`](apiref.TAverage_co). 215 | 216 | ### Bases 217 | 218 | - `Protocol[`[`TAverage_co`](apiref.TAverage_co)`]` 219 | 220 | ### Members 221 | 222 | #### abstract instancemethod `__add__[TSelf](__o)` 223 | 224 | Constraint 225 | ~ *self*: [`TSelf`](apiref.TSelf) 226 | 227 | Parameters 228 | ~ *__o*: [`TSelf`](apiref.TSelf) 229 | 230 | Returns 231 | ~ [`TSelf`](apiref.TSelf) 232 | 233 | 234 | 235 | --- 236 | 237 | #### abstract instancemethod `__truediv__(__o)` 238 | 239 | Parameters 240 | ~ *__o*: `int` 241 | 242 | Returns 243 | ~ [`TAverage_co`](apiref.TAverage_co) 244 | 245 | 246 | 247 | --- 248 | 249 | (apiref.SupportsLessThan)= 250 | ## class `SupportsLessThan` 251 | 252 | Instances of this protocol supports the `<` operation. 253 | 254 | Even though they may be unimplemented, the existence of `<` implies the existence of `>`, 255 | and probably `==`, `!=`, `<=` and `>=`. 256 | 257 | ### Bases 258 | 259 | - `Protocol` 260 | 261 | ### Members 262 | 263 | #### abstract instancemethod `__lt__(__o)` 264 | 265 | Parameters 266 | ~ *__o*: `Any` 267 | 268 | Returns 269 | ~ `bool` 270 | 271 | 272 | 273 | --- 274 | 275 | (apiref.SupportsAdd)= 276 | ## class `SupportsAdd` 277 | 278 | Instances of this protocol supports the homogeneous `+` operation. 279 | 280 | ### Bases 281 | 282 | - `Protocol` 283 | 284 | ### Members 285 | 286 | #### abstract instancemethod `__add__[TSelf](__o)` 287 | 288 | Constraint 289 | ~ *self*: [`TSelf`](apiref.TSelf) 290 | 291 | Parameters 292 | ~ *__o*: [`TSelf`](apiref.TSelf) 293 | 294 | Returns 295 | ~ [`TSelf`](apiref.TSelf) 296 | 297 | 298 | 299 | -------------------------------------------------------------------------------- /doc/api/types_linq.ordered_enumerable.md: -------------------------------------------------------------------------------- 1 | # module ``types_linq.ordered_enumerable`` 2 | 3 | (apiref.OrderedEnumerable)= 4 | ## class `OrderedEnumerable[TSource_co, TKey]` 5 | 6 | ```py 7 | from types_linq.ordered_enumerable import OrderedEnumerable 8 | ``` 9 | 10 | Represents a sorted Enumerable sequence that is sorted by some key. 11 | 12 | Users should not construct instances of this class directly. Use `Enumerable.order_by()` instead. 13 | 14 | ### Bases 15 | 16 | - [`Enumerable`](apiref.Enumerable)`[`[`TSource_co`](apiref.TSource_co)`]` 17 | - `Generic[`[`TSource_co`](apiref.TSource_co)`, `[`TKey`](apiref.TKey)`]` 18 | 19 | ### Members 20 | 21 | #### instancemethod `create_ordered_enumerable[TKey2](key_selector, comparer, descending)` 22 | 23 | Parameters 24 | ~ *key_selector*: `Callable[[`[`TSource_co`](apiref.TSource_co)`], `[`TKey2`](apiref.TKey2)`]` 25 | ~ *comparer*: `Optional[Callable[[`[`TKey2`](apiref.TKey2)`, `[`TKey2`](apiref.TKey2)`], int]]` 26 | ~ *descending*: `bool` 27 | 28 | Returns 29 | ~ [`OrderedEnumerable`](apiref.OrderedEnumerable)`[`[`TSource_co`](apiref.TSource_co)`, `[`TKey2`](apiref.TKey2)`]` 30 | 31 | Performs a subsequent ordering on the elements of the sequence according to a key. 32 | 33 | Comparer takes two values and return positive ints when lhs > rhs, negative ints 34 | if lhs < rhs, and 0 if they are equal. 35 | 36 | Revisions 37 | ~ v0.1.2: Fixed incorrect parameter type of comparer. 38 | 39 | --- 40 | 41 | #### instancemethod `then_by[TSupportsLessThan](key_selector)` 42 | 43 | Parameters 44 | ~ *key_selector*: `Callable[[`[`TSource_co`](apiref.TSource_co)`], `[`TSupportsLessThan`](apiref.TSupportsLessThan)`]` 45 | 46 | Returns 47 | ~ [`OrderedEnumerable`](apiref.OrderedEnumerable)`[`[`TSource_co`](apiref.TSource_co)`, `[`TSupportsLessThan`](apiref.TSupportsLessThan)`]` 48 | 49 | Performs a subsequent ordering of the elements in ascending order according to key. 50 | 51 | Example 52 | ~ ```py 53 | >>> class Pet(NamedTuple): 54 | ... name: str 55 | ... age: int 56 | 57 | >>> pets = [Pet('Barley', 8), Pet('Boots', 4), Pet('Roman', 5), Pet('Daisy', 4)] 58 | >>> Enumerable(pets).order_by(lambda p: p.age) \ 59 | ... .then_by(lambda p: p.name) \ 60 | ... .select(lambda p: p.name) \ 61 | ... .to_list() 62 | ['Boots', 'Daisy', 'Roman', 'Barley'] 63 | ``` 64 | 65 | --- 66 | 67 | #### instancemethod `then_by[TKey2](key_selector, __comparer)` 68 | 69 | Parameters 70 | ~ *key_selector*: `Callable[[`[`TSource_co`](apiref.TSource_co)`], `[`TKey2`](apiref.TKey2)`]` 71 | ~ *__comparer*: `Callable[[`[`TKey2`](apiref.TKey2)`, `[`TKey2`](apiref.TKey2)`], int]` 72 | 73 | Returns 74 | ~ [`OrderedEnumerable`](apiref.OrderedEnumerable)`[`[`TSource_co`](apiref.TSource_co)`, `[`TKey2`](apiref.TKey2)`]` 75 | 76 | Performs a subsequent ordering of the elements in ascending order by using a specified comparer. 77 | 78 | Such comparer takes two values and return positive ints when lhs > rhs, negative ints 79 | if lhs < rhs, and 0 if they are equal. 80 | 81 | --- 82 | 83 | #### instancemethod `then_by_descending[TSupportsLessThan](key_selector)` 84 | 85 | Parameters 86 | ~ *key_selector*: `Callable[[`[`TSource_co`](apiref.TSource_co)`], `[`TSupportsLessThan`](apiref.TSupportsLessThan)`]` 87 | 88 | Returns 89 | ~ [`OrderedEnumerable`](apiref.OrderedEnumerable)`[`[`TSource_co`](apiref.TSource_co)`, `[`TSupportsLessThan`](apiref.TSupportsLessThan)`]` 90 | 91 | Performs a subsequent ordering of the elements in descending order according to key. 92 | 93 | --- 94 | 95 | #### instancemethod `then_by_descending[TKey2](key_selector, __comparer)` 96 | 97 | Parameters 98 | ~ *key_selector*: `Callable[[`[`TSource_co`](apiref.TSource_co)`], `[`TKey2`](apiref.TKey2)`]` 99 | ~ *__comparer*: `Callable[[`[`TKey2`](apiref.TKey2)`, `[`TKey2`](apiref.TKey2)`], int]` 100 | 101 | Returns 102 | ~ [`OrderedEnumerable`](apiref.OrderedEnumerable)`[`[`TSource_co`](apiref.TSource_co)`, `[`TKey2`](apiref.TKey2)`]` 103 | 104 | Performs a subsequent ordering of the elements in descending order by using a specified comparer. 105 | 106 | Such comparer takes two values and return positive ints when lhs > rhs, negative ints 107 | if lhs < rhs, and 0 if they are equal. 108 | 109 | -------------------------------------------------------------------------------- /doc/api/types_linq.types_linq_error.md: -------------------------------------------------------------------------------- 1 | # module ``types_linq.types_linq_error`` 2 | 3 | (apiref.TypesLinqError)= 4 | ## class `TypesLinqError` 5 | 6 | ```py 7 | from types_linq import TypesLinqError 8 | ``` 9 | 10 | Types-linq has run into problems. 11 | 12 | ### Bases 13 | 14 | - `Exception` 15 | 16 | --- 17 | 18 | (apiref.InvalidOperationError)= 19 | ## class `InvalidOperationError` 20 | 21 | ```py 22 | from types_linq import InvalidOperationError 23 | ``` 24 | 25 | Exception raised when a call is invalid for the object's current state. 26 | 27 | ### Bases 28 | 29 | - [`TypesLinqError`](apiref.TypesLinqError) 30 | - `ValueError` 31 | 32 | --- 33 | 34 | (apiref.IndexOutOfRangeError)= 35 | ## class `IndexOutOfRangeError` 36 | 37 | ```py 38 | from types_linq import IndexOutOfRangeError 39 | ``` 40 | 41 | An `IndexError` with types-linq flavour. 42 | 43 | ### Bases 44 | 45 | - [`TypesLinqError`](apiref.TypesLinqError) 46 | - `IndexError` 47 | 48 | -------------------------------------------------------------------------------- /doc/api_spec.py: -------------------------------------------------------------------------------- 1 | from typing import TypedDict 2 | 3 | 4 | class ModuleSpec(TypedDict): 5 | file_path: str 6 | name: str 7 | gvs: set[str] 8 | classes: dict[str, 'ClassSpec'] 9 | 10 | class ClassSpec(TypedDict): 11 | fields: set[str] 12 | methods: set[str] 13 | readonly_properties: set[str] 14 | 15 | # ========================================================== 16 | # This describes which APIs are exported for doc 17 | 18 | _path = '../types_linq' 19 | 20 | _project = 'types_linq' 21 | 22 | type_file = f'{_path}/more_typing.py' 23 | 24 | modules: list[ModuleSpec] = [ 25 | { 26 | 'file_path': f'{_path}/cached_enumerable.py', 27 | 'name': f'{_project}.cached_enumerable', 28 | 'gvs': {*()}, 29 | 'classes': { 30 | 'CachedEnumerable': { 31 | 'fields': {*()}, 32 | 'methods': { 33 | 'as_cached', 34 | }, 35 | 'readonly_properties': {*()}, 36 | }, 37 | }, 38 | }, 39 | { 40 | 'file_path': f'{_path}/enumerable.pyi', 41 | 'name': f'{_project}.enumerable', 42 | 'gvs': {*()}, 43 | 'classes': { 44 | 'Enumerable': { 45 | 'fields': {*()}, 46 | 'methods': { 47 | '__init__', 48 | '__contains__', 49 | '__getitem__', 50 | '__iter__', 51 | '__len__', 52 | '__reversed__', 53 | 'aggregate', 54 | 'all', 55 | 'any', 56 | 'append', 57 | 'as_cached', 58 | 'as_more', 59 | 'average', 60 | 'average2', 61 | 'cast', 62 | 'chunk', 63 | 'concat', 64 | 'contains', 65 | 'count', 66 | 'default_if_empty', 67 | 'distinct', 68 | 'distinct_by', 69 | 'element_at', 70 | 'empty', 71 | 'except1', 72 | 'except_by', 73 | 'first', 74 | 'first2', 75 | 'group_by', 76 | 'group_by2', 77 | 'group_join', 78 | 'intersect', 79 | 'intersect_by', 80 | 'join', 81 | 'last', 82 | 'last2', 83 | 'max', 84 | 'max2', 85 | 'max_by', 86 | 'min', 87 | 'min2', 88 | 'min_by', 89 | 'of_type', 90 | 'order_by', 91 | 'order_by_descending', 92 | 'prepend', 93 | 'range', 94 | 'repeat', 95 | 'reverse', 96 | 'select', 97 | 'select2', 98 | 'select_many', 99 | 'select_many2', 100 | 'sequence_equal', 101 | 'single', 102 | 'single2', 103 | 'skip', 104 | 'skip_last', 105 | 'skip_while', 106 | 'skip_while2', 107 | 'sum', 108 | 'sum2', 109 | 'take', 110 | 'take_last', 111 | 'take_while', 112 | 'take_while2', 113 | 'to_dict', 114 | 'to_set', 115 | 'to_list', 116 | 'to_lookup', 117 | 'union', 118 | 'union_by', 119 | 'where', 120 | 'where2', 121 | 'zip', 122 | 'zip2', 123 | 'elements_in', 124 | 'to_tuple', 125 | }, 126 | 'readonly_properties': {*()}, 127 | }, 128 | }, 129 | }, 130 | { 131 | 'file_path': f'{_path}/grouping.py', 132 | 'name': f'{_project}.grouping', 133 | 'gvs': {*()}, 134 | 'classes': { 135 | 'Grouping': { 136 | 'fields': {*()}, 137 | 'methods': {*()}, 138 | 'readonly_properties': { 139 | 'key', 140 | }, 141 | }, 142 | }, 143 | }, 144 | { 145 | 'file_path': f'{_path}/lookup.py', 146 | 'name': f'{_project}.lookup', 147 | 'gvs': {*()}, 148 | 'classes': { 149 | 'Lookup': { 150 | 'fields': {*()}, 151 | 'methods': { 152 | '__contains__', 153 | '__len__', 154 | '__getitem__', 155 | 'apply_result_selector', 156 | 'contains', 157 | }, 158 | 'readonly_properties': { 159 | 'count', 160 | }, 161 | }, 162 | }, 163 | }, 164 | { 165 | 'file_path': f'{_path}/more_typing.py', 166 | 'name': f'{_project}.more_typing', 167 | 'gvs': { 168 | 'TAccumulate', 169 | 'TAverage_co', 170 | 'TCollection', 171 | 'TDefault', 172 | 'TInner', 173 | 'TKey', 174 | 'TKey2', 175 | 'TKey_co', 176 | 'TOther', 177 | 'TOther2', 178 | 'TOther3', 179 | 'TOther4', 180 | 'TResult', 181 | 'TSelf', 182 | 'TSource', 183 | 'TSource_co', 184 | 'TValue', 185 | 'TValue_co', 186 | 'TSupportsLessThan', 187 | 'TSupportsAdd', 188 | }, 189 | 'classes': { 190 | 'SupportsAverage': { 191 | 'fields': {*()}, 192 | 'methods': { 193 | '__add__', 194 | '__truediv__', 195 | }, 196 | 'readonly_properties': {*()}, 197 | }, 198 | 'SupportsLessThan': { 199 | 'fields': {*()}, 200 | 'methods': { 201 | '__lt__', 202 | }, 203 | 'readonly_properties': {*()}, 204 | }, 205 | 'SupportsAdd': { 206 | 'fields': {*()}, 207 | 'methods': { 208 | '__add__', 209 | }, 210 | 'readonly_properties': {*()}, 211 | }, 212 | }, 213 | }, 214 | { 215 | 'file_path': f'{_path}/ordered_enumerable.pyi', 216 | 'name': f'{_project}.ordered_enumerable', 217 | 'gvs': {*()}, 218 | 'classes': { 219 | 'OrderedEnumerable': { 220 | 'fields': {*()}, 221 | 'methods': { 222 | 'create_ordered_enumerable', 223 | 'then_by', 224 | 'then_by_descending', 225 | }, 226 | 'readonly_properties': {*()}, 227 | } 228 | }, 229 | }, 230 | { 231 | 'file_path': f'{_path}/types_linq_error.py', 232 | 'name': f'{_project}.types_linq_error', 233 | 'gvs': {*()}, 234 | 'classes': { 235 | 'TypesLinqError': { 236 | 'fields': {*()}, 237 | 'methods': {*()}, 238 | 'readonly_properties': {*()}, 239 | }, 240 | 'InvalidOperationError': { 241 | 'fields': {*()}, 242 | 'methods': {*()}, 243 | 'readonly_properties': {*()}, 244 | }, 245 | 'IndexOutOfRangeError': { 246 | 'fields': {*()}, 247 | 'methods': {*()}, 248 | 'readonly_properties': {*()}, 249 | }, 250 | }, 251 | }, 252 | { 253 | 'file_path': f'{_path}/more/more_enumerable.pyi', 254 | 'name': f'{_project}.more.more_enumerable', 255 | 'gvs': {*()}, 256 | 'classes': { 257 | 'MoreEnumerable': { 258 | 'fields': {*()}, 259 | 'methods': { 260 | 'aggregate_right', 261 | 'as_more', 262 | 'consume', 263 | 'cycle', 264 | 'enumerate', 265 | 'except_by2', 266 | 'flatten', 267 | 'flatten2', 268 | 'for_each', 269 | 'for_each2', 270 | 'interleave', 271 | 'maxima_by', 272 | 'minima_by', 273 | 'pipe', 274 | 'pre_scan', 275 | 'rank', 276 | 'rank_by', 277 | 'run_length_encode', 278 | 'scan', 279 | 'scan_right', 280 | 'segment', 281 | 'segment2', 282 | 'segment3', 283 | 'traverse_breath_first', 284 | 'traverse_depth_first', 285 | 'traverse_topological', 286 | 'traverse_topological2', 287 | }, 288 | 'readonly_properties': {*()}, 289 | }, 290 | }, 291 | }, 292 | { 293 | 'file_path': f'{_path}/more/extrema_enumerable.pyi', 294 | 'name': f'{_project}.more.extrema_enumerable', 295 | 'gvs': {*()}, 296 | 'classes': { 297 | 'ExtremaEnumerable': { 298 | 'fields': {*()}, 299 | 'methods': { 300 | 'take', 301 | 'take_last', 302 | }, 303 | 'readonly_properties': {*()}, 304 | }, 305 | }, 306 | }, 307 | { 308 | 'file_path': f'{_path}/more/more_enums.py', 309 | 'name': f'{_project}.more.more_enums', 310 | 'gvs': {*()}, 311 | 'classes': { 312 | 'RankMethods': { 313 | 'fields': { 314 | 'dense', 315 | 'competitive', 316 | 'ordinal', 317 | }, 318 | 'methods': {*()}, 319 | 'readonly_properties': {*()}, 320 | }, 321 | }, 322 | }, 323 | { 324 | 'file_path': f'{_path}/more/more_error.py', 325 | 'name': f'{_project}.more.more_error', 326 | 'gvs': {*()}, 327 | 'classes': { 328 | 'DirectedGraphNotAcyclicError': { 329 | 'fields': {*()}, 330 | 'methods': {*()}, 331 | 'readonly_properties': { 332 | 'cycle', 333 | }, 334 | }, 335 | }, 336 | }, 337 | ] 338 | -------------------------------------------------------------------------------- /doc/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 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'types-linq' 21 | copyright = '2023, cleoold' 22 | author = 'cleoold' 23 | 24 | 25 | # -- General configuration --------------------------------------------------- 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be 28 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 29 | # ones. 30 | extensions = [ 31 | 'myst_parser', 32 | ] 33 | 34 | # Add any paths that contain templates here, relative to this directory. 35 | templates_path = ['_templates'] 36 | 37 | # List of patterns, relative to source directory, that match files and 38 | # directories to ignore when looking for source files. 39 | # This pattern also affects html_static_path and html_extra_path. 40 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 41 | 42 | 43 | # -- Options for HTML output ------------------------------------------------- 44 | 45 | # The theme to use for HTML and HTML Help pages. See the documentation for 46 | # a list of builtin themes. 47 | # 48 | html_theme = 'sphinx_rtd_theme' 49 | 50 | # Add any paths that contain custom static files (such as style sheets) here, 51 | # relative to this directory. They are copied after the builtin static files, 52 | # so a file named "default.css" will overwrite the builtin "default.css". 53 | html_static_path = ['_static'] 54 | 55 | html_css_files = [ 56 | 'override.css', 57 | ] 58 | 59 | myst_enable_extensions = [ 60 | 'deflist', 61 | ] 62 | -------------------------------------------------------------------------------- /doc/gen_api_doc.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import os 3 | import re 4 | from itertools import chain 5 | from dataclasses import dataclass 6 | from textwrap import dedent, indent 7 | from typing import Any, Optional, Union 8 | 9 | import api_spec 10 | 11 | 12 | LINK_PREFIX = 'apiref.' 13 | 14 | 15 | @dataclass 16 | class MyParam: 17 | name: str 18 | tp: str 19 | default: Optional[str] 20 | 21 | def decl(self): 22 | if self.default is None: 23 | return self.name 24 | return f'{self.name}={self.default}' 25 | 26 | 27 | @dataclass 28 | class MyMethodDef: 29 | name: str 30 | tparams: list[str] 31 | params: list[MyParam] 32 | kwonlyparams: list[MyParam] 33 | self_type: str 34 | return_type: str 35 | comment: str 36 | is_static: bool 37 | is_abstract: bool 38 | 39 | def markdown(self): 40 | builder = ['#### '] 41 | if self.is_abstract: 42 | builder.append('abstract ') 43 | if self.is_static: 44 | builder.append('staticmethod ') 45 | else: 46 | builder.append('instancemethod ') 47 | builder.append(f'`{self.name}') 48 | if self.tparams: 49 | builder.append(f'[{", ".join(t for t in self.tparams)}]') 50 | builder.append('(') 51 | builder.append(', '.join(p.decl() for p in self.params)) 52 | if self.kwonlyparams: 53 | if not self.params: 54 | builder.append('*, ') 55 | elif self.params and self.params[-1].name.startswith('*'): 56 | builder.append(', ') 57 | elif self.params: 58 | builder.append(', *, ') 59 | for p in self.kwonlyparams: 60 | builder.append(', '.join(p.decl() for p in self.kwonlyparams)) 61 | builder.append(')`\n\n') 62 | 63 | builder = [''.join(builder)] 64 | 65 | if self.self_type: 66 | builder.append('Constraint\n') 67 | builder.append(f' ~ *self*: {rewrite_code_with_links(self.self_type)}\n\n') 68 | 69 | if self.params or self.kwonlyparams: 70 | builder.append('Parameters\n') 71 | for p in chain(self.params, self.kwonlyparams): 72 | builder.append(f' ~ *{p.name}*: {rewrite_code_with_links(p.tp)}\n') 73 | builder.append('\n') 74 | 75 | builder.append('Returns\n') 76 | builder.append(f' ~ {rewrite_code_with_links(self.return_type)}\n\n') 77 | builder.append(f'{self.comment}\n\n') 78 | return ''.join(builder) 79 | 80 | 81 | @dataclass 82 | class MyPropertyDef: 83 | name: str 84 | return_type: str 85 | comment: str 86 | 87 | def markdown(self): 88 | builder = [] 89 | builder.append(f'#### instanceproperty `{self.name}`\n\n') 90 | 91 | builder.append('Returns\n') 92 | builder.append(f' ~ {rewrite_code_with_links(self.return_type)}\n\n') 93 | builder.append(f'{self.comment}\n\n') 94 | return ''.join(builder) 95 | 96 | 97 | @dataclass 98 | class MyClassDef: 99 | name: str 100 | tparams: list[str] 101 | bases: list[str] 102 | comment: str 103 | fields: list['MyVariableDef'] 104 | methods: list[MyMethodDef] 105 | properties: list[MyPropertyDef] 106 | 107 | def typename(self): 108 | builder = [self.name] 109 | if self.tparams: 110 | builder.append(f'[{", ".join(self.tparams)}]') 111 | return ''.join(builder) 112 | 113 | def markdown(self): 114 | builder = [] 115 | builder.append(f'({LINK_PREFIX}{self.name})=\n') 116 | builder.append(f'## class `{self.typename()}`\n\n') 117 | 118 | builder.append(f'{self.comment}\n\n') 119 | 120 | builder.append('### Bases\n\n') 121 | for base in self.bases: 122 | builder.append(f'- {rewrite_code_with_links(base)}\n') 123 | builder.append('\n') 124 | 125 | builder.append(title := '### Fields\n\n') 126 | for field in self.fields: 127 | builder.append(field.markdown(tlevel=4, link=False)) 128 | builder.append('---\n\n') 129 | if builder[-1] == '---\n\n' or builder[-1] == title: 130 | builder.pop() 131 | 132 | builder.append(title := '### Members\n\n') 133 | for method in chain(self.properties, self.methods): 134 | builder.append(method.markdown()) 135 | builder.append('---\n\n') 136 | if builder[-1] == '---\n\n' or builder[-1] == title: 137 | builder.pop() 138 | return ''.join(builder) 139 | 140 | 141 | @dataclass 142 | class MyVariableDef: 143 | name: str 144 | comment: str 145 | value: str # currently module constants are only used as type or enum variables... 146 | # so value is fine 147 | 148 | def markdown(self, tlevel: int, link: bool): 149 | builder = [] 150 | if link: 151 | builder.append(f'({LINK_PREFIX}{self.name})=\n') 152 | builder.append(f'{"#" * tlevel} `{self.name}`\n\n') 153 | builder.append('Equals\n') 154 | builder.append(f' ~ {rewrite_code_with_links(self.value)}\n\n') 155 | builder.append(f'{self.comment}\n\n') 156 | return ''.join(builder) 157 | 158 | 159 | class ModuleVisitor(ast.NodeVisitor): 160 | def __init__(self, mspec: api_spec.ModuleSpec) -> None: 161 | super().__init__() 162 | self.mspec = mspec 163 | self.module_string = '' 164 | self.global_vars: list[MyVariableDef] = [] 165 | self.classes: list[MyClassDef] = [] 166 | 167 | self.found_gvs: set[str] = {*()} 168 | self.found_classes: set[str] = {*()} 169 | 170 | def visit_root(self, root: ast.Module): 171 | # the first node may be a docstring 172 | if doc := get_def_docstring(root): 173 | self.module_string = rewrite_comments(doc) 174 | 175 | # report assignments (with trailing docstring) 176 | for i in range(len(root.body)): 177 | if not isinstance(assign := root.body[i], ast.Assign): 178 | continue 179 | maybe_doc = None 180 | if i + 1 < len(root.body): 181 | maybe_doc = root.body[i + 1] 182 | 183 | assert isinstance(target := assign.targets[0], ast.Name) 184 | name = target.id 185 | print(f' Found assignment {name}') 186 | self.found_gvs.add(name) 187 | if name not in self.mspec['gvs']: 188 | continue 189 | self.global_vars.append(get_variable(assign, maybe_doc)) 190 | 191 | # report classes 192 | classdefs = (c for c in root.body if isinstance(c, ast.ClassDef)) 193 | for classdef in classdefs: 194 | self.visit_ClassDef(classdef) 195 | 196 | 197 | # top level class definition 198 | def visit_ClassDef(self, node: ast.ClassDef) -> Any: 199 | classname = node.name 200 | print(f' Found class {classname}') 201 | self.found_classes.add(classname) 202 | if classname not in self.mspec['classes']: 203 | return 204 | 205 | classspec = self.mspec['classes'][classname] 206 | 207 | class_tparams = get_tparam_for_class(node) 208 | classdef = MyClassDef( 209 | name=classname, 210 | tparams=class_tparams, 211 | bases=[ast.unparse(n) for n in node.bases], 212 | comment=rewrite_comments(get_def_docstring(node)), 213 | fields=[], 214 | methods=[], 215 | properties=[], 216 | ) 217 | 218 | found_varnames = {*()} 219 | found_funnames = {*()} 220 | 221 | for i, stmt in enumerate(node.body): 222 | if isinstance(stmt, ast.FunctionDef): 223 | defname = stmt.name 224 | print(f' Found def {defname}()') 225 | found_funnames.add(defname) 226 | if defname in classspec['methods']: 227 | classdef.methods.append(get_method(stmt, class_tparams)) 228 | if defname in classspec['readonly_properties']: 229 | classdef.properties.append(get_property(stmt)) 230 | elif isinstance(stmt, ast.Assign): 231 | maybe_doc = None 232 | if i + 1 < len(node.body): 233 | maybe_doc = node.body[i + 1] 234 | 235 | assert isinstance(target := stmt.targets[0], ast.Name) 236 | varname = target.id 237 | print(f' Found assignment {varname}') 238 | found_varnames.add(varname) 239 | if varname in classspec['fields']: 240 | classdef.fields.append(get_variable(stmt, maybe_doc)) 241 | 242 | enabled_varnames = set(v.name for v in classdef.fields) 243 | enabled_funnames = set(m.name for m in chain(classdef.methods, classdef.properties)) 244 | expected_funnames = classspec["methods"] | classspec["readonly_properties"] 245 | print(f' STAT Found vars - enabled vars : {psetdiff(found_varnames, enabled_varnames)}') 246 | print(f' STAT Expected vars - found vars: {psetdiff(classspec["fields"], found_varnames)}') 247 | print(f' STAT Found defs - enabled defs : {psetdiff(found_funnames, enabled_funnames)}') 248 | print(f' STAT Expected defs - found defs: {psetdiff(expected_funnames ,found_funnames)}') 249 | self.classes.append(classdef) 250 | 251 | # such logging is necessary to check which symbols should be exposed but forgotten in api_spec.py, 252 | # or vice versa 253 | def report_found_globals(self): 254 | enabled_varnames = set(a.name for a in self.global_vars) 255 | print(f' STAT Found gvs - enabled vars : {psetdiff(self.found_gvs, enabled_varnames)}') 256 | print(f' STAT Expected gvs - found vars: {psetdiff(self.mspec["gvs"], self.found_gvs)}') 257 | enabled_classnames = set(c.name for c in self.classes) 258 | print(f' STAT Found classes - enabled classes : {psetdiff(self.found_classes, enabled_classnames)}') 259 | print(f' STAT Expected classes - found classes: {psetdiff(set(self.mspec["classes"].keys()), self.found_classes)}') 260 | 261 | 262 | def psetdiff(a: set[str], b: set[str]): 263 | s = ', '.join(a - b) 264 | return f'{{{s}}}' 265 | 266 | 267 | def get_variable(assign: ast.Assign, maybe_doc: Optional[ast.AST]): 268 | assert len(assign.targets) == 1 269 | target = assign.targets[0] 270 | assert isinstance(target, ast.Name) 271 | doc = node_is_constant_str(maybe_doc) if maybe_doc is not None else '' 272 | return MyVariableDef( 273 | name=target.id, 274 | comment=rewrite_comments(doc), 275 | value=ast.unparse(assign.value), 276 | ) 277 | 278 | 279 | def get_method(fun_def: ast.FunctionDef, class_tparams: list[str]): 280 | # class tparam is not part of method's tparam 281 | is_static = find_decorator(fun_def, 'staticmethod') 282 | 283 | pos_args = [MyParam( 284 | name=arg.arg, 285 | tp=ast.unparse(arg.annotation) if arg.annotation else '', 286 | default=None, 287 | ) for arg in fun_def.args.args 288 | ] 289 | 290 | # fill in default vals for pos args 291 | if defaults := fun_def.args.defaults: 292 | for default, arg in zip(reversed(defaults), reversed(pos_args)): 293 | arg.default = ast.unparse(default) 294 | 295 | # remove self from args if not static and not constrained 296 | self_type = '' 297 | if not is_static and pos_args[0].name == 'self': 298 | self_type = pos_args[0].tp or '' 299 | del pos_args[0] 300 | 301 | # vaarg to the end of args 302 | if vaarg := fun_def.args.vararg: 303 | pos_args.append(MyParam( 304 | name=f'*{vaarg.arg}', 305 | tp=ast.unparse(vaarg.annotation), 306 | default=None, 307 | )) 308 | 309 | kwonlyargs = [MyParam( 310 | name=arg.arg, 311 | tp=ast.unparse(arg.annotation) if arg.annotation else '', 312 | default=None, 313 | ) for arg in fun_def.args.kwonlyargs 314 | ] 315 | 316 | # fill in default vals for kwonlyargs 317 | if kw_defaults := fun_def.args.kw_defaults: 318 | for default, arg in zip(reversed(kw_defaults), reversed(kwonlyargs)): 319 | arg.default = ast.unparse(default) if default else None 320 | 321 | return MyMethodDef( 322 | name=fun_def.name, 323 | tparams=get_tparam_for_method(fun_def, class_tparams), 324 | params=pos_args, 325 | kwonlyparams=kwonlyargs, 326 | self_type=self_type, 327 | return_type=ast.unparse(fun_def.returns), 328 | comment=rewrite_comments(get_def_docstring(fun_def)), 329 | is_static=is_static, 330 | is_abstract=find_decorator(fun_def, 'abstractmethod'), 331 | ) 332 | 333 | 334 | def get_property(fun_def: ast.FunctionDef): 335 | return MyPropertyDef( 336 | name=fun_def.name, 337 | return_type=ast.unparse(fun_def.returns), 338 | comment=rewrite_comments(get_def_docstring(fun_def)), 339 | ) 340 | 341 | 342 | def node_is_constant_str(node: ast.AST): 343 | if isinstance(node, ast.Expr) and \ 344 | isinstance(const := node.value, ast.Constant) and \ 345 | isinstance(s := const.value, str): 346 | return s 347 | return '' 348 | 349 | 350 | def get_def_docstring(any_def: Union[ast.FunctionDef, ast.ClassDef, ast.Module]): 351 | return node_is_constant_str(any_def.body[0]) 352 | 353 | 354 | def find_decorator(any_def: Union[ast.FunctionDef, ast.ClassDef], name: str): 355 | return any(d.id == name for d in any_def.decorator_list) 356 | 357 | 358 | class TParamFinder(ast.NodeVisitor): 359 | def __init__(self) -> None: 360 | super().__init__() 361 | self.tparams = [] 362 | 363 | def visit_Name(self, node: ast.Name) -> Any: 364 | if is_tparam(node.id) and node.id not in self.tparams: 365 | self.tparams.append(node.id) 366 | 367 | 368 | def get_tparam_for_class(class_def: ast.ClassDef): 369 | v = TParamFinder() 370 | for expr in class_def.bases: 371 | v.visit(expr) 372 | return v.tparams 373 | 374 | 375 | def get_tparam_for_method(fun_def: ast.FunctionDef, class_tparams: list[str]): 376 | v = TParamFinder() 377 | v.visit(fun_def.args) 378 | tparams = [p for p in v.tparams if p not in class_tparams] 379 | return tparams 380 | 381 | 382 | def rewrite_comments(s: str): 383 | s = dedent(s).strip() 384 | # rewrite the code Example blocks from markdown to myst's deflist 385 | eight = ' ' * 8 386 | s = re.sub( 387 | r'^Example\s```(.*?)\n(.*?)```$', 388 | lambda mat: f'Example\n ~ ```{mat.group(1)}\n{indent(mat.group(2), eight)}{eight}```', 389 | s, 390 | flags=re.DOTALL | re.MULTILINE, 391 | ) 392 | # rewrite code literals with their links if possible 393 | return re.sub( 394 | r'`([^\d\W][\w\d]*?)`', 395 | lambda mat: f'[`{name}`]({LINK_PREFIX}{name})' \ 396 | if (name := mat.group(1)) in linkable_items else mat.group(0), 397 | s, 398 | ) 399 | 400 | 401 | def rewrite_code_with_links(code: str): 402 | identifier_regex = r'([^\d\W][\w\d]*)' # not robust but sufficient for now 403 | builder = [] 404 | continueable = False 405 | for seg in re.split(identifier_regex, code): 406 | if not seg: 407 | continue 408 | if seg in linkable_items: 409 | builder.append(f'[`{seg}`]({LINK_PREFIX}{seg})') 410 | continueable = False 411 | elif continueable: 412 | builder[-1] = f'{builder[-1][:-1]}{seg}`' 413 | else: 414 | builder.append(f'`{seg}`') 415 | continueable = True 416 | return ''.join(builder) 417 | 418 | 419 | linkable_items = {*()} 420 | tparams = {*()} 421 | 422 | def is_tparam(s: str): 423 | return s in tparams 424 | 425 | 426 | # fill in hyperlinkable items and project tparams 427 | for module in api_spec.modules: 428 | if module['file_path'] == api_spec.type_file: 429 | tparams.update(v for v in module['gvs'] if v.startswith('T')) 430 | linkable_items.update(module['gvs']) 431 | linkable_items.update(module['classes'].keys()) 432 | 433 | # write api markdown for each module 434 | for module in api_spec.modules: 435 | m_name = module['name'] 436 | print(f'Processing module {m_name}') 437 | # if module name is more than one dot -> then the front ones means a submodule. 438 | # create nested folder 439 | sub_folder = '' 440 | if m_name.count('.') > 1: 441 | sub_folder = re.search(r'^\w+\.(.+)\.\w+$', m_name).group(1).replace('.', '/') 442 | os.makedirs(f'api/{sub_folder}', exist_ok=True) 443 | with open(module['file_path']) as fin, open(f'api/{sub_folder}/{m_name}.md', 'w') as fout: 444 | code = fin.read() 445 | v = ModuleVisitor(module) 446 | v.visit_root(ast.parse(code)) 447 | v.report_found_globals() 448 | 449 | fout.write(f'# module ``{m_name}``\n\n') 450 | 451 | if v.module_string: 452 | fout.write(f'{v.module_string}\n\n') 453 | 454 | if v.global_vars: 455 | fout.write('## Constants\n\n') 456 | for i, var in enumerate(v.global_vars): 457 | fout.write(var.markdown(tlevel=3, link=True)) 458 | if i < len(v.global_vars) - 1: 459 | fout.write('---\n\n') 460 | if v.classes: 461 | fout.write('---\n\n') 462 | 463 | for i, c in enumerate(v.classes): 464 | fout.write(c.markdown()) 465 | if i < len(v.classes) - 1: 466 | fout.write('---\n\n') 467 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to types-linq's documentation! 2 | ######################################## 3 | 4 | ``types-linq`` is a lightweight Python library that attempts to implement LINQ (Language Integrated Query) 5 | features seen in .NET languages (`see here for their documentation `_). 6 | 7 | This library provides similarly expressive and unified querying exprience on objects so long as it is 8 | `iterable `_. 9 | With few simple method calls and lambdas, developers can perform complex traversal, filter and transformations 10 | on any data that typically had to be done with many iterative logics such as for loops. 11 | 12 | There have been several libraries that try providing such functionalities, while this library tries to accomplish 13 | something different: 14 | 15 | * It incorporates the original APIs in .NET ``IEnumerable`` class as close as possible, including method names, 16 | conventions, edge behaviors, etc. This means typical Python conventions might be shadowed here 17 | * It tries to implement deferred evaluations. The library operates in a streaming manner if possible and handles 18 | infinite streams (Python generators) properly 19 | * Strong type safety while using this library is guarenteed since the APIs are `typed `_ 20 | * It honours the Python `collections.abc `_ interfaces 21 | 22 | The project is licensed under the BSD-2-Clause License. 23 | 24 | .. toctree:: 25 | :hidden: 26 | 27 | self 28 | 29 | .. toctree:: 30 | :maxdepth: 1 31 | :caption: To Start: 32 | 33 | to-start/installing.rst 34 | to-start/examples.rst 35 | to-start/differences.rst 36 | to-start/changelog.rst 37 | GitHub Project 38 | 39 | .. toctree:: 40 | :maxdepth: 1 41 | :caption: API: 42 | :glob: 43 | 44 | api/* 45 | 46 | .. toctree:: 47 | :maxdepth: 1 48 | :caption: API (MORE): 49 | :glob: 50 | 51 | api/more/* 52 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx==5.0.2 2 | sphinx-rtd-theme==1.0.0 3 | myst-parser==0.18.0 4 | -------------------------------------------------------------------------------- /doc/to-start/changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ############ 3 | 4 | `GitHub Releases `_ 5 | 6 | v1.2.1 7 | ******** 8 | 9 | - Fix CachedEnumerable buffering bug when capacity is zero 10 | - Add traverse_topological() to MoreEnumerable class 11 | - Add ranking methods MoreEnumerable.rank() and rank_by() methods 12 | - The documentation page now has hyperlinks for some terminologies in this release 13 | 14 | v1.2.0 15 | ******** 16 | 17 | - Add pre_scan(), scan(), scan_right() and segment() to MoreEnumerable class 18 | - Fix type annotation mistake in Enumerable.aggregate(__func) 19 | - Fix type annotation mistakes in MoreEnumerable.aggregate_right() 20 | 21 | v1.1.0 22 | ******** 23 | 24 | - Add consume(), cycle(), and run_length_encode() to MoreEnumerable class 25 | - Fix error in ExtremaEnumerable.take() when it takes a slice 26 | 27 | v1.0.0 28 | ******** 29 | 30 | - Add enumerate(), rank() and rank_by() to MoreEnumerable class 31 | - Add chunk(), max_by(), min_by(), intersect_by() and union_by() to Enumerable class 32 | - Enumerable.element_at() now supports negative index 33 | - Enumerable.take() now supports taking a slice (which is same as Enumerable.elements_in()) to be consistent with 34 | .NET 6 35 | - Enumerable.__getitem__() now supports providing a default value 36 | - **Breaking**: Add Enumerable.distinct_by() that returns an Enumerable instance. MoreEnumerable.distinct_by() that 37 | returned a MoreEnumerable instance is removed 38 | - **Breaking**: Add Enumerable.except_by(). The previous MoreEnumerable.except_by() that took homogeneous values as 39 | the second iterable is now renamed as MoreEnumerable.except_by2() 40 | 41 | v0.2.1 42 | ******** 43 | 44 | - Add pipe() to MoreEnumerable class 45 | - Enumerable.distinct(), except1(), .union(), .intersect(), .to_lookup(), .join(), .group_by(), .group_join(), 46 | MoreEnumerable.distinct_by(), .except_by() now have preliminary support for unhashable keys 47 | 48 | v0.2.0 49 | ******** 50 | 51 | - Add a MoreEnumerable class containing the following method names: aggregate_right(), distinct_by(), except_by(), 52 | flatten(), for_each(), interleave(), maxima_by(), minima_by(), traverse_breath_first() and traverse_depth_first() 53 | - Add as_more() to Enumerable class 54 | 55 | v0.1.2 56 | ******** 57 | 58 | - Add to_tuple() 59 | - Add an overload to sequence_equal() that accepts a comparision function 60 | - https://github.com/cleoold/types-linq/commit/f70bd510492a915776f6cac26854111650541b22 61 | 62 | v0.1.1 63 | ******** 64 | 65 | - Change zip() to support multiple 66 | - Add as_cached() method to memoize results 67 | - Fix OrderedEnumerable bug that once use [] operator on it, returning incorrect result 68 | - Add dunder to some parameter names seen in pyi to prevent them from being passed as named arguments 69 | - https://github.com/cleoold/types-linq/commit/b1b70b9d489cfe06ab1a69c4a0e4a5d195f5f5d7 70 | 71 | v0.1.0 72 | ******** 73 | 74 | - Initial releases under the BSD-2-Clause License 75 | -------------------------------------------------------------------------------- /doc/to-start/differences.rst: -------------------------------------------------------------------------------- 1 | Differences from `"IEnumerable"` 2 | ########################################## 3 | 4 | Because Python and C# are two languages that have a lot of differences, this library does not 5 | intimate everything from the .NET world as some practices are not possible Python world. This 6 | section lists some differences (or limitations) between the ``types_linq.Enumerable`` class 7 | and its .NET counterpart. 8 | 9 | * In C#, there are extension methods. By ``using`` the correct namespaces, the query methods 10 | will be automatically available on all references to ``IEnumerable`` variables. Such concepts 11 | do not exist in Python, hence users have to wrap the object under a ``types_linq.Enumerable`` 12 | class to use those query methods. 13 | * C# uses overloading extensively while there are no real method overloading in Python. Rather, 14 | to define overload methods in Python, one must use the ``typing.overload`` decorator to decorate 15 | stubs, then implement all overloads together in a single definition. An example can be found 16 | `here `_. 17 | The downside of this is that the features supported are quite limited. 18 | 19 | For example, it can be simple to seperate between ``def fn(a: int) -> None`` and ``def fn(a: int, b: int) -> None``, 20 | also between ``def fn(a: str) -> None`` and ``def fn(a: bytes) -> None`` by checking the number of 21 | arguments or using ``isinstance()``. However, when it comes to separate ``def fn(a: Callable[[TSource_co], bool]) -> None`` 22 | and ``def fn(Callable[[TSource_co, int], bool]) -> None``, there is no straightforward way that works 23 | for all occasions (typical reflection check will fail for C extensions, and try-except is impractical). 24 | This library's solution is a sketchy one: using different names for these methods, for example, ``Enumerable.where()`` 25 | and ``Enumerable.where2()``. This is the reason why some names of methods here end with numbers. 26 | 27 | It can also bring some troubles when disambiguating types that overlap. If an object implements both 28 | ``Iterable`` and ``Callable``, and there are method overloads for each, the behavior might be 29 | inconsistent if the implementation does not agree with stubs. Type checkers will pick the first matching 30 | overload. 31 | * There are no "IEqualityComparer" or something like that in Python. C# people will use these to compare 32 | objects, construct hashmaps, etc. While in Python such identities are often solely determined by object's 33 | magic methods such as ``__hash__()``, ``__eq__()``, ``__lt__()``, etc. So method overloads that involve such 34 | comparer interfaces are omitted in this library, or implemented in another form. 35 | * In C#, there are nullable types and default values for a type. For example, ``default(int) == 0`` and ``default(int?) == null``. 36 | Some C# methods return such default values if the source sequence is empty, or skip ``null``'s if the source sequence contains 37 | concrete data too. There are no such notions in Python and the C#-like default semantics are non-existent. So, this usage is 38 | not supported by this library (Can ``None`` be considered a default value for all cases? Hmm..). 39 | * C# has `Index `_ syntaxes, and to be Pythonic, these are 40 | negative indices. C# has `Range `_, which are 41 | `slices `_. This difference can be seen in ``Enumerable.element_at()`` 42 | and ``Enumerable.take()``. 43 | * All classes in this library are concrete. There are no interfaces like what are usually done in C#. 44 | 45 | Limitations: 46 | 47 | * To deal with overloads, some method parameters are positional-only, e.g. those starting with double 48 | underscores. Some of them can be improved. 49 | * ``OrderedEnumerable`` exposing unnecessary type parameter ``TKey``. 50 | * ``Lookup.__getitem__()``, ``Lookup.contains()``, ``Lookup.count`` are incompatible with the superclass methods they 51 | are overriding. 52 | -------------------------------------------------------------------------------- /doc/to-start/examples.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ######### 3 | 4 | The primary class importable from this library is the ``Enumerable`` class. To query on an 5 | existing object such as lists, tuples, or generators, you pass the object to the ``Enumerable`` 6 | constructor, then invoke chained methods like this: 7 | 8 | .. code-block:: python 9 | 10 | from types_linq import Enumerable 11 | 12 | lst = [1, 4, 7, 9, 16] 13 | 14 | query = Enumerable(lst).where(lambda x: x % 2 == 0).select(lambda x: x ** 2) 15 | 16 | for x in query: 17 | print(x) 18 | 19 | This will filter the list by whether the element is even, then converts each element to 20 | the square of it. The call to ``where`` and ``select`` will return immediately. Finally 21 | when the iterator of ``query`` is requested in the for loop, the element will be enumerated 22 | in the order. 23 | 24 | It is roughly equivalent to the following code: 25 | 26 | .. code-block:: python 27 | 28 | for x in lst: 29 | if x % 2 == 0: 30 | print(x ** 2) 31 | 32 | or 33 | 34 | .. code-block:: python 35 | 36 | for x in map(lambda x: x ** 2, filter(lambda x: x % 2 == 0, lst)): 37 | print(x) 38 | 39 | The output will be ``16`` and ``256`` printed on newlines. 40 | 41 | The class supports a lot more than this. The usage is simple if you know about the interfaces 42 | in .NET as this library provides almost the exact methods. It is advised to take a look at 43 | the `tests `_ to digest more in-action 44 | use cases. 45 | 46 | More examples 47 | ******************* 48 | 49 | `Grouping `_ and 50 | transforming lists: 51 | 52 | .. code-block:: python 53 | 54 | from typing import NamedTuple 55 | from types_linq import Enumerable as En 56 | 57 | 58 | class AnswerSheet(NamedTuple): 59 | subject: str 60 | score: int 61 | name: str 62 | 63 | students = ['Jacque', 'Franklin', 'Romeo'] 64 | papers = [ 65 | AnswerSheet(subject='Calculus', score=78, name='Jacque'), 66 | AnswerSheet(subject='Calculus', score=98, name='Romeo'), 67 | AnswerSheet(subject='Algorithms', score=59, name='Romeo'), 68 | AnswerSheet(subject='Mechanics', score=93, name='Jacque'), 69 | AnswerSheet(subject='E & M', score=87, name='Jacque'), 70 | ] 71 | 72 | query = En(students) \ 73 | .order_by(lambda student: student) \ 74 | .group_join(papers, 75 | lambda student: student, 76 | lambda paper: paper.name, 77 | lambda student, papers: { 78 | 'student': student, 79 | 'papers': papers.order_by(lambda paper: paper.subject) \ 80 | .select(lambda paper: { 81 | 'subject': paper.subject, 82 | 'score': paper.score, 83 | }).to_list(), 84 | 'gpa': papers.average2(lambda paper: paper.score, None), 85 | } 86 | ) 87 | 88 | for obj in query: 89 | print(obj) 90 | 91 | # output: 92 | # {'student': 'Franklin', 'papers': [], 'gpa': None} 93 | # {'student': 'Jacque', 'papers': [{'subject': 'E & M', 'score': 87}, {'subject': 'Mechanics', 'score': 93}, {'subject': 'Calculus', 'score': 78}], 'gpa': 86.0} 94 | # {'student': 'Romeo', 'papers': [{'subject': 'Algorithms', 'score': 59}, {'subject': 'Calculus', 'score': 98}], 'gpa': 78.5} 95 | 96 | Working with generators: 97 | 98 | .. code-block:: python 99 | 100 | import random 101 | from types_linq import Enumerable as En 102 | 103 | def toss_coins(): 104 | while True: 105 | yield random.choice(('Head', 'Tail')) 106 | 107 | times_head = En(toss_coins()).take(5) \ # [:5] also works 108 | .count(lambda r: r == 'Head') 109 | 110 | print(f'You tossed 5 times with {times_head} HEADs!') 111 | 112 | # possible output: 113 | # You tossed 5 times with 2 HEADs! 114 | 115 | Working with stream output: 116 | 117 | .. code-block:: python 118 | 119 | import sys, subprocess 120 | from types_linq import Enumerable as En 121 | 122 | proc = subprocess.Popen('kubectl logs -f my-pod', shell=True, stdout=subprocess.PIPE) 123 | stdout = iter(proc.stdout.readline, b'') 124 | 125 | query = En(stdout).where(lambda line: line.startswith(b'CRITICAL: ')) \ 126 | .select(lambda line: line[10:].decode()) 127 | 128 | for line in query: 129 | sys.stdout.write(line) 130 | sys.stdout.flush() 131 | 132 | # whatever. 133 | -------------------------------------------------------------------------------- /doc/to-start/installing.rst: -------------------------------------------------------------------------------- 1 | Installing 2 | ################ 3 | 4 | From pypi 5 | *********** 6 | 7 | The project is available on `pypi `_, to install the 8 | letest version, do: 9 | 10 | .. code-block:: bash 11 | 12 | $ pip install types-linq -U 13 | 14 | From GitHub Repo 15 | ****************** 16 | 17 | Clone the project and install from local files: 18 | 19 | .. code-block:: bash 20 | 21 | $ git clone https://github.com/cleoold/types-linq && cd types-linq 22 | $ pip install . 23 | # or 24 | $ python setup.py install 25 | 26 | Run Tests 27 | ============ 28 | 29 | In the project root, execute the following commands (or something similar) to run the 30 | test cases: 31 | 32 | .. code-block:: bash 33 | 34 | # optionally set up venv 35 | $ python -m venv 36 | $ ./scripts/activate 37 | 38 | $ pip install pytest 39 | $ python -m pytest 40 | 41 | If you want to run the project against `pyright `_, 42 | the following should do: 43 | 44 | .. code-block:: bash 45 | 46 | $ npm install pyright -g 47 | $ npx pyright 48 | 49 | Instead, opening vscode should also highlight red striggles (?) 50 | 51 | However, the `GitHub action settings `_ 52 | are most up-to-date and can be consulted. 53 | 54 | Build the Documentation 55 | ========================= 56 | 57 | To generate the pages you are currently looking at, in the project root, 58 | execute the following commands: 59 | 60 | .. code-block:: bash 61 | 62 | $ cd doc 63 | $ pip install -r requirements.txt 64 | # generate api md files 65 | $ python ./gen_api_doc.py 66 | # create html pages, contents are available in _build/html folder 67 | $ make html 68 | 69 | Note to generate api files, one must have Python version 3.9 or above. The api md files 70 | are committed to the repository. 71 | -------------------------------------------------------------------------------- /pyrightconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["types_linq", "tests"], 3 | "pythonVersion": "3.7", 4 | "typeCheckingMode":"strict", 5 | "reportUnknownLambdaType": "none", 6 | "reportUnknownVariableType": "none", 7 | "reportUnknownMemberType": "none", 8 | "reportUnnecessaryIsInstance": "none", 9 | "reportUnnecessaryCast": "none", 10 | "reportUnknownArgumentType": "none", 11 | "reportUnknownParameterType": "none", 12 | "reportInvalidStubStatement": "none", 13 | "reportPrivateUsage": "none", 14 | "reportMissingParameterType": "none", 15 | } 16 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | with open('README.md', 'r', encoding='utf-8') as f: 4 | long_description = f.read() 5 | 6 | setup( 7 | name='types-linq', 8 | version='v1.2.1', 9 | url='https://github.com/cleoold/types-linq', 10 | license='BSD 2-Clause License', 11 | author='cleoold', 12 | description='Standard sequence helper methods with full typing support', 13 | long_description=long_description, 14 | long_description_content_type='text/markdown', 15 | packages=['types_linq', 'types_linq.more'], 16 | package_data={ 17 | '': ['*.pyi', 'py.typed'], 18 | }, 19 | zip_safe=False, 20 | python_requires='>=3.7', 21 | extras_require={':python_version<"3.8"': ['typing_extensions']}, 22 | platforms='any', 23 | classifiers=[ 24 | 'Topic :: Utilities', 25 | 'Development Status :: 4 - Beta', 26 | 'License :: OSI Approved :: BSD License', 27 | 'Operating System :: OS Independent', 28 | 'Programming Language :: Python :: 3', 29 | 'Typing :: Typed', 30 | ], 31 | ) 32 | -------------------------------------------------------------------------------- /tests/test_more_usage.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Any, Callable, List, Optional, cast 3 | 4 | import pytest 5 | 6 | from types_linq import Enumerable, InvalidOperationError 7 | from types_linq.more import MoreEnumerable, DirectedGraphNotAcyclicError, RankMethods 8 | 9 | 10 | class Node: 11 | def __init__(self, val: int) -> None: 12 | self.val = val 13 | 14 | 15 | @dataclass 16 | class Tree: 17 | val: int 18 | left: 'Optional[Tree]' = None 19 | right: 'Optional[Tree]' = None 20 | 21 | 22 | class TestAsMore: 23 | def test_ret_type(self): 24 | en = Enumerable([1, 2]).as_more() 25 | assert isinstance(en, MoreEnumerable) 26 | assert en.to_list() == [1, 2] 27 | 28 | def test_ret_self(self): 29 | en = MoreEnumerable([1, 2]) 30 | assert en is en.as_more() 31 | 32 | 33 | class TestAggregateRightMethod: 34 | def test_overload1(self): 35 | fruits = ['apple', 'mango', 'orange', 'passionfruit', 'grape'] 36 | en = MoreEnumerable(fruits) 37 | st = en.aggregate_right('banana', lambda e, rr: f'({e}/{rr})', str.upper) 38 | assert st == '(APPLE/(MANGO/(ORANGE/(PASSIONFRUIT/(GRAPE/BANANA)))))' 39 | 40 | def test_overload1_empty(self): 41 | fruits: List[str] = [] 42 | en = MoreEnumerable(fruits) 43 | st = en.aggregate_right('banana', lambda e, rr: f'({e}/{rr})', str.upper) 44 | assert st == 'BANANA' 45 | 46 | def test_overload2(self): 47 | en = MoreEnumerable([9, 4, 2]) 48 | expr = en.aggregate_right('null', lambda e, rr: f'(cons {e} {rr})') 49 | assert expr == '(cons 9 (cons 4 (cons 2 null)))' 50 | 51 | def test_overload3(self): 52 | en = MoreEnumerable(['9', '4', '2', '5']) 53 | expr = en.aggregate_right(lambda e, rr: f'({e}+{rr})') 54 | assert expr == '(9+(4+(2+5)))' 55 | 56 | def test_overload3_empty(self): 57 | ints: List[int] = [] 58 | en = MoreEnumerable(ints) 59 | with pytest.raises(InvalidOperationError, match='Sequence is empty'): 60 | en.aggregate_right(lambda e, rr: e - rr) 61 | 62 | def test_overload3_1(self): 63 | ints = [87] 64 | en = MoreEnumerable(ints) 65 | sole = en.aggregate_right(lambda e, rr: e - rr) 66 | assert sole == 87 67 | 68 | 69 | class TestConsumeMethod: 70 | def test_consume(self): 71 | counter = 0 72 | def gen(): 73 | nonlocal counter 74 | for _ in range(5): 75 | counter += 1 76 | yield None 77 | en = MoreEnumerable(gen()) 78 | en.consume() 79 | assert counter == 5 80 | 81 | 82 | class TestEnumerateMethod: 83 | def test_enumerate(self): 84 | en = MoreEnumerable(['2', '4', '6']) 85 | assert en.enumerate().to_list() == [(0, '2'), (1, '4'), (2, '6')] 86 | 87 | def test_enumerate_start(self): 88 | en = MoreEnumerable(['2', '4', '6']) 89 | assert en.enumerate(1).to_list() == [(1, '2'), (2, '4'), (3, '6')] 90 | 91 | 92 | class TestExceptBy2Method: 93 | def test_except_by2(self): 94 | en = MoreEnumerable(['aaa', 'bb', 'c', 'dddd']) 95 | assert en.except_by2(['xx', 'y'], len).to_list() == ['aaa', 'dddd'] 96 | 97 | def test_unhashable(self): 98 | en = MoreEnumerable([['aaa'], ['bb'], ['c'], ['dddd']]) 99 | q = en.except_by2([['xx'], ['y']], lambda x: len(x[0])) 100 | assert q.to_list() == [['aaa'], ['dddd']] 101 | 102 | def test_no_repeat(self): 103 | en = MoreEnumerable(['aaa', 'bb', 'c', 'dddd', 'aaa']) 104 | assert en.except_by2(['xx', 'y'], len).to_list() == ['aaa', 'dddd'] 105 | 106 | def test_remove_nothing(self): 107 | i = -1 108 | def selector(_): 109 | nonlocal i; i += 1 110 | return i 111 | strs = ['aaa', 'bb', 'c', 'dddd', 'dddd'] 112 | en = MoreEnumerable(strs) 113 | assert en.except_by2((), selector).to_list() == strs 114 | 115 | 116 | class TestFlattenMethod: 117 | def test_flatten_overload1(self): 118 | en = MoreEnumerable( 119 | [ 120 | 1, 2, 3, 121 | [ 122 | 4, 5, 'orange', b'sequence', 123 | [ 124 | 6, [7], 125 | ], 126 | 8, 127 | ], 128 | 'foo', 129 | [], 130 | MoreEnumerable( 131 | [ 132 | 9, 10, (11, 12), 133 | ]), 134 | ]) 135 | assert en.flatten().to_list() == [ 136 | 1, 2, 3, 4, 5, 'orange', b'sequence', 6, 7, 8, 'foo', 9, 10, 11, 12, 137 | ] 138 | 139 | def test_flatten_overload2(self): 140 | en = MoreEnumerable([ 141 | 1, 2, 3, [4, 5], [6, 7, 8], [9, 10, [11, 12], 13], 142 | ]) 143 | assert en.flatten(lambda x: len(cast(List[Any], x)) != 2).to_list() == [ 144 | 1, 2, 3, [4, 5], 6, 7, 8, 9, 10, [11, 12], 13, 145 | ] 146 | 147 | def test_flatten_overload2_false(self): 148 | lst = [1, 2, 3, [4, 5, [6]]] 149 | en = MoreEnumerable(lst) 150 | assert en.flatten(lambda x: False).to_list() == lst 151 | 152 | def test_flatten2_tree(self): 153 | t = Tree \ 154 | ( 155 | left=Tree 156 | ( 157 | left=Tree(0), 158 | val=1, 159 | right=Tree(2), 160 | ), 161 | val=3, 162 | right=Tree 163 | ( 164 | left=Tree(4), 165 | val=5, 166 | right=Tree(6), 167 | ) 168 | ) 169 | en = MoreEnumerable((t, 'ignore_me')) 170 | 171 | def pred(x): 172 | if isinstance(x, int): 173 | return None 174 | elif isinstance(x, Tree): 175 | return (x.left, x.val, x.right) 176 | elif isinstance(x, str): 177 | return () 178 | elif isinstance(x, tuple): 179 | return x 180 | elif x is None: 181 | return () 182 | raise Exception 183 | 184 | assert en.flatten2(pred).to_list() == [*range(7)] 185 | 186 | def test_flatten2_empty(self): 187 | en = MoreEnumerable(()) 188 | assert en.flatten2(lambda x: x).to_list() == [] 189 | 190 | 191 | class TestForEachMethod: 192 | def test_for_each(self): 193 | side_effects = [] 194 | en = MoreEnumerable([7, 9, 11]) 195 | en.for_each(lambda x: side_effects.append(str(x))) 196 | assert side_effects == ['7', '9', '11'] 197 | 198 | def test_for_each2(self): 199 | side_effects = [] 200 | en = MoreEnumerable([7, 9, 11]) 201 | en.for_each2(lambda x, i: side_effects.append(f'{x}/{i}')) 202 | assert side_effects == ['7/0', '9/1', '11/2'] 203 | 204 | 205 | class TestInterleaveMethod: 206 | def test_interleave_one(self): 207 | en = MoreEnumerable([1, 2, 4]).interleave(*[]) 208 | assert en.to_list() == [1, 2, 4] 209 | 210 | def test_interleave_two(self): 211 | en = MoreEnumerable([1, 2, 3]).interleave([4, 5, 6]) 212 | assert en.to_list() == [1, 4, 2, 5, 3, 6] 213 | 214 | def test_interleave_three(self): 215 | en = MoreEnumerable([1, 2]).interleave([4, 5], [11, 12]) 216 | assert en.to_list() == [1, 4, 11, 2, 5, 12] 217 | 218 | def test_skip_consumed_me(self): 219 | en = MoreEnumerable(['1', '2']).interleave(['4', '5', '6'], ['7', '8', '9']) 220 | assert en.to_list() == ['1', '4', '7', '2', '5', '8', '6', '9'] 221 | 222 | def test_skip_consumed_them(self): 223 | en = MoreEnumerable([4, 5, 6]).interleave([9], [12, 17], [44, 45, 46]) 224 | assert en.to_list() == [4, 9, 12, 44, 5, 17, 45, 6, 46] 225 | 226 | 227 | class TestMaximaByMethod: 228 | strings = ['foo', 'bar', 'cheese', 'orange', 'baz', 'spam', 'egg', 'toasts', 'dish'] 229 | 230 | def test_overload1(self): 231 | en = MoreEnumerable(self.strings).maxima_by(len) 232 | assert en.to_list() == ['cheese', 'orange', 'toasts'] 233 | 234 | def test_overload1_empty(self): 235 | en = MoreEnumerable([]).maxima_by(len) 236 | assert en.to_list() == [] 237 | 238 | def test_overload2(self): 239 | s = [[[x]] for x in self.strings] 240 | en = MoreEnumerable(s).maxima_by(lambda x: x[0], lambda x, y: len(x[0]) - len(y[0])) 241 | assert en.to_list() == [[['cheese']], [['orange']], [['toasts']]] 242 | 243 | 244 | class TestMinimaByMethod: 245 | def test_overload1(self): 246 | en = MoreEnumerable(TestMaximaByMethod.strings).minima_by(len) 247 | assert en.to_list() == ['foo', 'bar', 'baz', 'egg'] 248 | 249 | def test_overload1_empty(self): 250 | en = MoreEnumerable([]).minima_by(len) 251 | assert en.to_list() == [] 252 | 253 | def test_overload2(self): 254 | s = [[[x]] for x in TestMaximaByMethod.strings] 255 | en = MoreEnumerable(s).minima_by(lambda x: x[0], lambda x, y: len(x[0]) - len(y[0])) 256 | assert en.to_list() == [[['foo']], [['bar']], [['baz']], [['egg']]] 257 | 258 | 259 | # no extensive testing because similar things are already tested in Enumerable 260 | class TestExtremaEnumerableFirstMethod: 261 | def test_first_overload1(self): 262 | en = MoreEnumerable(TestMaximaByMethod.strings).maxima_by(len) 263 | assert en.first() == 'cheese' 264 | 265 | def test_first_overload1_empty(self): 266 | en = MoreEnumerable([]).maxima_by(len) 267 | with pytest.raises(InvalidOperationError): 268 | en.first() 269 | 270 | def test_first_call_parent_overload2(self): 271 | 'properly delegates the with-predicate overload to base class implementation' 272 | # don't crash 273 | en = MoreEnumerable(TestMaximaByMethod.strings).maxima_by(len) 274 | assert en.first(lambda x: 'o' in x) == 'orange' 275 | 276 | def test_first2_overload1(self): 277 | en = MoreEnumerable(TestMaximaByMethod.strings).maxima_by(len) 278 | assert en.first2('mmmmmm') == 'cheese' 279 | 280 | def test_first2_overload1_empty(self): 281 | en = MoreEnumerable([]).maxima_by(len) 282 | assert en.first2('mmmmmm') == 'mmmmmm' 283 | 284 | def test_first2_call_parent_overload2(self): 285 | en = MoreEnumerable(TestMaximaByMethod.strings).maxima_by(len) 286 | assert en.first2(lambda x: False, 'mmmmmm') == 'mmmmmm' 287 | 288 | 289 | class TestExtremaEnumerableLastMethod: 290 | def test_last_overload1(self): 291 | en = MoreEnumerable(TestMaximaByMethod.strings).maxima_by(len) 292 | assert en.last() == 'toasts' 293 | 294 | def test_last_overload1_empty(self): 295 | en = MoreEnumerable([]).maxima_by(len) 296 | with pytest.raises(InvalidOperationError): 297 | en.last() 298 | 299 | def test_last_call_parent_overload2(self): 300 | en = MoreEnumerable(TestMaximaByMethod.strings).maxima_by(len) 301 | assert en.last(lambda x: 'g' in x) == 'orange' 302 | 303 | def test_last2_overload1(self): 304 | en = MoreEnumerable(TestMaximaByMethod.strings).maxima_by(len) 305 | assert en.last2('mmmmmm') == 'toasts' 306 | 307 | def test_last2_overload1_empty(self): 308 | en = MoreEnumerable([]).maxima_by(len) 309 | assert en.last2('mmmmmm') == 'mmmmmm' 310 | 311 | def test_last2_call_parent_overload2(self): 312 | en = MoreEnumerable(TestMaximaByMethod.strings).maxima_by(len) 313 | assert en.last2(lambda x: False, 'mmmmmm') == 'mmmmmm' 314 | 315 | 316 | class TestExtremaEnumerableSingleMethod: 317 | strings = ['foo', 'bar', 'cheese'] 318 | 319 | def test_single_overload1(self): 320 | en = MoreEnumerable(self.strings).maxima_by(len) 321 | assert en.single() == 'cheese' 322 | 323 | def test_single_overload1_more(self): 324 | en = MoreEnumerable(TestMaximaByMethod.strings).maxima_by(len) 325 | with pytest.raises(InvalidOperationError): 326 | en.single() 327 | 328 | def test_single_call_parent_overload2(self): 329 | en = MoreEnumerable(self.strings).maxima_by(len) 330 | with pytest.raises(InvalidOperationError): 331 | en.single(lambda x: False) 332 | 333 | def test_single2_overload1(self): 334 | en = MoreEnumerable(self.strings).maxima_by(len) 335 | assert en.single2('mmmmmm') == 'cheese' 336 | 337 | def test_single2_overload1_empty(self): 338 | en = MoreEnumerable([]).maxima_by(len) 339 | assert en.single2('mmmmmm') == 'mmmmmm' 340 | 341 | def test_single2_call_parent_overload2(self): 342 | en = MoreEnumerable(self.strings).maxima_by(len) 343 | assert en.single2(lambda x: False, 'mmmmmm') == 'mmmmmm' 344 | 345 | 346 | class TestExtremaEnumerableTakeMethod: 347 | @pytest.mark.parametrize('count,expected', [ 348 | (-1, []), 349 | (0, []), 350 | (1, ['cheese']), 351 | (2, ['cheese', 'orange']), 352 | (3, ['cheese', 'orange', 'toasts']), 353 | (4, ['cheese', 'orange', 'toasts']), 354 | ]) 355 | def test_take_overload1(self, count: int, expected: List[str]): 356 | en = MoreEnumerable(TestMaximaByMethod.strings).maxima_by(len).take(count) 357 | assert en.to_list() == expected 358 | 359 | def test_take_call_parent_overload2(self): 360 | en = MoreEnumerable(TestMaximaByMethod.strings).maxima_by(len) 361 | assert en.take(slice(1, None)).to_list() == ['orange', 'toasts'] 362 | 363 | 364 | class TestExtremaEnumerableTakeLastMethod: 365 | @pytest.mark.parametrize('count,expected', [ 366 | (-1, []), 367 | (0, []), 368 | (1, ['toasts']), 369 | (2, ['orange', 'toasts']), 370 | (3, ['cheese', 'orange', 'toasts']), 371 | (4, ['cheese', 'orange', 'toasts']), 372 | ]) 373 | def test_take_last(self, count: int, expected: List[str]): 374 | en = MoreEnumerable(TestMaximaByMethod.strings).maxima_by(len).take_last(count) 375 | assert en.to_list() == expected 376 | 377 | 378 | class TestPipeMethod: 379 | def test_pipe(self): 380 | store = [] 381 | ints = [1, 2] 382 | en = MoreEnumerable(ints) 383 | assert en.pipe(store.append).to_list() == ints 384 | assert store == ints 385 | 386 | def test_action_then_yield(self): 387 | en = MoreEnumerable([[], []]) 388 | q = en.pipe(lambda x: x.append(1)).where(lambda x: len(x) == 0) 389 | assert q.to_list() == [] 390 | 391 | 392 | class TestPreScanMethod: 393 | def test_pre_scan(self): 394 | ints = [9, 4, 2, 5, 7] 395 | en = MoreEnumerable(ints) 396 | sums = en.pre_scan(0, lambda acc, e: acc + e) 397 | assert sums.to_list() == [0, 9, 13, 15, 20] 398 | exprs = en.pre_scan('', lambda acc, e: f'{acc}+{e}') 399 | assert exprs.to_list() == ['', '+9', '+9+4', '+9+4+2', '+9+4+2+5'] 400 | 401 | def test_one_elem(self): 402 | ints = [9] 403 | en = MoreEnumerable(ints) 404 | q = en.pre_scan(10, lambda acc, e: acc + e) 405 | assert q.to_list() == [10] 406 | 407 | def test_empty(self): 408 | q = MoreEnumerable([]).pre_scan(0, lambda acc, e: acc + e) 409 | assert q.to_list() == [] 410 | 411 | 412 | class TestRankMethod: 413 | def gen(self): 414 | yield from [1, 4, 77, 23, 23, 4, 9, 0, -7, 101, 23] 415 | 416 | def test_overload1(self): 417 | en = MoreEnumerable(self.gen()) 418 | assert en.rank().to_list() == [6, 5, 2, 3, 3, 5, 4, 7, 8, 1, 3] 419 | 420 | def test_overload1_empty(self): 421 | assert MoreEnumerable([]).rank().to_list() == [] 422 | 423 | def test_overload1_sorted(self): 424 | ints = [444, 190, 129, 122, 100] 425 | assert MoreEnumerable(ints).rank().to_list() == [1, 2, 3, 4, 5] 426 | assert MoreEnumerable(reversed(ints)).rank().to_list() == [5, 4, 3, 2, 1] 427 | 428 | def test_overload1_same(self): 429 | en = MoreEnumerable([8, 8, 8]) 430 | assert en.rank().to_list() == [1, 1, 1] 431 | 432 | def test_overload1_competitive(self): 433 | en = MoreEnumerable(self.gen()) 434 | assert en.rank(method=RankMethods.competitive).to_list() \ 435 | == [9, 7, 2, 3, 3, 7, 6, 10, 11, 1, 3] 436 | 437 | def test_overload1_ordinal(self): 438 | en = MoreEnumerable(self.gen()) 439 | assert en.rank(method=RankMethods.ordinal).to_list() \ 440 | == [9, 7, 2, 3, 4, 8, 6, 10, 11, 1, 5] 441 | 442 | def test_overload2(self): 443 | en = MoreEnumerable([(1, ''), (1, ''), (4, ''), (4, ''), (3, '')]) 444 | assert en.rank(lambda lhs, rhs: lhs[0] - rhs[0]).to_list() == [3, 3, 1, 1, 2] 445 | 446 | def test_overload2_default_obj_eq_dont_use(self): 447 | en = MoreEnumerable([Node(1), Node(1), Node(4), Node(4), Node(3), Node(4)]) 448 | assert en.rank(lambda lhs, rhs: lhs.val - rhs.val).to_list() \ 449 | == [3, 3, 1, 1, 2, 1] 450 | 451 | def test_overload2_competitive_first_is_dup(self): 452 | en = MoreEnumerable([Node(1), Node(1), Node(4), Node(3), Node(0)]) 453 | assert en.rank( 454 | lambda lhs, rhs: lhs.val - rhs.val, 455 | method=RankMethods.competitive, 456 | ).to_list() == [3, 3, 1, 2, 5] 457 | 458 | 459 | # majority is already tested by rank test 460 | class TestRankByMethod: 461 | def test_overload1(self): 462 | def gen(): 463 | yield from ['aaa', 'xyz', 'carbon', 'emission', 'statistics', 'somany'] 464 | en = MoreEnumerable(gen()) 465 | assert en.rank_by(len, method=RankMethods.dense).to_list() \ 466 | == [4, 4, 3, 2, 1, 3] 467 | 468 | def test_overload1_competitive_no_tie(self): 469 | strs = ['aaa', 'emission', 'carbon'] 470 | en = MoreEnumerable(strs) 471 | assert en.rank_by(len, method=RankMethods.competitive).to_list() == [3, 1, 2] 472 | 473 | def test_overload2(self): 474 | en = MoreEnumerable([ 475 | ['aaa'], ['xyz'], ['carbon'], ['emission'], ['statistics'], ['somany'] 476 | ]) 477 | assert en.rank_by(lambda x: x[0], lambda lhs, rhs: len(lhs) - len(rhs)) \ 478 | .to_list() == [4, 4, 3, 2, 1, 3] 479 | 480 | def test_overload2_empty(self): 481 | en = MoreEnumerable([]) 482 | assert en.rank_by(lambda x: x[0], lambda lhs, rhs: len(lhs) - len(rhs)) \ 483 | .to_list() == [] 484 | 485 | 486 | class TestRunLengthEncodeMethod: 487 | def test_overload1(self): 488 | en = MoreEnumerable('abbcaeeeaa') 489 | assert en.run_length_encode().to_list() \ 490 | == [('a', 1), ('b', 2), ('c', 1), ('a', 1), ('e', 3), ('a', 2)] 491 | 492 | def test_overload1_empty(self): 493 | en = MoreEnumerable(()) 494 | assert en.run_length_encode().to_list() == [] 495 | 496 | def test_overload1_one_run(self): 497 | en = MoreEnumerable('AAAAA') 498 | assert en.run_length_encode().to_list() == [('A', 5)] 499 | 500 | def test_overload1_one_elem(self): 501 | en = MoreEnumerable('A') 502 | assert en.run_length_encode().to_list() == [('A', 1)] 503 | 504 | def test_overload1_no_run(self): 505 | en = MoreEnumerable('abcdefghijklmnopqrstuvwxyz') 506 | assert en.run_length_encode().to_list() == \ 507 | en.select(lambda x: (x, 1)).to_list() 508 | 509 | def test_overload2(self): 510 | en = MoreEnumerable('abBBbcaEeeff') 511 | assert en.run_length_encode(lambda x, y: x.lower() == y.lower()).to_list() \ 512 | == [('a', 1), ('b', 4), ('c', 1), ('a', 1), ('E', 3), ('f', 2)] 513 | 514 | 515 | class TestScanMethod: 516 | def test_overload1(self): 517 | ints = [9, 4, 2, 5, 7] 518 | en = MoreEnumerable(ints) 519 | sums = en.scan(lambda acc, e: acc + e) 520 | assert sums.to_list() == [9, 13, 15, 20, 27] 521 | 522 | def test_overload1_one_elem(self): 523 | ints = [9] 524 | en = MoreEnumerable(ints) 525 | q = en.scan(lambda acc, e: acc + e) 526 | assert q.to_list() == [9] 527 | 528 | def test_overload1_empty(self): 529 | q = MoreEnumerable([]).scan(lambda acc, e: acc + e) 530 | assert q.to_list() == [] 531 | 532 | def test_overload2(self): 533 | ints = [9, 4, 2, 5, 7] 534 | en = MoreEnumerable(ints) 535 | sums = en.scan('', lambda acc, e: f'{acc}+{e}') 536 | assert sums.to_list() == ['', '+9', '+9+4', '+9+4+2', '+9+4+2+5', '+9+4+2+5+7'] 537 | 538 | def test_overload2_one_elem(self): 539 | ints = [9] 540 | en = MoreEnumerable(ints) 541 | q = en.scan(10, lambda acc, e: acc + e) 542 | assert q.to_list() == [10, 19] 543 | 544 | def test_overload2_empty(self): 545 | q = MoreEnumerable([]).scan(-1, lambda acc, e: acc + e) 546 | assert q.to_list() == [-1] 547 | 548 | 549 | class TestScanRightMethod: 550 | def test_overload1(self): 551 | strs = ['9', '4', '2', '5'] 552 | en = MoreEnumerable(strs) 553 | q = en.scan_right(lambda e, rr: f'({e}+{rr})') 554 | assert q.to_list() == ['(9+(4+(2+5)))', '(4+(2+5))', '(2+5)', '5'] 555 | 556 | def test_overload1_one_elem(self): 557 | strs = ['-1'] 558 | en = MoreEnumerable(strs) 559 | q = en.scan_right(lambda e, rr: f'({e}+{rr})') 560 | assert q.to_list() == ['-1'] 561 | 562 | def test_overload1_empty(self): 563 | q = MoreEnumerable([]).scan_right(lambda e, rr: f'({e}+{rr})') 564 | assert q.to_list() == [] 565 | 566 | def test_overload2(self): 567 | ints = [9, 4, 2] 568 | en = MoreEnumerable(ints) 569 | q = en.scan_right('null', lambda e, rr: f'(cons {e} {rr})') 570 | assert q.to_list() == [ 571 | '(cons 9 (cons 4 (cons 2 null)))', 572 | '(cons 4 (cons 2 null))', 573 | '(cons 2 null)', 574 | 'null', 575 | ] 576 | 577 | def test_overload2_one_elem(self): 578 | ints = [9] 579 | en = MoreEnumerable(ints) 580 | q = en.scan_right('nil', lambda e, rr: f'(cons {e} {rr})') 581 | assert q.to_list() == ['(cons 9 nil)', 'nil'] 582 | 583 | def test_overload2_empty(self): 584 | en = MoreEnumerable([]) 585 | q = en.scan_right('nil', lambda e, rr: f'(cons {e} {rr})') 586 | assert q.to_list() == ['nil'] 587 | 588 | 589 | class TestSegmentMethod: 590 | def test_segment(self): 591 | ints = [0, 1, 2, 4, -4, -2, 6, 2, -2] 592 | en = MoreEnumerable(ints) 593 | q = en.segment(lambda x: x < 0).select(lambda x: x.to_list()) 594 | assert q.to_list() == [[0, 1, 2, 4], [-4], [-2, 6, 2], [-2]] 595 | 596 | def test_segment_empty(self): 597 | en = MoreEnumerable([]) 598 | q = en.segment(lambda x: x < 0).select(lambda x: x.to_list()) 599 | assert q.to_list() == [] 600 | 601 | def test_segment_always_split(self): 602 | ints = [1, 2, 3] 603 | en = MoreEnumerable(ints) 604 | q = en.segment(lambda x: True).select(lambda x: x.to_list()) 605 | assert q.to_list() == [[1], [2], [3]] 606 | 607 | def test_segment_no_splitting(self): 608 | ints = [1, 2, 3] 609 | en = MoreEnumerable(ints) 610 | q = en.segment(lambda x: False).select(lambda x: x.to_list()) 611 | assert q.to_list() == [ints] 612 | 613 | def test_segment_one_elem(self): 614 | def pred(_: str): raise Exception 615 | en = MoreEnumerable(['']) 616 | q = en.segment(pred).select(lambda x: x.to_list()) 617 | assert q.to_list() == [['']] 618 | 619 | def test_segment_reiterate(self): 620 | ints = [1, 2, 3] 621 | en = MoreEnumerable(ints) 622 | q = en.segment(lambda x: True) 623 | for segment in q: 624 | assert segment.to_list() == segment.to_list() 625 | 626 | def test_segment2_split_on_first(self): 627 | ints = [0, 1, 2, 4, -4, -2, 6, 2, -2] 628 | en = MoreEnumerable(ints) 629 | q = en.segment2(lambda x, i: x < 0 or i % 3 == 0) \ 630 | .select(Enumerable.to_list) 631 | assert q.to_list() == [[0, 1, 2], [4], [-4], [-2], [6, 2], [-2]] 632 | 633 | @pytest.mark.parametrize('func,expected', [ 634 | (lambda curr, prev, _: curr * prev < 0, 635 | [[0, 1, 2, 4], [-4, -2], [6, 2], [-2]]), 636 | (lambda curr, prev, i: curr < 0 and prev >= 0 or i == 1, 637 | [[0], [1, 2, 4], [-4, -2, 6, 2], [-2]]), 638 | ]) 639 | def test_segment3(self, func: Callable[[int, int, int], bool], expected: List[List[int]]): 640 | ints = [0, 1, 2, 4, -4, -2, 6, 2, -2] 641 | en = MoreEnumerable(ints) 642 | q = en.segment3(func).select(Enumerable.to_list) 643 | assert q.to_list() == expected 644 | 645 | 646 | class TestCycleMethod: 647 | def test_repeat(self): 648 | en = MoreEnumerable([1, 2, 3]) 649 | assert en.cycle(3).to_list() == [1, 2, 3] * 3 650 | 651 | def test_no_elem(self): 652 | en = MoreEnumerable([]) 653 | assert en.cycle(3).to_list() == [] 654 | 655 | def test_zero_count(self): 656 | en = MoreEnumerable([1, 2, 3]) 657 | assert en.cycle(0).to_list() == [] 658 | 659 | def test_one_count(self): 660 | en = MoreEnumerable([1, 2, 3]) 661 | assert en.cycle(1).to_list() == en.to_list() 662 | 663 | def test_infinite_count(self): 664 | def gen(): 665 | yield from [1, 2] 666 | en = MoreEnumerable(gen()) 667 | assert en.cycle(None).take(11).to_list() == [1, 2] * 5 + [1] 668 | 669 | def test_invalid(self): 670 | en = MoreEnumerable([1, 2, 3]) 671 | with pytest.raises(InvalidOperationError): 672 | en.cycle(-1) 673 | 674 | 675 | class TestTraverseBreathFirstMethod: 676 | tree = Tree \ 677 | ( 678 | left=Tree 679 | ( 680 | left=Tree(0), 681 | val=1, 682 | right=Tree(2), 683 | ), 684 | val=3, 685 | right=Tree 686 | ( 687 | left=None, 688 | val=4, 689 | right=Tree(5), 690 | ) 691 | ) 692 | 693 | @staticmethod 694 | def selector(n: Tree): 695 | return [t for t in (n.left, n.right) if t is not None] 696 | 697 | def test_traverse_tree(self): 698 | en = MoreEnumerable.traverse_breath_first(self.tree, self.selector) 699 | assert en.select(lambda n: n.val).to_list() == [3, 1, 4, 0, 2, 5] 700 | 701 | def test_preserve_children_order(self): 702 | en = MoreEnumerable.traverse_breath_first(0, lambda i: [*range(1, 10)] if i == 0 else []) 703 | assert en.to_list() == [*range(10)] 704 | 705 | def test_single(self): 706 | en = MoreEnumerable.traverse_breath_first(0, lambda _: ()) 707 | assert en.to_list() == [0] 708 | 709 | 710 | class TestTraverseDepthFirstMethod: 711 | def test_traverse_tree(self): 712 | en = MoreEnumerable.traverse_depth_first( 713 | TestTraverseBreathFirstMethod.tree, 714 | TestTraverseBreathFirstMethod.selector, 715 | ) 716 | assert en.select(lambda n: n.val).to_list() == [3, 1, 0, 2, 4, 5] 717 | 718 | def test_preserve_children_order(self): 719 | en = MoreEnumerable.traverse_depth_first(0, lambda i: [*range(1, 10)] if i == 0 else []) 720 | assert en.to_list() == [*range(10)] 721 | 722 | def test_single(self): 723 | en = MoreEnumerable.traverse_depth_first(0, lambda _: ()) 724 | assert en.to_list() == [0] 725 | 726 | 727 | class TestTraverseTopologicalMethod: 728 | def test_traverse1_simple(self): 729 | adj = { 730 | 5: [2, 0], 731 | 4: [0, 1], 732 | 2: [3], 733 | 3: [1], 734 | } 735 | en = MoreEnumerable([5, 4]).traverse_topological(lambda x: adj.get(x, ())) 736 | assert en.to_list() == [5, 2, 3, 4, 0, 1] 737 | 738 | def test_traverse1_linear_dfs(self): 739 | en = MoreEnumerable([TestTraverseBreathFirstMethod.tree]) \ 740 | .traverse_topological(TestTraverseBreathFirstMethod.selector) 741 | assert en.select(lambda n: n.val).to_list() == [3, 1, 0, 2, 4, 5] 742 | 743 | def test_traverse1_all_nodes(self): 744 | adj = [ 745 | [1, 2, 3], 746 | [3], 747 | [1, 3], 748 | [], 749 | ] 750 | en = MoreEnumerable(range(4)).traverse_topological(adj.__getitem__) 751 | assert en.to_list() == [0, 2, 1, 3] 752 | 753 | def test_traverse1_single(self): 754 | en = MoreEnumerable([0]).traverse_topological(lambda _: ()) 755 | assert en.to_list() == [0] 756 | 757 | def test_traverse1_cycle(self): 758 | adj = { 759 | 5: [2, 0], 760 | 4: [0, 1], 761 | 2: [3], 762 | 3: [1, 5], 763 | } 764 | en = MoreEnumerable([5, 4]).traverse_topological(lambda x: adj.get(x, ())) 765 | with pytest.raises(DirectedGraphNotAcyclicError) as excinfo: 766 | en.to_list() 767 | assert excinfo.value.cycle == (3, 5) 768 | 769 | def test_traverse1_self_loop(self): 770 | adj = { 771 | 1: [2, 3], 772 | 3: [4], 773 | 4: [5, 6, 4, 7], 774 | } 775 | en = MoreEnumerable([1]).traverse_topological(lambda x: adj.get(x, ())) 776 | with pytest.raises(DirectedGraphNotAcyclicError) as excinfo: 777 | en.to_list() 778 | assert excinfo.value.cycle == (4, 4) 779 | 780 | def test_traverse2_big(self): 781 | adj = { 782 | 0: [1, 2], 783 | 3: [2], 784 | 10: [12], 785 | 11: [12], 786 | 1: [5, 4], 787 | 2: [4], 788 | 12: [13], 789 | 5: [6], 790 | 4: [9], 791 | 13: [4], 792 | 6: [7, 8, 9], 793 | } 794 | roots = map(Node, [0, 3, 4, 10, 11]) 795 | en = MoreEnumerable(roots).traverse_topological2( 796 | lambda x: map(Node, adj.get(x.val, ())), 797 | lambda x: x.val, 798 | ) 799 | assert en.select(lambda x: x.val).to_list() \ 800 | == [0, 1, 5, 6, 7, 8, 3, 2, 10, 11, 12, 13, 4, 9] 801 | 802 | def test_traverse2_diamond(self): 803 | adj = { 804 | 0: [2], 805 | 1: [2, 3, 8], 806 | 2: [4, 5], 807 | 3: [7], 808 | 4: [6], 809 | 5: [6], 810 | 6: [], 811 | 7: [8], 812 | 8: [], 813 | } 814 | roots = map(Node, [0, 1]) 815 | en = MoreEnumerable(roots).traverse_topological2( 816 | lambda x: map(Node, adj[x.val]), 817 | lambda x: x.val, 818 | ) 819 | assert en.select(lambda x: x.val).to_list() \ 820 | == [0, 1, 2, 4, 5, 6, 3, 7, 8] 821 | 822 | def test_traverse2_two_cycles_get_first(self): 823 | adj = { 824 | 0: [1, 2], 825 | 3: [2], 826 | 10: [12], 827 | 11: [12], 828 | 1: [5, 4], 829 | 2: [4], 830 | 12: [13], 831 | 5: [6], 832 | 4: [9], 833 | 13: [4, 12], 834 | 6: [7, 9], 835 | 9: [3], 836 | } 837 | roots = map(Node, [0, 3, 4, 10, 11]) 838 | en = MoreEnumerable(roots).traverse_topological2( 839 | lambda x: map(Node, adj.get(x.val, ())), 840 | lambda x: x.val, 841 | ) 842 | count = set() 843 | with pytest.raises(DirectedGraphNotAcyclicError) as excinfo: 844 | for node in en: 845 | count.add(node.val) 846 | A, B = excinfo.value.cycle 847 | assert [A.val, B.val] == [2, 4] # type: ignore 848 | assert len(count) < 14 849 | -------------------------------------------------------------------------------- /tests/test_tricky.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Container, Iterable, Reversible, Sequence, Sized 2 | 3 | import pytest 4 | 5 | from types_linq import Enumerable, InvalidOperationError 6 | from types_linq.util import ComposeSet, ComposeMap 7 | 8 | def naturals(): 9 | i = 0 10 | while True: 11 | yield i 12 | i += 1 13 | 14 | 15 | class TestAbc: 16 | def test_abc(self): 17 | en = Enumerable([]) 18 | assert isinstance(en, Container) 19 | assert isinstance(en, Sequence) 20 | assert isinstance(en, Iterable) 21 | assert isinstance(en, Reversible) 22 | assert isinstance(en, Sized) 23 | 24 | class TestIterMethod: 25 | def test_nest_generator(self): 26 | gen = (chr(i) for i in range(120, 123)) 27 | en = Enumerable(Enumerable(gen)) 28 | assert en.to_list() == ['x', 'y', 'z'] 29 | 30 | 31 | class TestInfinite: 32 | def test_take(self): 33 | en = Enumerable(naturals()).select(lambda i: i * 2) 34 | assert en.take(2).to_list() == [0, 2] 35 | assert en.take(3).to_list() == [4, 6, 8] 36 | en2 = Enumerable(naturals).select(lambda i: i * 2) 37 | assert en2.take(2).to_list() == [0, 2] 38 | assert en2.take(2).to_list() == [0, 2] 39 | 40 | 41 | class TestAsCachedMethod: 42 | def test_enumerate_same_generator(self): 43 | gen = (i for i in range(6)) 44 | en = Enumerable(gen).as_cached() 45 | assert en.to_list() == [0, 1, 2, 3, 4, 5] 46 | assert en.count() == 6 47 | assert en.to_list() == [0, 1, 2, 3, 4, 5] 48 | 49 | def test_generator_empty(self): 50 | gen = (i for i in range(0)) 51 | en = Enumerable(gen).as_cached() 52 | assert en.to_list() == [] 53 | assert en.to_list() == [] 54 | 55 | def test_multiple_query_race(self): 56 | en = Enumerable(naturals()).as_cached() 57 | assert en.take(1).to_list() == [0] 58 | assert en.take(1).to_list() == [0] 59 | assert en.take(3).to_list() == [0, 1, 2] 60 | assert en.take(1).to_list() == [0] 61 | assert en.take(3).to_list() == [0, 1, 2] 62 | assert en.take(5).to_list() == [0, 1, 2, 3, 4] 63 | assert en.take(7).to_list() == [0, 1, 2, 3, 4, 5, 6] 64 | assert en.take(2).to_list() == [0, 1] 65 | 66 | def test_have_capacity(self): 67 | en = Enumerable(naturals()).as_cached(cache_capacity=100) 68 | assert en.take(100).to_list() == [*range(100)] 69 | assert en.take(100).to_list() == [*range(100)] 70 | assert en.take(101).to_list() == [*range(101)] 71 | assert en.take(100).to_list() == [*range(1, 101)] 72 | assert en.take(50).to_list() == [*range(1, 51)] 73 | assert en.take(102).to_list() == [*range(1, 103)] 74 | assert en.take(102).to_list() == [*range(3, 105)] 75 | assert en.take(0).to_list() == [] 76 | assert en.take(110).to_list() == [*range(5, 115)] 77 | assert en.take(100).to_list() == [*range(15, 115)] 78 | 79 | def test_multiple_branches_race(self): 80 | en = Enumerable(naturals()).as_cached(cache_capacity=5) 81 | b1 = en.take(100) 82 | b2 = en.take(100) 83 | assert b1.take(3).to_list() == [0, 1, 2] 84 | assert b2.take(3).to_list() == [0, 1, 2] 85 | assert b1.take(6).to_list() == [0, 1, 2, 3, 4, 5] 86 | assert b2.take(4).to_list() == [1, 2, 3, 4] 87 | assert b1.take(4).to_list() == [1, 2, 3, 4] 88 | assert b1.take(7).to_list() == [1, 2, 3, 4, 5, 6, 7] 89 | assert b2.take(7).to_list() == [3, 4, 5, 6, 7, 8, 9] 90 | b3 = en.take(100) 91 | assert b3.to_list() == [*range(5, 105)] 92 | assert b1.take(2).to_list() == [100, 101] 93 | 94 | def test_zero_capacity(self): 95 | en = Enumerable(naturals()).as_cached(cache_capacity=0) 96 | assert en.take(1).to_list() == [0] 97 | assert en.take(2).to_list() == [1, 2] 98 | 99 | def test_one_capacity(self): 100 | en = Enumerable(naturals()).as_cached(cache_capacity=1) 101 | assert en.take(1).to_list() == [0] 102 | assert en.take(3).to_list() == [0, 1, 2] 103 | assert en.take(3).to_list() == [2, 3, 4] 104 | assert en.take(1).to_list() == [4] 105 | assert en.take(2).to_list() == [4, 5] 106 | 107 | def test_capcity_grow_from_zero(self): 108 | en = Enumerable(naturals()).as_cached(cache_capacity=0) 109 | en.take(3).to_list() 110 | en.as_cached(cache_capacity=1) 111 | assert en.take(1).to_list() == [3] 112 | assert en.take(2).to_list() == [3, 4] 113 | 114 | def test_original_iter_after_capacity_change(self): 115 | en = Enumerable(naturals()).as_cached(cache_capacity=0) 116 | existing = Enumerable(iter(en.take(9))) 117 | existing.take(3).to_list() 118 | en.as_cached(cache_capacity=2) 119 | assert existing.take(3).to_list() == [3, 4, 5] 120 | assert existing.to_list() == [6, 7, 8] 121 | 122 | def test_capacity_grow_from_one(self): 123 | en = Enumerable(naturals()).as_cached(cache_capacity=1) 124 | en.take(10).to_list() 125 | en.as_cached(cache_capacity=5) 126 | assert en.take(10).to_list() == [*range(9, 19)] 127 | assert en.take(10).to_list() == [*range(14, 24)] 128 | 129 | def test_capacity_grow_to_inf(self): 130 | en = Enumerable(naturals()).as_cached(cache_capacity=5) 131 | en.take(10).to_list() 132 | en.as_cached(cache_capacity=None) 133 | en.take(10).to_list() 134 | en.take(10).to_list() 135 | assert en.take(15).to_list() == [*range(5, 20)] 136 | 137 | def test_capacity_shrink(self): 138 | en = Enumerable(naturals()).as_cached(cache_capacity=10) 139 | en.take(10).to_list() 140 | en.as_cached(cache_capacity=9) 141 | assert en.take(10).to_list() == [*range(1, 11)] 142 | en.as_cached(cache_capacity=5) 143 | assert en.take(10).to_list() == [*range(6, 16)] 144 | 145 | def test_capacity_shrink_to_zero(self): 146 | en = Enumerable(naturals()).as_cached(cache_capacity=10) 147 | en.take(10).to_list() 148 | en.as_cached(cache_capacity=0) 149 | assert en.take(10).to_list() == [*range(10, 20)] 150 | 151 | def test_capacity_shrink_no_delete(self): 152 | en = Enumerable(naturals()).as_cached(cache_capacity=10) 153 | en.take(5).to_list() 154 | en.as_cached(cache_capacity=6) 155 | assert en.take(6).to_list() == [*range(0, 6)] 156 | en.take(7).to_list() 157 | assert en.take(6).to_list() == [*range(1, 7)] 158 | 159 | def test_errors(self): 160 | gen = (i for i in range(0)) 161 | en = Enumerable(gen).as_cached() 162 | with pytest.raises(InvalidOperationError): 163 | en.as_cached(cache_capacity=-1) 164 | en2 = Enumerable(gen) 165 | with pytest.raises(InvalidOperationError): 166 | en2.as_cached(cache_capacity=-1) 167 | 168 | 169 | class TestOrderedByThenIter: 170 | def test_orderby_index(self): 171 | en = Enumerable([1, 3, 2]).order_by(lambda x: x) 172 | assert en.element_at(1) == en[1] == 2 173 | assert en.element_at(2) == en[2] == 3 174 | 175 | def test_as_cached_then_orderby(self): 176 | en = Enumerable(naturals()).take(5).as_cached() 177 | en.take(3).to_list() 178 | assert en.order_by(lambda x: x).to_list() == [*range(5)] 179 | 180 | 181 | 182 | class TestComposeMapAandSet: 183 | def __set_equal(self, iter1, iter2) -> bool: 184 | lst1 = [*iter1] 185 | lst2 = [*iter2] 186 | if len(lst1) != len(lst2): 187 | return False 188 | for e in iter1: 189 | if e not in lst2: 190 | return False 191 | lst2.remove(e) 192 | return len(lst2) == 0 193 | 194 | 195 | def test_set(self): 196 | set = ComposeSet([[1, 2, 4], (5, 6), 7, [], [9]]) 197 | assert len(set) == 5 198 | assert self.__set_equal(set, [[1, 2, 4], (5, 6), 7, [], [9]]) 199 | 200 | # add element 201 | set.add((5, 6)) 202 | assert self.__set_equal(set, [[1, 2, 4], (5, 6), 7, [], [9]]) 203 | set.add([1, 2, 4]) 204 | assert self.__set_equal(set, [[1, 2, 4], (5, 6), 7, [], [9]]) 205 | set.add([10, 11]) 206 | assert self.__set_equal(set, [[1, 2, 4], (5, 6), 7, [], [9], [10, 11]]) 207 | 208 | # remove element 209 | set.discard([1, 2, 4]) 210 | set.discard(7) 211 | assert self.__set_equal(set, [(5, 6), [], [9], [10, 11]]) 212 | 213 | # remove non-exist element 214 | # discard shouldn't throw KeyError 215 | set.discard([1, 2, 4]) 216 | with pytest.raises(KeyError): 217 | set.remove([1, 2, 4]) 218 | 219 | 220 | def test_map(self): 221 | map = ComposeMap([([1, 2, 4], 1), ((4, 6), 3), ([], 3)]) 222 | assert self.__set_equal(map, [[1, 2, 4], (4, 6), []]) 223 | assert self.__set_equal(map.items(), [([1, 2, 4], 1), ((4, 6), 3), ([], 3)]) 224 | 225 | # modify map values 226 | map[[1, 2, 4]] = 3 227 | assert map[[1, 2, 4]] == 3 228 | map[(4, 6)] = 1 229 | assert map[(4, 6)] == 1 230 | 231 | # delete element 232 | del map[[]] 233 | assert [] not in map 234 | del map[(4, 6)] 235 | assert (4, 6) not in map 236 | with pytest.raises(KeyError): 237 | del map[[]] 238 | assert self.__set_equal(map.items(), [([1, 2, 4], 3)]) 239 | -------------------------------------------------------------------------------- /types_linq/__init__.py: -------------------------------------------------------------------------------- 1 | from .enumerable import Enumerable 2 | from .types_linq_error import TypesLinqError, InvalidOperationError, IndexOutOfRangeError 3 | 4 | 5 | __all__ = [ 6 | 'Enumerable', 7 | 'TypesLinqError', 8 | 'InvalidOperationError', 9 | 'IndexOutOfRangeError', 10 | ] 11 | -------------------------------------------------------------------------------- /types_linq/cached_enumerable.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Dict, Iterable, Iterator, Optional 3 | 4 | from .enumerable import Enumerable 5 | from .types_linq_error import InvalidOperationError 6 | 7 | from .more_typing import ( 8 | TSource_co, 9 | ) 10 | 11 | 12 | class CachedEnumerable(Enumerable[TSource_co]): 13 | ''' 14 | ```py 15 | from types_linq.cached_enumerable import CachedEnumerable 16 | ``` 17 | 18 | Enumerable that stores the enumerated results which can be accessed repeatedly. 19 | 20 | Users should not construct instances of this class directly. Use `Enumerable.as_cached()` instead. 21 | 22 | Revisions 23 | ~ v0.1.1: New. 24 | ''' 25 | 26 | _iter: Iterator[TSource_co] 27 | _cache_capacity: Optional[int] 28 | _enumerated_values: Dict[int, TSource_co] 29 | _min_index: int 30 | _tracked: int 31 | 32 | def __init__(self, source: Iterable[TSource_co], cache_capacity: Optional[int]): 33 | if cache_capacity is not None and cache_capacity < 0: 34 | raise InvalidOperationError('cache_capacity must be nonnegative') 35 | self._iter = iter(source) 36 | super().__init__(self._iter) 37 | self._cache_capacity = cache_capacity 38 | self._enumerated_values = {} 39 | self._min_index = 0 40 | self._tracked = 0 41 | 42 | def _get_iterable(self) -> Iterator[TSource_co]: 43 | i = 0 44 | while True: 45 | while i < self._tracked and self._cache_capacity != 0: 46 | i = max(self._min_index, i) 47 | res = self._enumerated_values[i] 48 | i += 1 49 | yield res 50 | try: 51 | res = next(self._iter) 52 | except StopIteration: 53 | break 54 | if self._cache_capacity == 0: 55 | yield res 56 | continue 57 | len_ = len(self._enumerated_values) 58 | if self._cache_capacity is not None and \ 59 | len_ > 0 and len_ == self._cache_capacity: 60 | del self._enumerated_values[self._min_index] 61 | self._min_index += 1 62 | self._enumerated_values[self._tracked] = res 63 | self._tracked += 1 64 | i += 1 65 | yield res 66 | 67 | def as_cached(self, *, cache_capacity: Optional[int] = None) -> CachedEnumerable[TSource_co]: 68 | ''' 69 | Updates settings and returns the original CachedEnumerable reference. 70 | 71 | Raises `InvalidOperationError` if cache_capacity is negative. 72 | ''' 73 | if cache_capacity is not None: 74 | if cache_capacity < 0: 75 | raise InvalidOperationError('cache_capacity must be nonnegative') 76 | while len(self._enumerated_values) > cache_capacity: 77 | del self._enumerated_values[self._min_index] 78 | self._min_index += 1 79 | self._cache_capacity = cache_capacity 80 | return self 81 | -------------------------------------------------------------------------------- /types_linq/grouping.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Generic, List 3 | 4 | from .enumerable import Enumerable 5 | 6 | from .more_typing import ( 7 | TKey_co, 8 | TValue_co, 9 | ) 10 | 11 | 12 | # kind of respect IGrouping's covariant type parameters if the method _append() 13 | # is really treated as an internal method 14 | class Grouping(Enumerable[TValue_co], Generic[TKey_co, TValue_co]): 15 | ''' 16 | ```py 17 | from types_linq.grouping import Grouping 18 | ``` 19 | 20 | Represents a collection of objects that have a common key. 21 | 22 | Users should not construct instances of this class directly. Use `Enumerable.group_by()` instead. 23 | ''' 24 | 25 | _key: TKey_co 26 | _values: List[TValue_co] 27 | 28 | def __init__(self, key: TKey_co): # type: ignore 29 | self._values = [] 30 | super().__init__(self._values) 31 | self._key = key 32 | 33 | @property 34 | def key(self) -> TKey_co: 35 | ''' 36 | Gets the key of the grouping. 37 | ''' 38 | return self._key 39 | 40 | def _append(self, value: TValue_co) -> None: # type: ignore 41 | self._values.append(value) 42 | -------------------------------------------------------------------------------- /types_linq/lookup.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Callable, Iterable 3 | 4 | from .enumerable import Enumerable 5 | from .grouping import Grouping 6 | from .util import ComposeMap 7 | 8 | from .more_typing import ( 9 | TSource, 10 | TKey_co, 11 | TValue_co, 12 | TResult, 13 | ) 14 | 15 | 16 | # TODO: Wish to support Mapping[TKey, TValue], but its default mixin methods are doing something 17 | # weird. 18 | class Lookup(Enumerable[Grouping[TKey_co, TValue_co]]): 19 | ''' 20 | ```py 21 | from types_linq.lookup import Lookup 22 | ``` 23 | 24 | A lookup is a one-to-many dictionary. It maps keys to Enumerable sequences of values. 25 | 26 | Users should not construct instances of this class directly. Use `Enumerable.to_lookup()` 27 | instead. 28 | ''' 29 | 30 | _groupings: ComposeMap[TKey_co, Grouping[TKey_co, TValue_co]] 31 | 32 | def __init__(self, 33 | source: Iterable[TSource], 34 | key_selector: Callable[[TSource], TKey_co], 35 | value_selector: Callable[[TSource], TValue_co], 36 | ): 37 | self._groupings = ComposeMap() 38 | 39 | for src in source: 40 | key = key_selector(src) 41 | elem = value_selector(src) 42 | if key not in self._groupings: 43 | self._groupings[key] = Grouping(key) 44 | self._groupings[key]._append(elem) 45 | 46 | super().__init__(self._groupings.values()) 47 | 48 | def __contains__(self, value: object) -> bool: 49 | ''' 50 | Tests whether key is in the lookup. 51 | ''' 52 | return value in self._groupings 53 | 54 | def __len__(self) -> int: 55 | ''' 56 | Gets the number of key-collection pairs. 57 | ''' 58 | return len(self._groupings) 59 | 60 | def __getitem__(self, key: TKey_co) -> Enumerable[TValue_co]: # type: ignore 61 | ''' 62 | Gets the collection of values indexed by the specified key, or empty if no such key 63 | exists. 64 | ''' 65 | if key in self._groupings: 66 | return self._groupings[key] 67 | return Enumerable.empty() # type: ignore 68 | 69 | def apply_result_selector(self, 70 | result_selector: Callable[[TKey_co, Enumerable[TValue_co]], TResult], 71 | ) -> Enumerable[TResult]: 72 | ''' 73 | Applies a transform function to each key and its associated values, then returns the 74 | results. 75 | ''' 76 | def inner(): 77 | for key, grouping in self._groupings.items(): 78 | yield result_selector(key, grouping) 79 | return Enumerable(inner) 80 | 81 | def contains(self, value: object) -> bool: # type: ignore[override] 82 | ''' 83 | Tests whether key is in the lookup. 84 | ''' 85 | return value in self 86 | 87 | @property 88 | def count(self) -> int: # type: ignore[override] 89 | ''' 90 | Gets the number of key-collection pairs. 91 | ''' 92 | return len(self) 93 | -------------------------------------------------------------------------------- /types_linq/more/__init__.py: -------------------------------------------------------------------------------- 1 | from .more_enumerable import MoreEnumerable 2 | from .more_enums import RankMethods 3 | from .more_error import DirectedGraphNotAcyclicError 4 | 5 | 6 | __all__ = [ 7 | 'MoreEnumerable', 8 | 'RankMethods', 9 | 'DirectedGraphNotAcyclicError', 10 | ] 11 | -------------------------------------------------------------------------------- /types_linq/more/extrema_enumerable.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Any, Callable, Deque, Generic, Iterable, Optional, Union 3 | 4 | from .more_enumerable import MoreEnumerable 5 | from ..more_typing import ( 6 | TKey, 7 | TSource, 8 | TSource_co, 9 | ) 10 | 11 | 12 | # appreciation to: MoreLinq.IExtremaEnumerable by morelinq 13 | class ExtremaEnumerable(MoreEnumerable[TSource_co], Generic[TSource_co, TKey]): 14 | 15 | _selector: Callable[[TSource_co], TKey] 16 | _comparer: Optional[Callable[[TKey, TKey], int]] 17 | _for_min: bool 18 | 19 | def __init__(self, 20 | source: Iterable[TSource_co], 21 | selector: Callable[[TSource_co], TKey], 22 | comparer: Optional[Callable[[TKey, TKey], int]], 23 | for_min: bool, 24 | ) -> None: 25 | super().__init__(source) 26 | self._selector = selector 27 | self._comparer = comparer 28 | self._for_min = for_min 29 | 30 | # only enumerate extrema when needed. __iter__-ing it will get all extrema; take()-ing it with 31 | # count elems only store that number of elems in the buffer 32 | def _get_iterable(self) -> Iterable[TSource_co]: 33 | it = super()._get_iterable() 34 | getter = lambda: _FirstExtrema(None) 35 | return _extrema_by(it, getter, self._selector, self._comparer, self._for_min) 36 | 37 | # please maintain same overloads 38 | def first(self, *args): # pyright: ignore[reportIncompatibleMethodOverride] 39 | if len(args) == 0: 40 | return MoreEnumerable.first(self.take(1)) 41 | else: # len(args) == 1 42 | return super().first(*args) 43 | 44 | # please maintain same overloads 45 | def first2(self, *args): 46 | if len(args) == 1: 47 | return MoreEnumerable.first2(self.take(1), *args) 48 | else: # len(args) == 2 49 | return super().first2(*args) 50 | 51 | # please maintain same overloads 52 | def last(self, *args): # pyright: ignore[reportIncompatibleMethodOverride] 53 | if len(args) == 0: 54 | return MoreEnumerable.last(self.take_last(1)) 55 | else: # len(args) == 1 56 | return super().last(*args) 57 | 58 | # please maintain same overloads 59 | def last2(self, *args): 60 | if len(args) == 1: 61 | return MoreEnumerable.last2(self.take_last(1), *args) 62 | else: # len(args) == 2 63 | return super().last2(*args) 64 | 65 | # please maintain same overloads 66 | def single(self, *args): # pyright: ignore[reportIncompatibleMethodOverride] 67 | if len(args) == 0: 68 | return MoreEnumerable.single(self.take(2)) 69 | else: # len(args) == 1 70 | return super().single(*args) 71 | 72 | # please maintain same overloads 73 | def single2(self, *args): 74 | if len(args) == 1: 75 | return MoreEnumerable.single2(self.take(2), *args) 76 | else: # len(args) == 2 77 | return super().single2(*args) 78 | 79 | # please maintain same overloads 80 | def take(self, count: Union[int, slice]) -> Any: 81 | if isinstance(count, int): 82 | if count <= 0: 83 | return MoreEnumerable(()) 84 | it = super()._get_iterable() 85 | getter = lambda c=count: _FirstExtrema(c) 86 | return _extrema_by(it, getter, self._selector, self._comparer, self._for_min) 87 | else: # isinstance(count, slice) 88 | return super().take(count) 89 | 90 | def take_last(self, count: int) -> MoreEnumerable[TSource_co]: 91 | if count <= 0: 92 | return MoreEnumerable(()) 93 | it = super()._get_iterable() 94 | getter = lambda: _LastExtrema(count) 95 | return _extrema_by(it, getter, self._selector, self._comparer, self._for_min) 96 | 97 | 98 | def _extrema_by( 99 | source: Iterable[TSource], 100 | extrema_getter: Callable[[], _Extrema[TSource]], 101 | selector: Callable[[TSource], TKey], 102 | comparer: Optional[Callable[[TKey, TKey], int]], 103 | for_min: bool, 104 | ) -> MoreEnumerable[TSource]: 105 | it = iter(source) 106 | try: 107 | elem = next(it) 108 | except StopIteration: 109 | return MoreEnumerable(()) 110 | 111 | if comparer is None: 112 | # None comparer ensures TSource must support __lt__() 113 | comparer = _lt_comparer 114 | if for_min: 115 | comparer = lambda x, y, comp=comparer: comp(y, x) 116 | 117 | extrema = extrema_getter() 118 | extrema.add(elem) 119 | extremum_key = selector(elem) 120 | for elem in it: 121 | key = selector(elem) 122 | cmp = comparer(key, extremum_key) 123 | if cmp > 0: 124 | extrema.restart() 125 | extrema.add(elem) 126 | extremum_key = key 127 | elif cmp == 0: 128 | extrema.add(elem) 129 | 130 | def inner(): 131 | yield from extrema.store 132 | return MoreEnumerable(inner) 133 | 134 | 135 | def _lt_comparer(x, y) -> int: 136 | if x < y: return -1 137 | # assumes total ordering 138 | elif y < x: return 1 139 | return 0 140 | 141 | 142 | class _FirstExtrema(Generic[TSource]): 143 | def __init__(self, capacity: Optional[int]) -> None: 144 | self.store = [] 145 | self.capacity = capacity 146 | 147 | def add(self, element: TSource) -> None: 148 | if self.capacity is None or len(self.store) < self.capacity: 149 | self.store.append(element) 150 | 151 | def restart(self) -> None: 152 | self.store.clear() 153 | 154 | 155 | class _LastExtrema(Generic[TSource]): 156 | def __init__(self, capacity: Optional[int]) -> None: 157 | self.store = Deque() 158 | self.capacity = capacity 159 | 160 | def add(self, element: TSource) -> None: 161 | if self.capacity is not None and len(self.store) == self.capacity: 162 | self.store.popleft() 163 | self.store.append(element) 164 | 165 | def restart(self) -> None: 166 | self.store.clear() 167 | 168 | 169 | _Extrema = Union[_FirstExtrema[TSource], _LastExtrema[TSource]] 170 | -------------------------------------------------------------------------------- /types_linq/more/extrema_enumerable.pyi: -------------------------------------------------------------------------------- 1 | from typing import Callable, Generic, Iterable, Optional, overload 2 | 3 | from .more_enumerable import MoreEnumerable 4 | from ..enumerable import Enumerable 5 | from ..more_typing import ( 6 | TKey, 7 | TSource_co, 8 | ) 9 | 10 | 11 | class ExtremaEnumerable(MoreEnumerable[TSource_co], Generic[TSource_co, TKey]): 12 | ''' 13 | ```py 14 | from types_linq.more.extrema_enumerable import ExtremaEnumerable 15 | ``` 16 | 17 | Specialization for manipulating extrema. 18 | 19 | Users should not construct instances of this class directly. Use `MoreEnumerable.maxima_by()` 20 | instead. 21 | 22 | Revisions 23 | ~ v0.2.0: New. 24 | ''' 25 | 26 | def __init__(self, 27 | source: Iterable[TSource_co], 28 | selector: Callable[[TSource_co], TKey], 29 | comparer: Optional[Callable[[TKey, TKey], int]], 30 | for_min: bool, 31 | ) -> None: ... # internal 32 | 33 | @overload 34 | def take(self, count: int) -> MoreEnumerable[TSource_co]: 35 | ''' 36 | Returns a specified number of contiguous elements from the start of the sequence. 37 | ''' 38 | 39 | @overload 40 | def take(self, __index: slice) -> Enumerable[TSource_co]: 41 | ''' 42 | Identical to parent. 43 | 44 | Revisions 45 | ~ v1.1.0: Fixed incorrect override of `Enumerable.take()` when it takes a slice. 46 | ''' 47 | 48 | def take_last(self, count: int) -> MoreEnumerable[TSource_co]: 49 | ''' 50 | Returns a new sequence that contains the last `count` elements. 51 | ''' 52 | -------------------------------------------------------------------------------- /types_linq/more/more_enumerable.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any, Callable, Deque, Iterable, Iterator, List, Optional, TYPE_CHECKING, Tuple 4 | 5 | if TYPE_CHECKING: 6 | from .extrema_enumerable import ExtremaEnumerable 7 | 8 | from .more_enums import RankMethods 9 | from .more_error import DirectedGraphNotAcyclicError 10 | from ..enumerable import Enumerable 11 | from ..util import ( 12 | ComposeMap, 13 | ComposeSet, 14 | default_equal, 15 | identity, 16 | ) 17 | from ..more_typing import ( 18 | TAccumulate, 19 | TKey, 20 | TSource, 21 | TSource_co, 22 | ) 23 | 24 | 25 | class MoreEnumerable(Enumerable[TSource_co]): 26 | 27 | def aggregate_right(self, *args) -> Any: 28 | # NOTE: a copy of the sequence is made in this call because it falls back 29 | it = iter(self.reverse()) 30 | if len(args) == 3: 31 | seed, func, result_selector = args 32 | elif len(args) == 2: 33 | seed, func, result_selector = args[0], args[1], identity 34 | else: # len(args) == 1 35 | func, result_selector = args[0], identity 36 | try: 37 | seed = next(it) 38 | except StopIteration: 39 | self._raise_empty_sequence() 40 | 41 | for elem in it: 42 | seed = func(elem, seed) 43 | return result_selector(seed) 44 | 45 | def as_more(self) -> MoreEnumerable[TSource_co]: # pyright: ignore[reportIncompatibleMethodOverride] 46 | return self 47 | 48 | def consume(self) -> None: 49 | for _ in self: 50 | ... 51 | 52 | def cycle(self, count: Optional[int] = None) -> MoreEnumerable[TSource_co]: 53 | if count is not None: 54 | if count < 0: 55 | self._raise_count_negative() # type: ignore 56 | elif count == 0: 57 | return MoreEnumerable(()) 58 | elif count == 1: 59 | return self 60 | 61 | def inner(cnt=count): 62 | memo: List[TSource_co] = [] 63 | for elem in self: 64 | memo.append(elem) 65 | yield elem 66 | while cnt is None or cnt > 1: 67 | yield from memo 68 | if cnt is not None: 69 | cnt -= 1 70 | return MoreEnumerable(inner) 71 | 72 | def enumerate(self, start: int = 0) -> MoreEnumerable[Tuple[int, TSource_co]]: 73 | return MoreEnumerable(lambda: enumerate(self, start)) 74 | 75 | def except_by2(self, 76 | second: Iterable[TSource_co], 77 | key_selector: Callable[[TSource_co], object], 78 | ) -> MoreEnumerable[TSource_co]: 79 | def inner(): 80 | s = ComposeSet(key_selector(s) for s in second) 81 | for elem in self: 82 | key = key_selector(elem) 83 | if key in s: 84 | continue 85 | s.add(key) 86 | yield elem 87 | return MoreEnumerable(inner) 88 | 89 | def flatten(self, *args: Callable[[Iterable[Any]], bool]) -> MoreEnumerable[Any]: 90 | if len(args) == 0: 91 | return self.flatten(lambda x: not isinstance(x, (str, bytes))) 92 | else: # len(args) == 1 93 | predicate = args[0] 94 | return self.flatten2(lambda x: x 95 | if isinstance(x, Iterable) and predicate(x) 96 | else None) 97 | 98 | def flatten2(self, selector: Callable[[Any], Optional[Iterable[object]]]) -> MoreEnumerable[Any]: 99 | def inner(): 100 | stack: List[Iterator[object]] = [iter(self)] 101 | while stack: 102 | it = stack.pop() 103 | while True: 104 | try: 105 | elem = next(it) 106 | except StopIteration: 107 | break 108 | nested = selector(elem) 109 | if nested is not None: 110 | stack.append(it) 111 | it = iter(nested) 112 | continue 113 | yield elem 114 | return MoreEnumerable(inner) 115 | 116 | def for_each(self, action: Callable[[TSource_co], object]) -> None: 117 | for elem in self: 118 | action(elem) 119 | 120 | def for_each2(self, action: Callable[[TSource_co, int], object]) -> None: 121 | for i, elem in enumerate(self): 122 | action(elem, i) 123 | 124 | def interleave(self, *iters: Iterable[TSource_co]) -> MoreEnumerable[TSource_co]: 125 | def inner(): 126 | its = [iter(self)] 127 | for iter_ in iters: 128 | its.append(iter(iter_)) 129 | while its: 130 | i = 0 131 | while i < len(its): 132 | try: 133 | yield next(its[i]) 134 | i += 1 135 | except StopIteration: 136 | its.pop(i) 137 | return MoreEnumerable(inner) 138 | 139 | def maxima_by(self, 140 | selector: Callable[[TSource_co], TKey], 141 | *args: Callable[[TKey, TKey], int], 142 | ) -> ExtremaEnumerable[TSource_co, TKey]: 143 | from .extrema_enumerable import ExtremaEnumerable 144 | if len(args) == 0: 145 | comparer = None 146 | else: # len(args) == 1 147 | comparer = args[0] 148 | return ExtremaEnumerable(self, selector, comparer, False) 149 | 150 | def minima_by(self, 151 | selector: Callable[[TSource_co], TKey], 152 | *args: Callable[[TKey, TKey], int], 153 | ) -> ExtremaEnumerable[TSource_co, TKey]: 154 | from .extrema_enumerable import ExtremaEnumerable 155 | if len(args) == 0: 156 | comparer = None 157 | else: # len(args) == 1 158 | comparer = args[0] 159 | return ExtremaEnumerable(self, selector, comparer, True) 160 | 161 | def pipe(self, action: Callable[[TSource_co], object]) -> MoreEnumerable[TSource_co]: 162 | def inner(): 163 | for elem in self: 164 | action(elem) 165 | yield elem 166 | return MoreEnumerable(inner) 167 | 168 | def pre_scan(self, 169 | identity: TAccumulate, 170 | transformation: Callable[[TAccumulate, TSource_co], TAccumulate], 171 | ) -> MoreEnumerable[TAccumulate]: 172 | def inner(aggregator: TAccumulate = identity): 173 | it = iter(self) 174 | try: 175 | past = next(it) 176 | except StopIteration: 177 | return 178 | yield identity 179 | for elem in it: 180 | aggregator = transformation(aggregator, past) 181 | yield aggregator 182 | past = elem 183 | return MoreEnumerable(inner) 184 | 185 | def rank(self, 186 | *args: Callable[[TSource_co, TSource_co], int], 187 | method: RankMethods = RankMethods.dense, 188 | ) -> MoreEnumerable[int]: 189 | return self.rank_by(identity, *args, method=method) 190 | 191 | def rank_by(self, 192 | key_selector: Callable[[TSource_co], TKey], 193 | *args: Callable[[TKey, TKey], int], 194 | method: RankMethods = RankMethods.dense, 195 | ) -> MoreEnumerable[int]: 196 | if len(args) == 0: 197 | # it is sufficient to have only equality 198 | comparer = lambda l, r: 0 if l == r else 1 199 | else: # len(args) == 1 200 | comparer = args[0] 201 | 202 | def inner(): 203 | # avoid enumerating twice 204 | copy = self.select(key_selector).to_list() 205 | if not copy: 206 | return 207 | l = len(copy) 208 | ordered = MoreEnumerable(range(l)) \ 209 | .order_by_descending(lambda i: copy[i], *args).to_list() 210 | rank_map = [0] * l 211 | rank_map[ordered[0]] = 1 212 | rank = 1 213 | consec = 1 214 | for i in range(1, l): 215 | if method == RankMethods.ordinal: 216 | rank += 1 217 | elif comparer(copy[ordered[i - 1]], copy[ordered[i]]) != 0: 218 | rank += consec 219 | consec = 1 220 | elif method == RankMethods.competitive: 221 | consec += 1 222 | rank_map[ordered[i]] = rank 223 | del ordered 224 | for i in rank_map: 225 | yield i 226 | return MoreEnumerable(inner) 227 | 228 | def run_length_encode(self, 229 | *args: Callable[[TSource_co, TSource_co], bool], 230 | ) -> MoreEnumerable[Tuple[TSource_co, int]]: 231 | if len(args) == 0: 232 | comparer = default_equal 233 | else: 234 | comparer = args[0] 235 | def inner(): 236 | iterator = iter(self) 237 | try: 238 | prev_elem = next(iterator) 239 | except StopIteration: 240 | return 241 | count = 1 242 | while True: 243 | try: 244 | elem = next(iterator) 245 | except StopIteration: 246 | break 247 | if comparer(prev_elem, elem): 248 | count += 1 249 | else: 250 | yield (prev_elem, count) 251 | prev_elem = elem 252 | count = 1 253 | yield (prev_elem, count) 254 | return MoreEnumerable(inner) 255 | 256 | def scan(self, *args) -> Any: 257 | if len(args) == 2: 258 | seed, transformation = args 259 | else: # len(args) == 1 260 | transformation = args[0] 261 | def inner(): 262 | it = iter(self) 263 | nonlocal seed 264 | if len(args) == 1: 265 | try: 266 | seed = next(it) 267 | except StopIteration: 268 | return 269 | yield seed 270 | for elem in it: 271 | seed = transformation(seed, elem) 272 | yield seed 273 | return MoreEnumerable(inner) 274 | 275 | def scan_right(self, *args) -> Any: 276 | if len(args) == 2: 277 | seed, func = args 278 | else: # len(args) == 1 279 | func = args[0] 280 | def inner(): 281 | # NOTE: a copy of the sequence is made in this call because it falls back 282 | it = iter(self.reverse()) 283 | nonlocal seed 284 | if len(args) == 1: 285 | try: 286 | seed = next(it) 287 | except StopIteration: 288 | return 289 | results = [] 290 | results.append(seed) 291 | for elem in it: 292 | seed = func(elem, seed) 293 | results.append(seed) 294 | yield from reversed(results) 295 | return MoreEnumerable(inner) 296 | 297 | def segment(self, 298 | new_segment_predicate: Callable[[TSource_co], bool], 299 | ) -> MoreEnumerable[MoreEnumerable[TSource_co]]: 300 | return self.segment3(lambda x, p, i: new_segment_predicate(x)) 301 | 302 | def segment2(self, 303 | new_segment_predicate: Callable[[TSource_co, int], bool], 304 | ) -> MoreEnumerable[MoreEnumerable[TSource_co]]: 305 | return self.segment3(lambda x, _, i: new_segment_predicate(x, i)) 306 | 307 | def segment3(self, 308 | new_segment_predicate: Callable[[TSource_co, TSource_co, int], bool], 309 | ) -> MoreEnumerable[MoreEnumerable[TSource_co]]: 310 | def inner(): 311 | it = iter(self) 312 | try: 313 | prev = next(it) 314 | except StopIteration: 315 | return 316 | segment = [prev] 317 | for i, elem in enumerate(it, 1): 318 | if new_segment_predicate(elem, prev, i): 319 | yield MoreEnumerable(segment) 320 | segment = [elem] 321 | else: 322 | segment.append(elem) 323 | prev = elem 324 | yield MoreEnumerable(segment) 325 | return MoreEnumerable(inner) 326 | 327 | @staticmethod 328 | def traverse_breath_first( 329 | root: TSource, 330 | children_selector: Callable[[TSource], Iterable[TSource]], 331 | ) -> MoreEnumerable[TSource]: 332 | def inner(): 333 | queue = Deque((root,)) 334 | while queue: 335 | elem = queue.popleft() 336 | yield elem 337 | for child in children_selector(elem): 338 | queue.append(child) 339 | return MoreEnumerable(inner) 340 | 341 | @staticmethod 342 | def traverse_depth_first( 343 | root: TSource, 344 | children_selector: Callable[[TSource], Iterable[TSource]], 345 | ) -> MoreEnumerable[TSource]: 346 | def inner(): 347 | stack = [root] 348 | while stack: 349 | elem = stack.pop() 350 | yield elem 351 | children = [*children_selector(elem)] 352 | for child in reversed(children): 353 | stack.append(child) 354 | return MoreEnumerable(inner) 355 | 356 | def traverse_topological(self, 357 | children_selector: Callable[[TSource_co], Iterable[TSource_co]], 358 | ) -> MoreEnumerable[TSource_co]: 359 | return self.traverse_topological2(children_selector, identity) 360 | 361 | def traverse_topological2(self, 362 | children_selector: Callable[[TSource_co], Iterable[TSource_co]], 363 | key_selector: Callable[[TSource_co], object], 364 | ) -> MoreEnumerable[TSource_co]: 365 | def inner(): 366 | stack: List[Tuple[TSource_co, bool]] = [] 367 | visited = ComposeMap() 368 | result: List[TSource_co] = [] # post order 369 | result_keys = ComposeSet() 370 | # NOTE: a copy of the sequence is made in this call because it falls back 371 | for root in self.reverse(): 372 | if not visited.get(key_selector(root)): 373 | stack.append((root, False)) 374 | while stack: 375 | node, done = stack.pop() 376 | if done: 377 | # detect cycle. normally edges from node will go to nodes in the 378 | # result list. otherwise we found cycle 379 | for child in children_selector(node): 380 | if key_selector(child) not in result_keys: 381 | raise DirectedGraphNotAcyclicError((node, child)) 382 | # good 383 | result.append(node) 384 | result_keys.add(key_selector(node)) 385 | continue 386 | visited[key_selector(node)] = True 387 | stack.append((node, True)) 388 | for child in children_selector(node): 389 | if not visited.get(key_selector(child)): 390 | stack.append((child, False)) 391 | while result: 392 | yield result.pop() 393 | return MoreEnumerable(inner) 394 | -------------------------------------------------------------------------------- /types_linq/more/more_enumerable.pyi: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable, Iterable, Optional, Tuple, overload 2 | 3 | from ..enumerable import Enumerable 4 | from .extrema_enumerable import ExtremaEnumerable 5 | from .more_enums import RankMethods 6 | from ..more_typing import ( 7 | TAccumulate, 8 | TKey, 9 | TResult, 10 | TSource, 11 | TSource_co, 12 | TSupportsLessThan, 13 | ) 14 | 15 | 16 | class MoreEnumerable(Enumerable[TSource_co]): 17 | ''' 18 | ```py 19 | from types_linq.more import MoreEnumerable 20 | ``` 21 | 22 | MoreEnumerable provides more query methods. Instances of this class can be created by directly 23 | constructing, using `as_more()`, or invoking MoreEnumerable methods that return MoreEnumerable 24 | instead of Enumerable. 25 | 26 | These APIs may have breaking changes more frequently than those in Enumerable class because updates 27 | in .NET are happening and sometimes ones of these APIs could be moved to Enumerable with modification, 28 | or changed to accommodate changes to Enumerable. 29 | 30 | Revisions 31 | ~ v0.2.0: New. 32 | ''' 33 | 34 | @overload 35 | def aggregate_right(self, 36 | __seed: TAccumulate, 37 | __func: Callable[[TSource_co, TAccumulate], TAccumulate], 38 | __result_selector: Callable[[TAccumulate], TResult], 39 | ) -> TResult: 40 | ''' 41 | Applies a right-associative accumulator function over the sequence. The seed is used as 42 | the initial accumulator value, and the result_selector is used to select the result value. 43 | 44 | Revisions 45 | ~ v1.2.0: Fixed annotation for __func. 46 | ''' 47 | 48 | @overload 49 | def aggregate_right(self, 50 | __seed: TAccumulate, 51 | __func: Callable[[TSource_co, TAccumulate], TAccumulate], 52 | ) -> TAccumulate: 53 | ''' 54 | Applies a right-associative accumulator function over the sequence. The seed is used as the 55 | initial accumulator value. 56 | 57 | Example 58 | ```py 59 | >>> values = [9, 4, 2] 60 | >>> MoreEnumerable(values).aggregate_right('null', lambda e, rr: f'(cons {e} {rr})') 61 | '(cons 9 (cons 4 (cons 2 null)))' 62 | ``` 63 | 64 | Revisions 65 | ~ v1.2.0: Fixed annotation for __func. 66 | ''' 67 | 68 | @overload 69 | def aggregate_right(self, 70 | __func: Callable[[TSource_co, TSource_co], TSource_co], 71 | ) -> TSource_co: 72 | ''' 73 | Applies a right-associative accumulator function over the sequence. Raises `InvalidOperationError` 74 | if there is no value in the sequence. 75 | 76 | Example 77 | ```py 78 | >>> values = ['9', '4', '2', '5'] 79 | >>> MoreEnumerable(values).aggregate_right(lambda e, rr: f'({e}+{rr})') 80 | '(9+(4+(2+5)))' 81 | ``` 82 | 83 | Revisions 84 | ~ v1.2.0: Fixed annotation for __func. 85 | ''' 86 | 87 | def as_more(self) -> MoreEnumerable[TSource_co]: 88 | ''' 89 | Returns the original MoreEnumerable reference. 90 | ''' 91 | 92 | def consume(self) -> None: 93 | ''' 94 | Consumes the sequence completely. This method iterates the sequence immediately and does not save 95 | any intermediate data. 96 | 97 | Revisions 98 | ~ v1.1.0: New. 99 | ''' 100 | 101 | def cycle(self, count: Optional[int] = None) -> MoreEnumerable[TSource_co]: 102 | ''' 103 | Repeats the sequence `count` times. 104 | 105 | If `count` is `None`, the sequence is infinite. Raises `InvalidOperationError` if `count` 106 | is negative. 107 | 108 | Example 109 | ```py 110 | >>> MoreEnumerable([1, 2, 3]).cycle(3).to_list() 111 | [1, 2, 3, 1, 2, 3, 1, 2, 3] 112 | ``` 113 | 114 | Revisions 115 | ~ v1.1.0: New. 116 | ''' 117 | 118 | def enumerate(self, start: int = 0) -> MoreEnumerable[Tuple[int, TSource_co]]: 119 | ''' 120 | Returns a sequence of tuples containing the index and the value from the source sequence. `start` 121 | is used to specify the starting index. 122 | 123 | Example 124 | ```py 125 | >>> ints = [2, 4, 6] 126 | >>> MoreEnumerable(ints).enumerate().to_list() 127 | [(0, 2), (1, 4), (2, 6)] 128 | ``` 129 | 130 | Revisions 131 | ~ v1.0.0: New. 132 | ''' 133 | 134 | def except_by2(self, 135 | second: Iterable[TSource_co], 136 | key_selector: Callable[[TSource_co], object], 137 | ) -> MoreEnumerable[TSource_co]: 138 | ''' 139 | Produces the set difference of two sequences: self - second, according to a key selector that 140 | determines "distinctness". Note the second iterable is homogenous to self. 141 | 142 | Example 143 | ```py 144 | >>> first = [(16, 'x'), (9, 'y'), (12, 'd'), (16, 't')] 145 | >>> second = [(24, 'd'), (77, 'y')] 146 | >>> MoreEnumerable(first).except_by2(second, lambda x: x[1]).to_list() 147 | [(16, 'x'), (16, 't')] 148 | ``` 149 | 150 | Revisions 151 | ~ v1.0.0: Renamed from `except_by()` to this name to accommodate an update to Enumerable class. 152 | ~ v0.2.1: Added preliminary support for unhashable keys. 153 | ''' 154 | 155 | @overload 156 | def flatten(self) -> MoreEnumerable[Any]: 157 | ''' 158 | Flattens the sequence containing arbitrarily-nested subsequences. 159 | 160 | Note: the nested objects must be Iterable to be flatten. 161 | Instances of `str` or `bytes` are not flattened. 162 | 163 | Example 164 | ```py 165 | >>> lst = ['apple', ['orange', ['juice', 'mango'], 'delta function']] 166 | >>> MoreEnumerable(lst).flatten().to_list() 167 | ['apple', 'orange', 'juice', 'mango', 'delta function'] 168 | ``` 169 | ''' 170 | 171 | @overload 172 | def flatten(self, __predicate: Callable[[Iterable[Any]], bool]) -> MoreEnumerable[Any]: 173 | ''' 174 | Flattens the sequence containing arbitrarily-nested subsequences. A predicate function determines 175 | whether a nested iterable should be flattened or not. 176 | 177 | Note: the nested objects must be Iterable to be flatten. 178 | ''' 179 | 180 | def flatten2(self, selector: Callable[[Any], Optional[Iterable[object]]]) -> MoreEnumerable[Any]: 181 | ''' 182 | Flattens the sequence containing arbitrarily-nested subsequences. A selector is used to select a 183 | subsequence based on the object's properties. If the selector returns None, then the object is 184 | considered a leaf. 185 | ''' 186 | 187 | def for_each(self, action: Callable[[TSource_co], object]) -> None: 188 | ''' 189 | Executes the given function on each element in the source sequence. The return values are discarded. 190 | 191 | Example 192 | ```py 193 | >>> def gen(): 194 | ... yield 116; yield 35; yield -9 195 | 196 | >>> Enumerable(gen()).where(lambda x: x > 0).as_more().for_each(print) 197 | 116 198 | 35 199 | ``` 200 | ''' 201 | 202 | def for_each2(self, action: Callable[[TSource_co, int], object]) -> None: 203 | ''' 204 | Executes the given function on each element in the source sequence. Each element's index is used in 205 | the logic of the function. The return values are discarded. 206 | ''' 207 | 208 | def interleave(self, *iters: Iterable[TSource_co]) -> MoreEnumerable[TSource_co]: 209 | ''' 210 | Interleaves the elements of two or more sequences into a single sequence, skipping sequences if they 211 | are consumed. 212 | 213 | Example 214 | ```py 215 | >>> MoreEnumerable(['1', '2']).interleave(['4', '5', '6'], ['7', '8', '9']).to_list() 216 | ['1', '4', '7', '2', '5', '8', '6', '9'] 217 | ``` 218 | ''' 219 | 220 | @overload 221 | def maxima_by(self, 222 | selector: Callable[[TSource_co], TSupportsLessThan], 223 | ) -> ExtremaEnumerable[TSource_co, TSupportsLessThan]: 224 | ''' 225 | Returns the maximal elements of the sequence based on the given selector. 226 | 227 | Example 228 | ```py 229 | >>> strings = ['foo', 'bar', 'cheese', 'orange', 'baz', 'spam', 'egg', 'toasts', 'dish'] 230 | >>> MoreEnumerable(strings).maxima_by(len).to_list() 231 | ['cheese', 'orange', 'toasts'] 232 | >>> MoreEnumerable(strings).maxima_by(lambda x: x.count('e')).first() 233 | 'cheese' 234 | ``` 235 | ''' 236 | 237 | @overload 238 | def maxima_by(self, 239 | selector: Callable[[TSource_co], TKey], 240 | __comparer: Callable[[TKey, TKey], int], 241 | ) -> ExtremaEnumerable[TSource_co, TKey]: 242 | ''' 243 | Returns the maximal elements of the sequence based on the given selector and the comparer. 244 | 245 | Such comparer takes two values and return positive ints when lhs > rhs, negative ints 246 | if lhs < rhs, and 0 if they are equal. 247 | ''' 248 | 249 | @overload 250 | def minima_by(self, 251 | selector: Callable[[TSource_co], TSupportsLessThan], 252 | ) -> ExtremaEnumerable[TSource_co, TSupportsLessThan]: 253 | ''' 254 | Returns the minimal elements of the sequence based on the given selector. 255 | ''' 256 | 257 | @overload 258 | def minima_by(self, 259 | selector: Callable[[TSource_co], TKey], 260 | __comparer: Callable[[TKey, TKey], int], 261 | ) -> ExtremaEnumerable[TSource_co, TKey]: 262 | ''' 263 | Returns the minimal elements of the sequence based on the given selector and the comparer. 264 | 265 | Such comparer takes two values and return positive ints when lhs > rhs, negative ints 266 | if lhs < rhs, and 0 if they are equal. 267 | ''' 268 | 269 | def pipe(self, action: Callable[[TSource_co], object]) -> MoreEnumerable[TSource_co]: 270 | ''' 271 | Executes the given action on each element in the sequence and yields it. Return values of 272 | action are discarded. 273 | 274 | Example 275 | ```py 276 | >>> store = set() 277 | >>> MoreEnumerable([1, 2, 2, 1]).pipe(store.add).where(lambda x: x % 2 == 0).to_list() 278 | [2, 2] 279 | >>> store 280 | {1, 2} 281 | ``` 282 | 283 | Revisions 284 | ~ v0.2.1: New. 285 | ''' 286 | 287 | # note: diffrent from morelinq: identity is first parameter 288 | def pre_scan(self, 289 | identity: TAccumulate, 290 | transformation: Callable[[TAccumulate, TSource_co], TAccumulate], 291 | ) -> MoreEnumerable[TAccumulate]: 292 | ''' 293 | Performs a pre-scan (exclusive prefix sum) over the sequence. Such scan returns an 294 | equal-length sequence where the first element is the identity, and i-th element (i>1) is 295 | the sum of the first i-1 (and identity) elements in the original sequence. 296 | 297 | Example 298 | ```py 299 | >>> values = [9, 4, 2, 5, 7] 300 | >>> MoreEnumerable(values).pre_scan(0, lambda acc, e: acc + e).to_list() 301 | [0, 9, 13, 15, 20] 302 | >>> MoreEnumerable([]).pre_scan(0, lambda acc, e: acc + e).to_list() 303 | [] 304 | ``` 305 | 306 | Revisions 307 | ~ v1.2.0: New. 308 | ''' 309 | 310 | @overload 311 | def rank(self: MoreEnumerable[TSupportsLessThan], 312 | *, 313 | method: RankMethods = RankMethods.dense, 314 | ) -> MoreEnumerable[int]: 315 | ''' 316 | Ranks each item in the sequence in descending order using the method provided. 317 | 318 | Example 319 | ```py 320 | >>> scores = [1, 4, 77, 23, 23, 4, 9, 0, -7, 101, 23] 321 | >>> MoreEnumerable(scores).rank().to_list() 322 | [6, 5, 2, 3, 3, 5, 4, 7, 8, 1, 3] # 101 is largest, so has rank of 1 323 | 324 | >>> MoreEnumerable(scores).rank(method=RankMethods.competitive).to_list() 325 | [9, 7, 2, 3, 3, 7, 6, 10, 11, 1, 3] # there are no 4th or 5th since there 326 | # are three 3rd's 327 | 328 | >>> MoreEnumerable(scores).rank(method=RankMethods.ordinal).to_list() 329 | [9, 7, 2, 3, 4, 8, 6, 10, 11, 1, 5] # as in sorting 330 | ``` 331 | 332 | Revisions 333 | ~ v1.2.1: Added method parameter to support more ranking methods. 334 | ~ v1.0.0: New. 335 | ''' 336 | 337 | @overload 338 | def rank(self, 339 | __comparer: Callable[[TSource_co, TSource_co], int], 340 | *, 341 | method: RankMethods = RankMethods.dense, 342 | ) -> MoreEnumerable[int]: 343 | ''' 344 | Ranks each item in the sequence in descending order using the given comparer and the 345 | method. 346 | 347 | Such comparer takes two values and return positive ints when lhs > rhs, negative ints 348 | if lhs < rhs, and 0 if they are equal. 349 | 350 | Revisions 351 | ~ v1.2.1: Added method parameter to support more ranking methods. 352 | ~ v1.0.0: New. 353 | ''' 354 | 355 | @overload 356 | def rank_by(self, 357 | key_selector: Callable[[TSource_co], TSupportsLessThan], 358 | *, 359 | method: RankMethods = RankMethods.dense, 360 | ) -> MoreEnumerable[int]: 361 | ''' 362 | Ranks each item in the sequence in descending order using the given selector and the 363 | method. 364 | 365 | Example 366 | ```py 367 | >>> scores = [ 368 | ... {'name': 'Frank', 'score': 75}, 369 | ... {'name': 'Alica', 'score': 90}, 370 | ... {'name': 'Erika', 'score': 99}, 371 | ... {'name': 'Rogers', 'score': 90}, 372 | ... ] 373 | 374 | >>> MoreEnumerable(scores).rank_by(lambda x: x['score']) \\ 375 | ... .zip(scores) \\ 376 | ... .group_by(lambda t: t[0], lambda t: t[1]['name']) \\ 377 | ... .to_dict(lambda g: g.key, lambda g: g.to_list()) 378 | {3: ['Frank'], 2: ['Alica', 'Rogers'], 1: ['Erika']} 379 | ``` 380 | 381 | Revisions 382 | ~ v1.2.1: Added method parameter to support more ranking methods. 383 | ~ v1.0.0: New. 384 | ''' 385 | 386 | @overload 387 | def rank_by(self, 388 | key_selector: Callable[[TSource_co], TKey], 389 | __comparer: Callable[[TKey, TKey], int], 390 | *, 391 | method: RankMethods = RankMethods.dense, 392 | ) -> MoreEnumerable[int]: 393 | ''' 394 | Ranks each item in the sequence in descending order using the given selector, comparer 395 | and the method. 396 | 397 | Such comparer takes two values and return positive ints when lhs > rhs, negative ints 398 | if lhs < rhs, and 0 if they are equal. 399 | 400 | Revisions 401 | ~ v1.2.1: Added method parameter to support more ranking methods. 402 | ~ v1.0.0: New. 403 | ''' 404 | 405 | @overload 406 | def run_length_encode(self) -> MoreEnumerable[Tuple[TSource_co, int]]: 407 | ''' 408 | Run-length encodes the sequence into a sequence of tuples where each tuple contains an 409 | (the first) element and its number of contingent occurrences, where equality is based on 410 | `==`. 411 | 412 | Example 413 | ```py 414 | >>> MoreEnumerable('abbcaeeeaa').run_length_encode().to_list() 415 | [('a', 1), ('b', 2), ('c', 1), ('a', 1), ('e', 3), ('a', 2)] 416 | ``` 417 | 418 | Revisions 419 | ~ v1.1.0: New. 420 | ''' 421 | 422 | @overload 423 | def run_length_encode(self, 424 | __comparer: Callable[[TSource_co, TSource_co], bool], 425 | ) -> MoreEnumerable[Tuple[TSource_co, int]]: 426 | ''' 427 | Run-length encodes the sequence into a sequence of tuples where each tuple contains an 428 | (the first) element and its number of contingent occurrences, where equality is determined by 429 | the comparer. 430 | 431 | Example 432 | ```py 433 | >>> MoreEnumerable('abBBbcaEeeff') \\ 434 | >>> .run_length_encode(lambda x, y: x.lower() == y.lower()).to_list() 435 | [('a', 1), ('b', 4), ('c', 1), ('a', 1), ('E', 3), ('f', 2)] 436 | ``` 437 | 438 | Revisions 439 | ~ v1.1.0: New. 440 | ''' 441 | 442 | @overload 443 | def scan(self, 444 | __transformation: Callable[[TSource_co, TSource_co], TSource_co], 445 | ) -> MoreEnumerable[TSource_co]: 446 | ''' 447 | Performs a inclusive prefix sum over the sequence. Such scan returns an equal-length sequence 448 | where the i-th element is the sum of the first i elements in the original sequence. 449 | 450 | Example 451 | ```py 452 | >>> values = [9, 4, 2, 5, 7] 453 | >>> MoreEnumerable(values).scan(lambda acc, e: acc + e).to_list() 454 | [9, 13, 15, 20, 27] 455 | >>> MoreEnumerable([]).scan(lambda acc, e: acc + e).to_list() 456 | [] 457 | ``` 458 | 459 | Example 460 | ```py 461 | >>> # running max 462 | >>> fruits = ['apple', 'mango', 'orange', 'passionfruit', 'grape'] 463 | >>> MoreEnumerable(fruits).scan(lambda acc, e: e if len(e) > len(acc) else acc).to_list() 464 | ['apple', 'apple', 'orange', 'passionfruit', 'passionfruit'] 465 | ``` 466 | 467 | Revisions 468 | ~ v1.2.0: New. 469 | ''' 470 | 471 | @overload 472 | def scan(self, 473 | __seed: TAccumulate, 474 | __transformation: Callable[[TAccumulate, TSource_co], TAccumulate], 475 | ) -> MoreEnumerable[TAccumulate]: 476 | ''' 477 | Like Enumerable.aggregate(seed, transformation) except that the intermediate results are 478 | included in the result sequence. 479 | 480 | Example 481 | ```py 482 | >>> Enumerable.range(1, 5).as_more().scan(-1, lambda acc, e: acc * e).to_list() 483 | [-1, -1, -2, -6, -24, -120] 484 | ``` 485 | 486 | Revisions 487 | ~ v1.2.0: New. 488 | ''' 489 | 490 | @overload 491 | def scan_right(self, 492 | __func: Callable[[TSource_co, TSource_co], TSource_co], 493 | ) -> MoreEnumerable[TSource_co]: 494 | ''' 495 | Performs a right-associative inclusive prefix sum over the sequence. This is the 496 | right-associative version of MoreEnumerable.scan(func). 497 | 498 | Example 499 | ```py 500 | >>> values = ['9', '4', '2', '5'] 501 | >>> MoreEnumerable(values).scan_right(lambda e, rr: f'({e}+{rr})').to_list() 502 | ['(9+(4+(2+5)))', '(4+(2+5))', '(2+5)', '5'] 503 | >>> MoreEnumerable([]).scan_right(lambda e, rr: e + rr).to_list() 504 | [] 505 | ``` 506 | 507 | Revisions 508 | ~ v1.2.0: New. 509 | ''' 510 | 511 | @overload 512 | def scan_right(self, 513 | __seed: TAccumulate, 514 | __func: Callable[[TSource_co, TAccumulate], TAccumulate], 515 | ) -> MoreEnumerable[TAccumulate]: 516 | ''' 517 | The right-associative version of MoreEnumerable.scan(seed, func). 518 | 519 | Example 520 | ```py 521 | >>> values = [9, 4, 2] 522 | >>> MoreEnumerable(values).scan_right('null', lambda e, rr: f'(cons {e} {rr})').to_list() 523 | ['(cons 9 (cons 4 (cons 2 null)))', '(cons 4 (cons 2 null))', '(cons 2 null)', 'null'] 524 | ``` 525 | 526 | Revisions 527 | ~ v1.2.0: New. 528 | ''' 529 | 530 | def segment(self, 531 | new_segment_predicate: Callable[[TSource_co], bool], 532 | ) -> MoreEnumerable[MoreEnumerable[TSource_co]]: 533 | ''' 534 | Splits the sequence into segments by using a detector function that returns True to signal a 535 | new segment. 536 | 537 | Example 538 | ```py 539 | >>> values = [0, 1, 2, 4, -4, -2, 6, 2, -2] 540 | >>> MoreEnumerable(values).segment(lambda x: x < 0).select(lambda x: x.to_list()).to_list() 541 | [[0, 1, 2, 4], [-4], [-2, 6, 2], [-2]] 542 | ``` 543 | 544 | Revisions 545 | ~ v1.2.0: New. 546 | ''' 547 | 548 | def segment2(self, 549 | new_segment_predicate: Callable[[TSource_co, int], bool], 550 | ) -> MoreEnumerable[MoreEnumerable[TSource_co]]: 551 | ''' 552 | Splits the sequence into segments by using a detector function that returns True to signal a 553 | new segment. The element's index is used in the detector function. 554 | 555 | Example 556 | ```py 557 | >>> values = [0, 1, 2, 4, -4, -2, 6, 2, -2] 558 | >>> MoreEnumerable(values).segment2(lambda x, i: x < 0 or i % 3 == 0) \\ 559 | ... .select(lambda x: x.to_list()) \\ 560 | ... .to_list() 561 | [[0, 1, 2], [4], [-4], [-2], [6, 2], [-2]] 562 | ``` 563 | 564 | Revisions 565 | ~ v1.2.0: New. 566 | ''' 567 | 568 | def segment3(self, 569 | new_segment_predicate: Callable[[TSource_co, TSource_co, int], bool], 570 | ) -> MoreEnumerable[MoreEnumerable[TSource_co]]: 571 | ''' 572 | Splits the sequence into segments by using a detector function that returns True to signal a 573 | new segment. The last element and the current element's index are used in the detector 574 | function. 575 | 576 | Example 577 | ```py 578 | >>> values = [0, 1, 2, 4, -4, -2, 6, 2, -2] 579 | >>> MoreEnumerable(values).segment3(lambda curr, prev, i: curr * prev < 0) \\ 580 | ... .select(lambda x: x.to_list()) \\ 581 | ... .to_list() 582 | [[0, 1, 2, 4], [-4, -2], [6, 2], [-2]] 583 | ``` 584 | 585 | Revisions 586 | ~ v1.2.0: New. 587 | ''' 588 | 589 | @staticmethod 590 | def traverse_breath_first( 591 | root: TSource, 592 | children_selector: Callable[[TSource], Iterable[TSource]], 593 | ) -> MoreEnumerable[TSource]: 594 | ''' 595 | Traverses the tree (graph) from the root node in a breath-first fashion. A selector is used to 596 | select children of each node. 597 | 598 | Graphs are not checked for cycles or duplicates visits. If the resulting sequence needs to be 599 | finite then it is the responsibility of children_selector to ensure that duplicate nodes are not 600 | visited. 601 | 602 | Example 603 | ```py 604 | >>> tree = { 3: [1, 4], 1: [0, 2], 4: [5] } 605 | >>> MoreEnumerable.traverse_breath_first(3, lambda x: tree.get(x, [])) \\ 606 | >>> .to_list() 607 | [3, 1, 4, 0, 2, 5] 608 | ``` 609 | ''' 610 | 611 | @staticmethod 612 | def traverse_depth_first( 613 | root: TSource, 614 | children_selector: Callable[[TSource], Iterable[TSource]], 615 | ) -> MoreEnumerable[TSource]: 616 | ''' 617 | Traverses the tree (graph) from the root node in a depth-first fashion. A selector is used to 618 | select children of each node. 619 | 620 | Graphs are not checked for cycles or duplicates visits. If the resulting sequence needs to be 621 | finite then it is the responsibility of children_selector to ensure that duplicate nodes are not 622 | visited. 623 | 624 | Example 625 | ```py 626 | >>> tree = { 3: [1, 4], 1: [0, 2], 4: [5] } 627 | >>> MoreEnumerable.traverse_depth_first(3, lambda x: tree.get(x, [])) \\ 628 | >>> .to_list() 629 | [3, 1, 0, 2, 4, 5] 630 | ``` 631 | ''' 632 | 633 | # NOTE: leave room for comparer overloads 634 | # TODO: when hasher support is done 635 | def traverse_topological(self, 636 | children_selector: Callable[[TSource_co], Iterable[TSource_co]], 637 | ) -> MoreEnumerable[TSource_co]: 638 | ''' 639 | Traverses the graph in topological order, A selector is used to select children of each 640 | node. The ordering created from this method is a variant of depth-first traversal and ensures 641 | duplicate nodes are output once. 642 | 643 | To invoke this method, the self sequence contains nodes with zero in-degrees to start the 644 | iteration. Passing a list of all nodes is allowed although not required. 645 | 646 | Raises `DirectedGraphNotAcyclicError` if the directed graph contains a cycle and the 647 | topological ordering cannot be produced. 648 | 649 | Example 650 | ```py 651 | >>> adj = { 5: [2, 0], 4: [0, 1], 2: [3], 3: [1] } 652 | >>> MoreEnumerable([5, 4]).traverse_topological(lambda x: adj.get(x, [])) \\ 653 | >>> .to_list() 654 | [5, 2, 3, 4, 0, 1] 655 | ``` 656 | 657 | Revisions 658 | ~ v1.2.1: New. 659 | ''' 660 | 661 | def traverse_topological2(self, 662 | children_selector: Callable[[TSource_co], Iterable[TSource_co]], 663 | key_selector: Callable[[TSource_co], object], 664 | ) -> MoreEnumerable[TSource_co]: 665 | ''' 666 | Traverses the graph in topological order, A selector is used to select children of each 667 | node. The ordering created from this method is a variant of depth-first traversal and 668 | ensures duplicate nodes are output once. A key selector is used to determine equality 669 | between nodes. 670 | 671 | To invoke this method, the self sequence contains nodes with zero in-degrees to start the 672 | iteration. Passing a list of all nodes is allowed although not required. 673 | 674 | Raises `DirectedGraphNotAcyclicError` if the directed graph contains a cycle and the 675 | topological ordering cannot be produced. 676 | 677 | Revisions 678 | ~ v1.2.1: New. 679 | ''' 680 | -------------------------------------------------------------------------------- /types_linq/more/more_enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, auto 2 | 3 | 4 | class RankMethods(Enum): 5 | ''' 6 | ```py 7 | from types_linq.more import RankMethods 8 | ``` 9 | 10 | Enumeration to select different methods of assigning rankings when breaking 11 | [ties](https://en.wikipedia.org/wiki/Ranking#Strategies_for_assigning_rankings). 12 | 13 | Revisions 14 | ~ v1.2.1: New. 15 | ''' 16 | dense = auto() 17 | ''' 18 | Items that compare equally receive the same ranking, and the next items get the immediately 19 | following ranking. *(1223)* 20 | ''' 21 | 22 | competitive = auto() 23 | ''' 24 | Items that compare equally receive the same highest ranking, and gaps are left out. *(1224)* 25 | ''' 26 | 27 | ordinal = auto() 28 | ''' 29 | Each item receives unique rankings. *(1234)* 30 | ''' 31 | -------------------------------------------------------------------------------- /types_linq/more/more_error.py: -------------------------------------------------------------------------------- 1 | # class 'MoreError' is nonexistent 2 | from __future__ import annotations 3 | from typing import Tuple 4 | 5 | from ..types_linq_error import InvalidOperationError 6 | 7 | 8 | class DirectedGraphNotAcyclicError(InvalidOperationError): 9 | ''' 10 | ```py 11 | from types_linq.more import DirectedGraphNotAcyclicError 12 | ``` 13 | 14 | Exception raised when a cycle exists in a graph. 15 | 16 | Revisions 17 | ~ v1.2.1: New. 18 | ''' 19 | 20 | def __init__(self, cycle: Tuple[object, object]) -> None: 21 | super().__init__('cycle detected') 22 | self._cycle = cycle 23 | 24 | @property 25 | def cycle(self) -> Tuple[object, object]: 26 | ''' 27 | The two elements (A, B) in this tuple are part of a cycle. There exists an edge from A to B, 28 | and a path from B back to A. A and B may be identical. 29 | 30 | Example 31 | ```py 32 | >>> adj = { 5: [2, 0], 4: [0, 1], 2: [3], 3: [1, 5] } 33 | >>> try: 34 | >>> MoreEnumerable([5, 4]).traverse_topological(lambda x: adj.get(x, [])) \\ 35 | >>> .consume() 36 | >>> except DirectedGraphNotAcyclicError as e: 37 | >>> print(e.cycle) 38 | (3, 5) # 3 -> 5 -> 2 -> 3 39 | ``` 40 | ''' 41 | return self._cycle 42 | -------------------------------------------------------------------------------- /types_linq/more_typing.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Typing utilities used by methods's declarations across the library. For more details, see 3 | [`typing`](https://docs.python.org/3/library/typing.html). 4 | ```{note} Definitions in this module are for documenting purposes only. 5 | ``` 6 | ''' 7 | import sys 8 | from abc import ABCMeta, abstractmethod 9 | from typing import Any, TypeVar 10 | 11 | if sys.version_info >= (3, 8, 0): 12 | from typing import Protocol, runtime_checkable 13 | else: 14 | from typing_extensions import Protocol, runtime_checkable # type: ignore 15 | 16 | 17 | TAccumulate = TypeVar('TAccumulate') 18 | 'A generic type parameter.' 19 | 20 | TAverage_co = TypeVar('TAverage_co', covariant=True) 21 | 'A generic covariant type parameter.' 22 | 23 | TCollection = TypeVar('TCollection') 24 | 'A generic type parameter.' 25 | 26 | TDefault = TypeVar('TDefault') 27 | 'A generic type parameter.' 28 | 29 | TInner = TypeVar('TInner') 30 | 'A generic type parameter.' 31 | 32 | TKey = TypeVar('TKey') 33 | 'A generic type parameter.' 34 | 35 | TKey2 = TypeVar('TKey2') 36 | 'A generic type parameter.' 37 | 38 | TKey_co = TypeVar('TKey_co', covariant=True) 39 | 'A generic covariant type parameter.' 40 | 41 | TOther = TypeVar('TOther') 42 | 'A generic type parameter.' 43 | 44 | TOther2 = TypeVar('TOther2') 45 | 'A generic type parameter.' 46 | 47 | TOther3 = TypeVar('TOther3') 48 | 'A generic type parameter.' 49 | 50 | TOther4 = TypeVar('TOther4') 51 | 'A generic type parameter.' 52 | 53 | TResult = TypeVar('TResult') 54 | 'A generic type parameter.' 55 | 56 | TSelf = TypeVar('TSelf') 57 | 'A generic type parameter.' 58 | 59 | TSource = TypeVar('TSource') 60 | 'A generic type parameter.' 61 | 62 | TSource_co = TypeVar('TSource_co', covariant=True) 63 | 'A generic covariant type parameter.' 64 | 65 | TValue = TypeVar('TValue') 66 | 'A generic type parameter.' 67 | 68 | TValue_co = TypeVar('TValue_co', covariant=True) 69 | 'A generic covariant type parameter.' 70 | 71 | 72 | @runtime_checkable 73 | class SupportsAverage(Protocol[TAverage_co]): 74 | ''' 75 | Instances of this protocol supports the averaging operation. that is, if `x` is such an instance, 76 | and `N` is an integer, then `(x + x + ...) / N` is allowed, and has the type `TAverage_co`. 77 | ''' 78 | @abstractmethod 79 | def __add__(self: TSelf, __o: TSelf) -> TSelf: ... 80 | @abstractmethod 81 | def __truediv__(self, __o: int) -> TAverage_co: ... 82 | 83 | 84 | @runtime_checkable 85 | class SupportsLessThan(Protocol, metaclass=ABCMeta): 86 | ''' 87 | Instances of this protocol supports the `<` operation. 88 | 89 | Even though they may be unimplemented, the existence of `<` implies the existence of `>`, 90 | and probably `==`, `!=`, `<=` and `>=`. 91 | ''' 92 | @abstractmethod 93 | def __lt__(self, __o: Any) -> bool: ... 94 | 95 | @runtime_checkable 96 | class SupportsAdd(Protocol, metaclass=ABCMeta): 97 | ''' 98 | Instances of this protocol supports the homogeneous `+` operation. 99 | ''' 100 | @abstractmethod 101 | def __add__(self: TSelf, __o: TSelf) -> TSelf: ... 102 | 103 | 104 | # a type like 'Hashable' is rn useless because a subclass of a hashable class may not 105 | # be hashable (object -> list) 106 | 107 | TSupportsLessThan = TypeVar('TSupportsLessThan', bound=SupportsLessThan) 108 | 'A generic type parameter that represents a type that `SupportsLessThan`.' 109 | 110 | TSupportsAdd = TypeVar('TSupportsAdd', bound=SupportsAdd) 111 | 'A generic type parameter that represents a type that `SupportsAdd`.' 112 | -------------------------------------------------------------------------------- /types_linq/ordered_enumerable.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Any, Callable, Generic, Iterable, Iterator, Optional 3 | 4 | from .enumerable import Enumerable 5 | 6 | from .more_typing import ( 7 | TSource_co, 8 | TKey, 9 | TKey2, 10 | ) 11 | 12 | 13 | class OrderedEnumerable(Enumerable[TSource_co], Generic[TSource_co, TKey]): 14 | 15 | _parent: Optional[OrderedEnumerable[TSource_co, Any]] 16 | _key_selector: Callable[[TSource_co], TKey] 17 | _comparer: Optional[Callable[[TKey, TKey], int]] 18 | _descending: bool 19 | 20 | def __init__(self, 21 | source: Callable[[], Iterable[TSource_co]], # pass along 22 | parent: Optional[OrderedEnumerable[TSource_co, Any]], 23 | key_selector: Callable[[TSource_co], TKey], 24 | comparer: Optional[Callable[[TKey, TKey], int]], 25 | descending: bool, 26 | ): 27 | super().__init__(source) 28 | self._parent = parent 29 | self._key_selector = key_selector 30 | self._comparer = comparer 31 | self._descending = descending 32 | 33 | def _get_iterable(self) -> Iterator[TSource_co]: 34 | lst = [elem for elem in super()._get_iterable()] 35 | curr = self 36 | while curr is not None: 37 | comparer = curr._comparer 38 | if comparer is None: 39 | # comparer-less overload for order_by(), etc. ensures TSource_co must support __lt__() 40 | key = curr._key_selector # type: ignore 41 | else: 42 | selector = curr._key_selector 43 | class key: 44 | def __init__(self, elem): 45 | self.elem = elem 46 | def __lt__(self, __o: key): 47 | return comparer(selector(self.elem), selector(__o.elem)) < 0 # type: ignore 48 | # it is OK to omit __eq__() because python sort only uses __lt__() 49 | lst.sort(key=key, reverse=curr._descending) 50 | curr = curr._parent 51 | yield from lst 52 | 53 | def create_ordered_enumerable(self, 54 | key_selector: Callable[[TSource_co], TKey2], 55 | comparer: Optional[Callable[[TKey2, TKey2], int]], 56 | descending: bool, 57 | ) -> OrderedEnumerable[TSource_co, TKey2]: 58 | return OrderedEnumerable( 59 | self._get_iterable, 60 | self, 61 | key_selector, 62 | comparer, 63 | descending, 64 | ) 65 | 66 | def then_by(self, 67 | key_selector: Callable[[TSource_co], TKey2], 68 | *args: Callable[[TKey2, TKey2], int], 69 | ) -> OrderedEnumerable[TSource_co, TKey2]: 70 | if len(args) == 1: 71 | comparer = args[0] 72 | else: # len(args) == 2: 73 | comparer = None 74 | return self.create_ordered_enumerable( 75 | key_selector, 76 | comparer, 77 | False, 78 | ) 79 | 80 | def then_by_descending(self, 81 | key_selector: Callable[[TSource_co], TKey2], 82 | *args: Callable[[TKey2, TKey2], int], 83 | ) -> OrderedEnumerable[TSource_co, TKey2]: 84 | if len(args) == 1: 85 | comparer = args[0] 86 | else: # len(args) == 2: 87 | comparer = None 88 | return self.create_ordered_enumerable( 89 | key_selector, 90 | comparer, 91 | True, 92 | ) 93 | -------------------------------------------------------------------------------- /types_linq/ordered_enumerable.pyi: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Callable, Generic, Optional, overload 3 | 4 | from .enumerable import Enumerable 5 | from .more_typing import ( 6 | TSource_co, 7 | TKey, 8 | TKey2, 9 | TSupportsLessThan, 10 | ) 11 | 12 | 13 | class OrderedEnumerable(Enumerable[TSource_co], Generic[TSource_co, TKey]): 14 | ''' 15 | ```py 16 | from types_linq.ordered_enumerable import OrderedEnumerable 17 | ``` 18 | 19 | Represents a sorted Enumerable sequence that is sorted by some key. 20 | 21 | Users should not construct instances of this class directly. Use `Enumerable.order_by()` instead. 22 | ''' 23 | 24 | def __init__(self, *args): ... 25 | 26 | def create_ordered_enumerable(self, 27 | key_selector: Callable[[TSource_co], TKey2], 28 | comparer: Optional[Callable[[TKey2, TKey2], int]], 29 | descending: bool, 30 | ) -> OrderedEnumerable[TSource_co, TKey2]: 31 | ''' 32 | Performs a subsequent ordering on the elements of the sequence according to a key. 33 | 34 | Comparer takes two values and return positive ints when lhs > rhs, negative ints 35 | if lhs < rhs, and 0 if they are equal. 36 | 37 | Revisions 38 | ~ v0.1.2: Fixed incorrect parameter type of comparer. 39 | ''' 40 | 41 | @overload 42 | def then_by(self, 43 | key_selector: Callable[[TSource_co], TSupportsLessThan], 44 | ) -> OrderedEnumerable[TSource_co, TSupportsLessThan]: 45 | ''' 46 | Performs a subsequent ordering of the elements in ascending order according to key. 47 | 48 | Example 49 | ```py 50 | >>> class Pet(NamedTuple): 51 | ... name: str 52 | ... age: int 53 | 54 | >>> pets = [Pet('Barley', 8), Pet('Boots', 4), Pet('Roman', 5), Pet('Daisy', 4)] 55 | >>> Enumerable(pets).order_by(lambda p: p.age) \\ 56 | ... .then_by(lambda p: p.name) \\ 57 | ... .select(lambda p: p.name) \\ 58 | ... .to_list() 59 | ['Boots', 'Daisy', 'Roman', 'Barley'] 60 | ``` 61 | ''' 62 | 63 | @overload 64 | def then_by(self, 65 | key_selector: Callable[[TSource_co], TKey2], 66 | __comparer: Callable[[TKey2, TKey2], int], 67 | ) -> OrderedEnumerable[TSource_co, TKey2]: 68 | ''' 69 | Performs a subsequent ordering of the elements in ascending order by using a specified comparer. 70 | 71 | Such comparer takes two values and return positive ints when lhs > rhs, negative ints 72 | if lhs < rhs, and 0 if they are equal. 73 | ''' 74 | 75 | @overload 76 | def then_by_descending(self, 77 | key_selector: Callable[[TSource_co], TSupportsLessThan], 78 | ) -> OrderedEnumerable[TSource_co, TSupportsLessThan]: 79 | ''' 80 | Performs a subsequent ordering of the elements in descending order according to key. 81 | ''' 82 | 83 | @overload 84 | def then_by_descending(self, 85 | key_selector: Callable[[TSource_co], TKey2], 86 | __comparer: Callable[[TKey2, TKey2], int], 87 | ) -> OrderedEnumerable[TSource_co, TKey2]: 88 | ''' 89 | Performs a subsequent ordering of the elements in descending order by using a specified comparer. 90 | 91 | Such comparer takes two values and return positive ints when lhs > rhs, negative ints 92 | if lhs < rhs, and 0 if they are equal. 93 | ''' 94 | -------------------------------------------------------------------------------- /types_linq/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cleoold/types-linq/63a4a050fb72e67f53f2c03eaa8a6d85b03d0fe6/types_linq/py.typed -------------------------------------------------------------------------------- /types_linq/types_linq_error.py: -------------------------------------------------------------------------------- 1 | class TypesLinqError(Exception): 2 | ''' 3 | ```py 4 | from types_linq import TypesLinqError 5 | ``` 6 | 7 | Types-linq has run into problems. 8 | ''' 9 | 10 | 11 | class InvalidOperationError(TypesLinqError, ValueError): 12 | ''' 13 | ```py 14 | from types_linq import InvalidOperationError 15 | ``` 16 | 17 | Exception raised when a call is invalid for the object's current state. 18 | ''' 19 | 20 | 21 | class IndexOutOfRangeError(TypesLinqError, IndexError): 22 | ''' 23 | ```py 24 | from types_linq import IndexOutOfRangeError 25 | ``` 26 | 27 | An `IndexError` with types-linq flavour. 28 | ''' 29 | -------------------------------------------------------------------------------- /types_linq/util.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Any, Tuple, Iterator, Hashable, List, Dict, Iterable, Set, Optional, MutableMapping, MutableSet 3 | 4 | from .more_typing import ( 5 | TKey, 6 | TSupportsLessThan, 7 | TValue 8 | ) 9 | 10 | 11 | def identity(x: Any) -> Any: 12 | return x 13 | 14 | 15 | def return_second(x: Any, y: TValue) -> TValue: 16 | return y 17 | 18 | 19 | def default_equal(x: TValue, y: TValue) -> bool: 20 | return x == y 21 | 22 | 23 | def default_lt(x: TSupportsLessThan, y: TSupportsLessThan) -> bool: 24 | return x < y 25 | 26 | 27 | def default_gt(x: TSupportsLessThan, y: TSupportsLessThan) -> bool: 28 | return y < x 29 | 30 | 31 | class _ListMap(MutableMapping[TKey, TValue]): 32 | 33 | def __init__(self) -> None: 34 | self._lst: List[Tuple[TKey, TValue]] = list() 35 | 36 | def __setitem__(self, key: TKey, value: TValue) -> None: 37 | for i in range(0, len(self._lst)): 38 | if self._lst[i][0] == key: 39 | self._lst[i] = (self._lst[i][0], value) 40 | return 41 | self._lst.append((key, value)) 42 | 43 | def __getitem__(self, key: TKey) -> TValue: 44 | for k, v in self._lst: 45 | if k == key: 46 | return v 47 | raise KeyError(key) 48 | 49 | def __delitem__(self, key: TKey) -> None: 50 | for i in range(0, len(self._lst)): 51 | if self._lst[i][0] == key: 52 | del self._lst[i] 53 | return 54 | raise KeyError(key) 55 | 56 | def __iter__(self) -> Iterator[TKey]: 57 | return map(lambda e: e[0], self._lst) 58 | 59 | def __len__(self) -> int: 60 | return len(self._lst) 61 | 62 | 63 | # Wrap Map to Set 64 | class _SetWrapper(MutableSet[TValue]): 65 | 66 | def __init__(self, map: MutableMapping[TValue, None]) -> None: 67 | self._map = map 68 | 69 | def __contains__(self, x: object) -> bool: 70 | return x in self._map 71 | 72 | def __len__(self) -> int: 73 | return self._map.__len__() 74 | 75 | def add(self, value: TValue) -> None: 76 | self._map[value] = None 77 | 78 | def discard(self, value: TValue) -> None: 79 | try: 80 | self._map.pop(value) 81 | except KeyError: 82 | # set.discard() don't throw KeyError 83 | # if element doesn't exist 84 | ... 85 | 86 | def __iter__(self) -> Iterator[TValue]: 87 | return self._map.__iter__() 88 | 89 | 90 | class ComposeSet(MutableSet[TValue]): 91 | ''' 92 | A set which support hashable and unhashable type 93 | ''' 94 | _set: Set[TValue] 95 | _cmp_set: MutableSet[TValue] 96 | 97 | def __init__(self, iter: Optional[Iterable[TValue]] = None): 98 | self._set = set() 99 | self._cmp_set = _SetWrapper(_ListMap()) 100 | if iter: 101 | for item in iter: 102 | self.add(item) 103 | 104 | def _get_set(self, x: object) -> MutableSet[TValue]: 105 | if isinstance(x, Hashable): 106 | return self._set 107 | else: 108 | return self._cmp_set 109 | 110 | def __contains__(self, x: object) -> bool: 111 | return x in self._get_set(x) 112 | 113 | def __len__(self) -> int: 114 | return len(self._set) + len(self._cmp_set) 115 | 116 | def __iter__(self) -> Iterator[TValue]: 117 | yield from self._set 118 | yield from self._cmp_set 119 | 120 | def add(self, value: TValue) -> None: 121 | return self._get_set(value).add(value) 122 | 123 | def discard(self, value: TValue) -> None: 124 | return self._get_set(value).discard(value) 125 | 126 | 127 | class ComposeMap(MutableMapping[TKey, TValue]): 128 | ''' 129 | A map which support hashable and unhashable key type 130 | ''' 131 | _map: Dict[TKey, TValue] 132 | _cmp_map: MutableMapping[TKey, TValue] 133 | 134 | def __init__(self, iter: Optional[Iterable[Tuple[TKey, TValue]]] = None) -> None: 135 | self._map = dict() 136 | self._cmp_map = _ListMap() 137 | if iter: 138 | for k, v in iter: 139 | self[k] = v 140 | 141 | def _get_map(self, x: object) -> MutableMapping[TKey, TValue]: 142 | # TODO: this test is not perfectly sufficient. 143 | # counterexample: x is namedtuple('t', ['x'])([]) 144 | if isinstance(x, Hashable): 145 | return self._map 146 | else: 147 | return self._cmp_map 148 | 149 | def __setitem__(self, key: TKey, value: TValue): 150 | self._get_map(key).__setitem__(key, value) 151 | 152 | def __getitem__(self, key: TKey) -> TValue: 153 | return self._get_map(key).__getitem__(key) 154 | 155 | def __delitem__(self, key: TKey) -> None: 156 | return self._get_map(key).__delitem__(key) 157 | 158 | def __iter__(self) -> Iterator[TKey]: 159 | # notice on iteration order: 160 | # if this data structure contains only hashable objects (stored in the dict) 161 | # then the iter order is consistent with dict => using insertion order. this is 162 | # useful for meeting the .net ToLookup() spec. however, if this is not the case 163 | # then the iter order is broken. 164 | yield from self._map 165 | yield from self._cmp_map 166 | 167 | def __len__(self) -> int: 168 | return len(self._map) + len(self._cmp_map) 169 | --------------------------------------------------------------------------------