├── multiset ├── py.typed ├── __init__.pyi └── __init__.py ├── MANIFEST.in ├── setup.py ├── docs ├── requirements.txt ├── api.rst ├── Makefile ├── make.bat ├── conf.py └── index.rst ├── dev-requirements.txt ├── flit.ini ├── .coveragerc ├── .readthedocs.yml ├── Makefile ├── tox.ini ├── .github ├── dependabot.yml └── workflows │ ├── python-publish.yml │ ├── python-dependabot.yml │ └── python-test.yml ├── make.bat ├── tests ├── conftest.py └── test_multiset.py ├── pyproject.toml ├── LICENSE ├── setup.cfg ├── .gitignore ├── README.rst └── .pylintrc /multiset/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.pyi -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup 3 | setup(use_scm_version=True) -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx-autodoc-typehints==1.15.3 2 | sphinx==4.4.0 3 | setuptools-scm==8.0.4 -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | pylint>=1.6,<3 2 | pytest>=3.0,<9 3 | coverage>=6.3,<8 4 | setuptools_scm>=7.0,<9.0 5 | tox>=2.5,<4.0 6 | pytest-cov>=4.0,<5.0 7 | mypy>=1.9,<2 8 | build>=1.2,<2 9 | -------------------------------------------------------------------------------- /flit.ini: -------------------------------------------------------------------------------- 1 | [metadata] 2 | module = multiset 3 | author = Manuel Krebber 4 | author-email = admin@wheerd.de 5 | home-page = https://github.com/wheerd/multiset 6 | classifiers = License :: OSI Approved :: MIT License 7 | 8 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | 4 | [report] 5 | exclude_lines = 6 | pragma: no cover 7 | def __repr__ 8 | def __str__ 9 | raise AssertionError 10 | raise NotImplementedError 11 | assert False 12 | if __name__ == .__main__.: -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API Documentation 2 | ================= 3 | 4 | This page is automatically generated from the docstrings. 5 | 6 | .. automodule:: multiset 7 | :members: 8 | :undoc-members: 9 | :show-inheritance: 10 | :special-members: __init__ 11 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.8" 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | 11 | python: 12 | install: 13 | - requirements: docs/requirements.txt 14 | - method: pip 15 | path: . 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | init: 2 | pip install -r dev-requirements.txt 3 | 4 | test: 5 | py.test tests/ --doctest-modules multiset README.rst 6 | 7 | stubtest: 8 | python -m mypy.stubtest multiset 9 | 10 | check: 11 | pylint multiset 12 | 13 | coverage: 14 | py.test --cov=multiset --cov-report lcov --cov-report term-missing tests/ 15 | 16 | build: 17 | python -m build 18 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py38, py39, py310, py311, py312 8 | 9 | [testenv] 10 | commands = 11 | py.test "{toxinidir}/tests" --doctest-modules --pyargs multiset "{toxinidir}/README.rst" 12 | deps = 13 | pytest>=3.0 14 | 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: pip 8 | directory: "/" 9 | schedule: 10 | interval: daily 11 | time: "04:00" 12 | open-pull-requests-limit: 10 13 | ignore: 14 | - dependency-name: sphinx 15 | versions: 16 | - 3.4.3 17 | - 3.5.0 18 | - 3.5.1 19 | - 3.5.2 20 | - 3.5.3 21 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | deploy: 9 | 10 | runs-on: ubuntu-latest 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.x' 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install build twine 22 | - name: Build and publish 23 | env: 24 | TWINE_USERNAME: __token__ 25 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 26 | run: | 27 | python -m build 28 | twine upload dist/* 29 | -------------------------------------------------------------------------------- /make.bat: -------------------------------------------------------------------------------- 1 | @ECHO off 2 | if /I %1 == init goto :init 3 | if /I %1 == test goto :test 4 | if /I %1 == stubtest goto :stubtest 5 | if /I %1 == check goto :check 6 | if /I %1 == coverage goto :coverage 7 | if /I %1 == build goto :build 8 | 9 | goto :eof 10 | 11 | :init 12 | pip install -r dev-requirements.txt 13 | goto :eof 14 | 15 | :test 16 | py.test tests\ --doctest-modules multiset README.rst 17 | goto :eof 18 | 19 | 20 | :stubtest 21 | python -m mypy.stubtest multiset 22 | goto :eof 23 | 24 | :check 25 | pylint multiset 26 | goto :eof 27 | 28 | :coverage 29 | py.test --cov=multiset --cov-report lcov --cov-report term-missing tests/ 30 | goto :eof 31 | 32 | :build 33 | python -m build 34 | goto :eof -------------------------------------------------------------------------------- /.github/workflows/python-dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot Tests 2 | 3 | on: pull_request_target 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | if: ${{ github.actor == 'dependabot[bot]' }} 9 | strategy: 10 | matrix: 11 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 12 | name: Run tests 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | ref: ${{ github.event.pull_request.head.sha }} 17 | - name: Setup python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | architecture: x64 22 | - name: Install dependencies 23 | run: make init 24 | - name: Run tests 25 | run: make test 26 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys, os 3 | import pytest 4 | 5 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 6 | from multiset import Multiset, FrozenMultiset 7 | 8 | 9 | @pytest.fixture(autouse=True) 10 | def add_default_expressions(doctest_namespace): 11 | doctest_namespace['Multiset'] = Multiset 12 | doctest_namespace['FrozenMultiset'] = FrozenMultiset 13 | 14 | 15 | def pytest_generate_tests(metafunc): 16 | if 'MultisetCls' in metafunc.fixturenames: 17 | metafunc.parametrize('MultisetCls', ['frozen', 'regular'], indirect=True) 18 | 19 | 20 | @pytest.fixture 21 | def MultisetCls(request): 22 | if request.param == 'frozen': 23 | return FrozenMultiset 24 | elif request.param == 'regular': 25 | return Multiset 26 | else: 27 | raise ValueError("Invalid internal test config") 28 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=42", 4 | "setuptools_scm[toml]>=3.4", 5 | "wheel" 6 | ] 7 | build-backend = "setuptools.build_meta" 8 | 9 | [project] 10 | name = "multiset" 11 | requires-python = ">=3.8" 12 | description = "An implementation of a multiset." 13 | license = {text = "MIT"} 14 | classifiers = [ 15 | "Development Status :: 5 - Production/Stable", 16 | "Intended Audience :: Developers", 17 | "License :: OSI Approved :: MIT License", 18 | "Programming Language :: Python", 19 | "Programming Language :: Python :: 3", 20 | "Programming Language :: Python :: 3.8", 21 | "Programming Language :: Python :: 3.9", 22 | "Programming Language :: Python :: 3.10", 23 | "Programming Language :: Python :: 3.11", 24 | "Programming Language :: Python :: 3.12", 25 | ] 26 | authors = [ 27 | {name = "Manuel Krebber", email = "admin@wheerd.de"}, 28 | ] 29 | readme = {file = "README.rst", content-type="text/x-rst"} 30 | dynamic = ["version"] 31 | 32 | [tool.setuptools_scm] 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Manuel Krebber 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = multiset 3 | description = An implementation of a multiset. 4 | long_description = file: README.rst 5 | license = MIT 6 | license_files = LICENSE 7 | classifiers = 8 | Development Status :: 5 - Production/Stable 9 | Intended Audience :: Developers 10 | License :: OSI Approved :: MIT License 11 | Programming Language :: Python 12 | Programming Language :: Python :: 3 13 | Programming Language :: Python :: 3.8 14 | Programming Language :: Python :: 3.9 15 | Programming Language :: Python :: 3.10 16 | Programming Language :: Python :: 3.11 17 | Programming Language :: Python :: 3.12 18 | home_page = https://github.com/wheerd/multiset 19 | repository = https://github.com/wheerd/multiset 20 | documentation = https://multiset.readthedocs.io/ 21 | author = Manuel Krebber 22 | author_email = admin@wheerd.de 23 | readme = file: README.rst 24 | 25 | [options] 26 | zip_safe = True 27 | include_package_data = True 28 | setup_requires = 29 | setuptools >= 46 30 | setuptools_scm 31 | python_requires = >= 3.8 32 | packages = multiset 33 | test_suite = tests 34 | 35 | [options.package_data] 36 | multiset = *.pyi, py.typed 37 | 38 | [flake8] 39 | max-line-length = 120 40 | 41 | [pep8] 42 | max-line-length = 120 43 | 44 | [yapf] 45 | based_on_style = pep8 46 | column_limit = 120 47 | 48 | [aliases] 49 | test=pytest 50 | 51 | [bdist_wheel] 52 | universal=1 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /docs/_build 2 | .vscode 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 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 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # IPython Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # dotenv 82 | .env 83 | 84 | # virtualenv 85 | venv/ 86 | ENV/ 87 | 88 | # Spyder project settings 89 | .spyderproject 90 | 91 | # Rope project settings 92 | .ropeproject 93 | 94 | .DS_Store -------------------------------------------------------------------------------- /.github/workflows/python-test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | if: ${{ github.actor != 'dependabot[bot]' }} 15 | strategy: 16 | matrix: 17 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 18 | fail-fast: false 19 | name: Run tests 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Setup python 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | architecture: x64 27 | - name: Install dependencies 28 | run: make init 29 | - name: Lint 30 | run: make check 31 | if: matrix.python-version == '3.8' 32 | continue-on-error: true 33 | - name: Run tests 34 | run: make test 35 | - name: Run stubtest 36 | run: make stubtest 37 | - name: Build 38 | run: make build 39 | - name: Upload coverage 40 | run: make coverage 41 | - name: Coveralls Parallel 42 | uses: coverallsapp/github-action@v2 43 | with: 44 | flag-name: run-${{ join(matrix.*, '-') }} 45 | parallel: true 46 | 47 | finish: 48 | needs: build 49 | if: ${{ always() }} 50 | runs-on: ubuntu-latest 51 | steps: 52 | - name: Coveralls Finished 53 | uses: coverallsapp/github-action@v2 54 | with: 55 | parallel-finished: true 56 | carryforward: "run-3.8" 57 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | multiset 2 | ======== 3 | 4 | This package provides a multiset_ implementation for python. 5 | 6 | |pypi| |coverage| |build| |docs| 7 | 8 | Overview 9 | -------- 10 | 11 | A multiset is similar to the builtin set_, but it allows an element to occur multiple times. 12 | It is an unordered collection of elements which have to be hashable just like in a set_. 13 | It supports the same methods and operations as set_ does, e.g. membership test, union, intersection, and 14 | (symmetric) difference:: 15 | 16 | >>> set1 = Multiset('aab') 17 | >>> set2 = Multiset('abc') 18 | >>> sorted(set1 | set2) 19 | ['a', 'a', 'b', 'c'] 20 | 21 | Multisets can be used in combination with sets_:: 22 | 23 | >>> Multiset('aab') >= {'a', 'b'} 24 | True 25 | 26 | Multisets are mutable:: 27 | 28 | >>> set1.update('bc') 29 | >>> sorted(set1) 30 | ['a', 'a', 'b', 'b', 'c'] 31 | 32 | There is an immutable version similar to the frozenset_ which is also hashable:: 33 | 34 | >>> set1 = FrozenMultiset('abc') 35 | >>> set2 = FrozenMultiset('abc') 36 | >>> hash(set1) == hash(set2) 37 | True 38 | >>> set1 is set2 39 | False 40 | 41 | The implementation is based on a dict_ that maps the elements to their multiplicity in the multiset. 42 | Hence, some dictionary operations are supported. 43 | 44 | In contrast to the `collections.Counter`_ from the standard library, it has proper support for set 45 | operations and only allows positive counts. Also, elements with a zero multiplicity are automatically 46 | removed from the multiset. 47 | 48 | Installation 49 | ------------ 50 | 51 | Installing `multiset` is simple with `pip `_:: 52 | 53 | $ pip install multiset 54 | 55 | Documentation 56 | ------------- 57 | 58 | The documentation is available at `Read the Docs`_. 59 | 60 | .. _`Read the Docs`: http://multiset.readthedocs.io/ 61 | 62 | API Documentation 63 | ................. 64 | 65 | If you are looking for information on a particular method of the Multiset class, have a look at the 66 | `API Documentation`_. It is automatically generated from the docstrings. 67 | 68 | .. _`API Documentation`: http://multiset.readthedocs.io/en/latest/api.html 69 | 70 | License 71 | ------- 72 | 73 | Licensed under the MIT_ license. 74 | 75 | 76 | .. _multiset: https://en.wikipedia.org/wiki/Multiset 77 | .. _set: https://docs.python.org/3.10/library/stdtypes.html#set-types-set-frozenset 78 | .. _sets: set_ 79 | .. _frozenset: set_ 80 | .. _dict: https://docs.python.org/3.10/library/stdtypes.html#mapping-types-dict 81 | .. _`collections.Counter`: https://docs.python.org/3.10/library/collections.html#collections.Counter 82 | .. _MIT: https://opensource.org/licenses/MIT 83 | 84 | 85 | .. |pypi| image:: https://img.shields.io/pypi/v/multiset.svg?style=flat-square&label=latest%20stable%20version 86 | :target: https://pypi.python.org/pypi/multiset 87 | :alt: Latest version released on PyPi 88 | 89 | .. |coverage| image:: https://coveralls.io/repos/github/wheerd/multiset/badge.svg?branch=master 90 | :target: https://coveralls.io/github/wheerd/multiset?branch=master 91 | :alt: Test coverage 92 | 93 | .. |build| image:: https://github.com/wheerd/multiset/workflows/Tests/badge.svg?branch=master 94 | :target: https://github.com/wheerd/multiset/actions?query=workflow%3ATests 95 | :alt: Build status of the master branch 96 | 97 | .. |docs| image:: https://readthedocs.org/projects/multiset/badge/?version=latest 98 | :target: http://multiset.readthedocs.io/en/latest/?badge=latest 99 | :alt: Documentation Status 100 | -------------------------------------------------------------------------------- /multiset/__init__.pyi: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from typing import (Generic, ItemsView, Iterable, Iterator, KeysView, Mapping, Hashable, 3 | MutableMapping, Optional, Set, Type, TypeVar, Union, ValuesView, overload) 4 | 5 | from _typeshed import SupportsKeysAndGetItem 6 | 7 | _T = TypeVar('_T') 8 | _TElement = TypeVar('_TElement', bound=Hashable) 9 | _OtherType = Union[Iterable[_TElement], Mapping[_TElement, int]] 10 | _Self = TypeVar('_Self', bound='BaseMultiset') 11 | 12 | 13 | class BaseMultiset(Mapping[_TElement, int], Generic[_TElement]): 14 | def __init__(self, iterable: Optional[_OtherType] = None) -> None: ... 15 | def __new__(cls, iterable=None): ... 16 | def __contains__(self, element: object) -> bool: ... 17 | def __getitem__(self, element: _TElement) -> int: ... 18 | def __str__(self) -> str: ... 19 | def __repr__(self) -> str: ... 20 | def __len__(self) -> int: ... 21 | def __iter__(self) -> Iterator[_TElement]: ... 22 | def isdisjoint(self, other: _OtherType) -> bool: ... 23 | def difference(self: _Self, *others: _OtherType) -> _Self: ... 24 | def __bool__(self) -> bool: ... 25 | @overload 26 | def __sub__(self: _Self, other: Set[_TElement]) -> _Self: ... 27 | @overload 28 | def __sub__(self: _Self, other: 'BaseMultiset[_TElement]') -> _Self: ... 29 | @overload 30 | def __rsub__(self: _Self, other: Set[_TElement]) -> _Self: ... 31 | @overload 32 | def __rsub__(self: _Self, other: 'BaseMultiset[_TElement]') -> _Self: ... 33 | def union(self: _Self, *others: _OtherType) -> _Self: ... 34 | @overload 35 | def __or__(self: _Self, other: Set[_TElement]) -> _Self: ... 36 | @overload 37 | def __or__(self: _Self, other: 'BaseMultiset[_TElement]') -> _Self: ... 38 | __ror__ = __or__ 39 | def combine(self: _Self, *others: _OtherType) -> _Self: ... 40 | @overload 41 | def __add__(self: _Self, other: Set[_TElement]) -> _Self: ... 42 | @overload 43 | def __add__(self: _Self, other: 'BaseMultiset[_TElement]') -> _Self: ... 44 | __radd__ = __add__ 45 | def intersection(self: _Self, *others: _OtherType) -> _Self: ... 46 | @overload 47 | def __and__(self: _Self, other: Set[_TElement]) -> _Self: ... 48 | @overload 49 | def __and__(self: _Self, other: 'BaseMultiset[_TElement]') -> _Self: ... 50 | __rand__ = __and__ 51 | def symmetric_difference(self: _Self, other: _OtherType) -> _Self: ... 52 | @overload 53 | def __xor__(self: _Self, other: Set[_TElement]) -> _Self: ... 54 | @overload 55 | def __xor__(self: _Self, other: 'BaseMultiset[_TElement]') -> _Self: ... 56 | __rxor__ = __xor__ 57 | def times(self: _Self, factor: int) -> _Self: ... 58 | def __mul__(self: _Self, factor: int) -> _Self: ... 59 | __rmul__ = __mul__ 60 | def _issubset(self, other: _OtherType, strict: bool) -> bool: ... 61 | def issubset(self, other: _OtherType) -> bool: ... 62 | @overload 63 | def __le__(self, other: Set[_TElement]) -> bool: ... 64 | @overload 65 | def __le__(self, other: 'BaseMultiset[_TElement]') -> bool: ... 66 | @overload 67 | def __lt__(self, other: Set[_TElement]) -> bool: ... 68 | @overload 69 | def __lt__(self, other: 'BaseMultiset[_TElement]') -> bool: ... 70 | def _issuperset(self, other: _OtherType, strict: bool) -> bool: ... 71 | def issuperset(self, other: _OtherType) -> bool: ... 72 | @overload 73 | def __ge__(self, other: Set[_TElement]) -> bool: ... 74 | @overload 75 | def __ge__(self, other: 'BaseMultiset[_TElement]') -> bool: ... 76 | @overload 77 | def __gt__(self, other: Set[_TElement]) -> bool: ... 78 | @overload 79 | def __gt__(self, other: 'BaseMultiset[_TElement]') -> bool: ... 80 | def __eq__(self, other: object): ... 81 | def __ne__(self, other: object): ... 82 | def get(self, element: _TElement, default: int) -> int: ... # type: ignore 83 | @classmethod 84 | def from_elements(cls: Type[_Self], elements: Iterable[_TElement], multiplicity: int) -> _Self: ... 85 | def copy(self: _Self) -> _Self: ... 86 | def __copy__(self: _Self) -> _Self: ... 87 | def items(self) -> ItemsView[_TElement, int]: ... 88 | def distinct_elements(self) -> KeysView[_TElement]: ... 89 | def multiplicities(self) -> ValuesView[int]: ... 90 | 91 | 92 | class Multiset(BaseMultiset[_TElement], MutableMapping[_TElement, int], Generic[_TElement]): 93 | def __setitem__(self, element: _TElement, multiplicity: int) -> None: ... 94 | def __delitem__(self, element: _TElement) -> None: ... 95 | @overload 96 | def update(self, *others: _OtherType) -> None: ... 97 | @overload 98 | def update(self, __m: SupportsKeysAndGetItem[_T, int], /, **kwargs: int) -> None: ... 99 | @overload 100 | def update(self, __m: Iterable[tuple[_T, int]], /, **kwargs: int) -> None: ... 101 | @overload 102 | def update(self, **kwargs: int) -> None: ... 103 | def union_update(self, *others: _OtherType) -> None: ... 104 | @overload 105 | def __ior__(self: _Self, other: Set[_TElement]) -> _Self: ... 106 | @overload 107 | def __ior__(self: _Self, other: 'BaseMultiset[_TElement]') -> _Self: ... 108 | def intersection_update(self, *others: _OtherType) -> None: ... 109 | @overload 110 | def __iand__(self: _Self, other: Set[_TElement]) -> _Self: ... 111 | @overload 112 | def __iand__(self: _Self, other: 'BaseMultiset[_TElement]') -> _Self: ... 113 | def difference_update(self, *others: _OtherType) -> None: ... 114 | @overload 115 | def __isub__(self: _Self, other: Set[_TElement]) -> _Self: ... 116 | @overload 117 | def __isub__(self: _Self, other: 'BaseMultiset[_TElement]') -> _Self: ... 118 | def symmetric_difference_update(self, other: _OtherType) -> None: ... 119 | @overload 120 | def __ixor__(self: _Self, other: Set[_TElement]) -> _Self: ... 121 | @overload 122 | def __ixor__(self: _Self, other: 'BaseMultiset[_TElement]') -> _Self: ... 123 | def times_update(self, factor: int) -> None: ... 124 | def __imul__(self: _Self, factor: int) -> _Self: ... 125 | def add(self, element: _TElement, multiplicity: int = 1) -> None: ... 126 | def remove(self, element: _TElement, multiplicity: Optional[int] = None) -> int: ... 127 | def discard(self, element: _TElement, multiplicity: Optional[int] = None) -> int: ... 128 | def pop(self, element: _TElement, default: int) -> int: ... # type: ignore 129 | def setdefault(self, element: _TElement, default: int) -> int: ... # type: ignore 130 | def clear(self) -> None: ... 131 | 132 | 133 | class FrozenMultiset(BaseMultiset[_TElement], Generic[_TElement]): 134 | def __hash__(self): ... 135 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help 18 | help: 19 | @echo "Please use \`make ' where is one of" 20 | @echo " html to make standalone HTML files" 21 | @echo " dirhtml to make HTML files named index.html in directories" 22 | @echo " singlehtml to make a single large HTML file" 23 | @echo " pickle to make pickle files" 24 | @echo " json to make JSON files" 25 | @echo " htmlhelp to make HTML files and a HTML help project" 26 | @echo " qthelp to make HTML files and a qthelp project" 27 | @echo " applehelp to make an Apple Help Book" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " epub3 to make an epub3" 31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 32 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 33 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 34 | @echo " text to make text files" 35 | @echo " man to make manual pages" 36 | @echo " texinfo to make Texinfo files" 37 | @echo " info to make Texinfo files and run them through makeinfo" 38 | @echo " gettext to make PO message catalogs" 39 | @echo " changes to make an overview of all changed/added/deprecated items" 40 | @echo " xml to make Docutils-native XML files" 41 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 42 | @echo " linkcheck to check all external links for integrity" 43 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 44 | @echo " coverage to run coverage check of the documentation (if enabled)" 45 | @echo " dummy to check syntax errors of document sources" 46 | 47 | .PHONY: clean 48 | clean: 49 | rm -rf $(BUILDDIR)/* 50 | 51 | .PHONY: html 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | .PHONY: dirhtml 58 | dirhtml: 59 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 60 | @echo 61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 62 | 63 | .PHONY: singlehtml 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | .PHONY: pickle 70 | pickle: 71 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 72 | @echo 73 | @echo "Build finished; now you can process the pickle files." 74 | 75 | .PHONY: json 76 | json: 77 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 78 | @echo 79 | @echo "Build finished; now you can process the JSON files." 80 | 81 | .PHONY: htmlhelp 82 | htmlhelp: 83 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 84 | @echo 85 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 86 | ".hhp project file in $(BUILDDIR)/htmlhelp." 87 | 88 | .PHONY: qthelp 89 | qthelp: 90 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 91 | @echo 92 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 93 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 94 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/multiset.qhcp" 95 | @echo "To view the help file:" 96 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/multiset.qhc" 97 | 98 | .PHONY: applehelp 99 | applehelp: 100 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 101 | @echo 102 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 103 | @echo "N.B. You won't be able to view it unless you put it in" \ 104 | "~/Library/Documentation/Help or install it in your application" \ 105 | "bundle." 106 | 107 | .PHONY: devhelp 108 | devhelp: 109 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 110 | @echo 111 | @echo "Build finished." 112 | @echo "To view the help file:" 113 | @echo "# mkdir -p $$HOME/.local/share/devhelp/multiset" 114 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/multiset" 115 | @echo "# devhelp" 116 | 117 | .PHONY: epub 118 | epub: 119 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 120 | @echo 121 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 122 | 123 | .PHONY: epub3 124 | epub3: 125 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 126 | @echo 127 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 128 | 129 | .PHONY: latex 130 | latex: 131 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 132 | @echo 133 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 134 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 135 | "(use \`make latexpdf' here to do that automatically)." 136 | 137 | .PHONY: latexpdf 138 | latexpdf: 139 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 140 | @echo "Running LaTeX files through pdflatex..." 141 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 142 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 143 | 144 | .PHONY: latexpdfja 145 | latexpdfja: 146 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 147 | @echo "Running LaTeX files through platex and dvipdfmx..." 148 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 149 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 150 | 151 | .PHONY: text 152 | text: 153 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 154 | @echo 155 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 156 | 157 | .PHONY: man 158 | man: 159 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 160 | @echo 161 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 162 | 163 | .PHONY: texinfo 164 | texinfo: 165 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 166 | @echo 167 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 168 | @echo "Run \`make' in that directory to run these through makeinfo" \ 169 | "(use \`make info' here to do that automatically)." 170 | 171 | .PHONY: info 172 | info: 173 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 174 | @echo "Running Texinfo files through makeinfo..." 175 | make -C $(BUILDDIR)/texinfo info 176 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 177 | 178 | .PHONY: gettext 179 | gettext: 180 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 181 | @echo 182 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 183 | 184 | .PHONY: changes 185 | changes: 186 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 187 | @echo 188 | @echo "The overview file is in $(BUILDDIR)/changes." 189 | 190 | .PHONY: linkcheck 191 | linkcheck: 192 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 193 | @echo 194 | @echo "Link check complete; look for any errors in the above output " \ 195 | "or in $(BUILDDIR)/linkcheck/output.txt." 196 | 197 | .PHONY: doctest 198 | doctest: 199 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 200 | @echo "Testing of doctests in the sources finished, look at the " \ 201 | "results in $(BUILDDIR)/doctest/output.txt." 202 | 203 | .PHONY: coverage 204 | coverage: 205 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 206 | @echo "Testing of coverage in the sources finished, look at the " \ 207 | "results in $(BUILDDIR)/coverage/python.txt." 208 | 209 | .PHONY: xml 210 | xml: 211 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 212 | @echo 213 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 214 | 215 | .PHONY: pseudoxml 216 | pseudoxml: 217 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 218 | @echo 219 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 220 | 221 | .PHONY: dummy 222 | dummy: 223 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 224 | @echo 225 | @echo "Build finished. Dummy builder generates no files." 226 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. epub3 to make an epub3 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | echo. dummy to check syntax errors of document sources 43 | goto end 44 | ) 45 | 46 | if "%1" == "clean" ( 47 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 48 | del /q /s %BUILDDIR%\* 49 | goto end 50 | ) 51 | 52 | 53 | REM Check if sphinx-build is available and fallback to Python version if any 54 | %SPHINXBUILD% 1>NUL 2>NUL 55 | if errorlevel 9009 goto sphinx_python 56 | goto sphinx_ok 57 | 58 | :sphinx_python 59 | 60 | set SPHINXBUILD=python -m sphinx.__init__ 61 | %SPHINXBUILD% 2> nul 62 | if errorlevel 9009 ( 63 | echo. 64 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 65 | echo.installed, then set the SPHINXBUILD environment variable to point 66 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 67 | echo.may add the Sphinx directory to PATH. 68 | echo. 69 | echo.If you don't have Sphinx installed, grab it from 70 | echo.http://sphinx-doc.org/ 71 | exit /b 1 72 | ) 73 | 74 | :sphinx_ok 75 | 76 | 77 | if "%1" == "html" ( 78 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 79 | if errorlevel 1 exit /b 1 80 | echo. 81 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 82 | goto end 83 | ) 84 | 85 | if "%1" == "dirhtml" ( 86 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 87 | if errorlevel 1 exit /b 1 88 | echo. 89 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 90 | goto end 91 | ) 92 | 93 | if "%1" == "singlehtml" ( 94 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 95 | if errorlevel 1 exit /b 1 96 | echo. 97 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 98 | goto end 99 | ) 100 | 101 | if "%1" == "pickle" ( 102 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 103 | if errorlevel 1 exit /b 1 104 | echo. 105 | echo.Build finished; now you can process the pickle files. 106 | goto end 107 | ) 108 | 109 | if "%1" == "json" ( 110 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 111 | if errorlevel 1 exit /b 1 112 | echo. 113 | echo.Build finished; now you can process the JSON files. 114 | goto end 115 | ) 116 | 117 | if "%1" == "htmlhelp" ( 118 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 119 | if errorlevel 1 exit /b 1 120 | echo. 121 | echo.Build finished; now you can run HTML Help Workshop with the ^ 122 | .hhp project file in %BUILDDIR%/htmlhelp. 123 | goto end 124 | ) 125 | 126 | if "%1" == "qthelp" ( 127 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 128 | if errorlevel 1 exit /b 1 129 | echo. 130 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 131 | .qhcp project file in %BUILDDIR%/qthelp, like this: 132 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\multiset.qhcp 133 | echo.To view the help file: 134 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\multiset.ghc 135 | goto end 136 | ) 137 | 138 | if "%1" == "devhelp" ( 139 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 140 | if errorlevel 1 exit /b 1 141 | echo. 142 | echo.Build finished. 143 | goto end 144 | ) 145 | 146 | if "%1" == "epub" ( 147 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 148 | if errorlevel 1 exit /b 1 149 | echo. 150 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 151 | goto end 152 | ) 153 | 154 | if "%1" == "epub3" ( 155 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 156 | if errorlevel 1 exit /b 1 157 | echo. 158 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 159 | goto end 160 | ) 161 | 162 | if "%1" == "latex" ( 163 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 164 | if errorlevel 1 exit /b 1 165 | echo. 166 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdf" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "latexpdfja" ( 181 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 182 | cd %BUILDDIR%/latex 183 | make all-pdf-ja 184 | cd %~dp0 185 | echo. 186 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 187 | goto end 188 | ) 189 | 190 | if "%1" == "text" ( 191 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 192 | if errorlevel 1 exit /b 1 193 | echo. 194 | echo.Build finished. The text files are in %BUILDDIR%/text. 195 | goto end 196 | ) 197 | 198 | if "%1" == "man" ( 199 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 200 | if errorlevel 1 exit /b 1 201 | echo. 202 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 203 | goto end 204 | ) 205 | 206 | if "%1" == "texinfo" ( 207 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 208 | if errorlevel 1 exit /b 1 209 | echo. 210 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 211 | goto end 212 | ) 213 | 214 | if "%1" == "gettext" ( 215 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 216 | if errorlevel 1 exit /b 1 217 | echo. 218 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 219 | goto end 220 | ) 221 | 222 | if "%1" == "changes" ( 223 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 224 | if errorlevel 1 exit /b 1 225 | echo. 226 | echo.The overview file is in %BUILDDIR%/changes. 227 | goto end 228 | ) 229 | 230 | if "%1" == "linkcheck" ( 231 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 232 | if errorlevel 1 exit /b 1 233 | echo. 234 | echo.Link check complete; look for any errors in the above output ^ 235 | or in %BUILDDIR%/linkcheck/output.txt. 236 | goto end 237 | ) 238 | 239 | if "%1" == "doctest" ( 240 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 241 | if errorlevel 1 exit /b 1 242 | echo. 243 | echo.Testing of doctests in the sources finished, look at the ^ 244 | results in %BUILDDIR%/doctest/output.txt. 245 | goto end 246 | ) 247 | 248 | if "%1" == "coverage" ( 249 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 250 | if errorlevel 1 exit /b 1 251 | echo. 252 | echo.Testing of coverage in the sources finished, look at the ^ 253 | results in %BUILDDIR%/coverage/python.txt. 254 | goto end 255 | ) 256 | 257 | if "%1" == "xml" ( 258 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 259 | if errorlevel 1 exit /b 1 260 | echo. 261 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 262 | goto end 263 | ) 264 | 265 | if "%1" == "pseudoxml" ( 266 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 267 | if errorlevel 1 exit /b 1 268 | echo. 269 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 270 | goto end 271 | ) 272 | 273 | if "%1" == "dummy" ( 274 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 275 | if errorlevel 1 exit /b 1 276 | echo. 277 | echo.Build finished. Dummy builder generates no files. 278 | goto end 279 | ) 280 | 281 | :end 282 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # multiset documentation build configuration file, created by 5 | # sphinx-quickstart on Sat Oct 15 11:27:14 2016. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | import os 21 | import sys 22 | from setuptools_scm import get_version 23 | 24 | _doc_dir = os.path.dirname(__file__) 25 | _pkg_dir = os.path.abspath(os.path.join(_doc_dir, '..')) 26 | 27 | sys.path.insert(0, _pkg_dir) 28 | 29 | # -- General configuration ------------------------------------------------ 30 | 31 | # If your documentation needs a minimal Sphinx version, state it here. 32 | # 33 | # needs_sphinx = '1.0' 34 | 35 | # Add any Sphinx extension module names here, as strings. They can be 36 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 37 | # ones. 38 | extensions = [ 39 | 'sphinx.ext.autodoc', 40 | 'sphinx.ext.intersphinx', 41 | 'sphinx.ext.viewcode', 42 | 'sphinx.ext.githubpages', 43 | 'sphinx.ext.napoleon', 44 | 'sphinx_autodoc_typehints', 45 | 'alabaster', 46 | ] 47 | 48 | # Add any paths that contain templates here, relative to this directory. 49 | templates_path = ['_templates'] 50 | 51 | # The suffix(es) of source filenames. 52 | # You can specify multiple suffix as a list of string: 53 | # 54 | # source_suffix = ['.rst', '.md'] 55 | source_suffix = '.rst' 56 | 57 | # The encoding of source files. 58 | # 59 | # source_encoding = 'utf-8-sig' 60 | 61 | # The master toctree document. 62 | master_doc = 'index' 63 | 64 | # General information about the project. 65 | project = 'multiset' 66 | copyright = '2016, Manuel Krebber' 67 | author = 'Manuel Krebber' 68 | 69 | # The version info for the project you're documenting, acts as replacement for 70 | # |version| and |release|, also used in various other places throughout the 71 | # built documents. 72 | # 73 | # The short X.Y version. 74 | version = get_version(root='..', relative_to=__file__) 75 | # The full version, including alpha/beta/rc tags. 76 | release = version 77 | 78 | # The language for content autogenerated by Sphinx. Refer to documentation 79 | # for a list of supported languages. 80 | # 81 | # This is also used if you do content translation via gettext catalogs. 82 | # Usually you set "language" from the command line for these cases. 83 | language = None 84 | 85 | # There are two options for replacing |today|: either, you set today to some 86 | # non-false value, then it is used: 87 | # 88 | # today = '' 89 | # 90 | # Else, today_fmt is used as the format for a strftime call. 91 | # 92 | # today_fmt = '%B %d, %Y' 93 | 94 | # List of patterns, relative to source directory, that match files and 95 | # directories to ignore when looking for source files. 96 | # This patterns also effect to html_static_path and html_extra_path 97 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 98 | 99 | # The reST default role (used for this markup: `text`) to use for all 100 | # documents. 101 | # 102 | default_role = 'obj' 103 | 104 | # If true, '()' will be appended to :func: etc. cross-reference text. 105 | # 106 | # add_function_parentheses = True 107 | 108 | # If true, the current module name will be prepended to all description 109 | # unit titles (such as .. function::). 110 | # 111 | # add_module_names = True 112 | 113 | # If true, sectionauthor and moduleauthor directives will be shown in the 114 | # output. They are ignored by default. 115 | # 116 | # show_authors = False 117 | 118 | # The name of the Pygments (syntax highlighting) style to use. 119 | pygments_style = 'sphinx' 120 | 121 | # A list of ignored prefixes for module index sorting. 122 | # modindex_common_prefix = [] 123 | 124 | # If true, keep warnings as "system message" paragraphs in the built documents. 125 | # keep_warnings = False 126 | 127 | # If true, `todo` and `todoList` produce output, else they produce nothing. 128 | todo_include_todos = False 129 | 130 | 131 | # -- Options for HTML output ---------------------------------------------- 132 | 133 | # The theme to use for HTML and HTML Help pages. See the documentation for 134 | # a list of builtin themes. 135 | # 136 | html_theme = 'alabaster' 137 | 138 | # Theme options are theme-specific and customize the look and feel of a theme 139 | # further. For a list of options available for each theme, see the 140 | # documentation. 141 | # 142 | html_theme_options = { 143 | 'description': "A multiset implementation", 144 | 'github_user': 'wheerd', 145 | 'github_repo': 'multiset', 146 | 'fixed_sidebar': True, 147 | } 148 | 149 | # Add any paths that contain custom themes here, relative to this directory. 150 | # html_theme_path = [] 151 | 152 | # The name for this set of Sphinx documents. 153 | # " v documentation" by default. 154 | # 155 | # html_title = 'multiset v0.1' 156 | 157 | # A shorter title for the navigation bar. Default is the same as html_title. 158 | # 159 | # html_short_title = None 160 | 161 | # The name of an image file (relative to this directory) to place at the top 162 | # of the sidebar. 163 | # 164 | # html_logo = None 165 | 166 | # The name of an image file (relative to this directory) to use as a favicon of 167 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 168 | # pixels large. 169 | # 170 | # html_favicon = None 171 | 172 | # Add any paths that contain custom static files (such as style sheets) here, 173 | # relative to this directory. They are copied after the builtin static files, 174 | # so a file named "default.css" will overwrite the builtin "default.css". 175 | html_static_path = ['_static'] 176 | 177 | # Add any extra paths that contain custom files (such as robots.txt or 178 | # .htaccess) here, relative to this directory. These files are copied 179 | # directly to the root of the documentation. 180 | # 181 | # html_extra_path = [] 182 | 183 | # If not None, a 'Last updated on:' timestamp is inserted at every page 184 | # bottom, using the given strftime format. 185 | # The empty string is equivalent to '%b %d, %Y'. 186 | # 187 | # html_last_updated_fmt = None 188 | 189 | # If true, SmartyPants will be used to convert quotes and dashes to 190 | # typographically correct entities. 191 | # 192 | # html_use_smartypants = True 193 | 194 | # Custom sidebar templates, maps document names to template names. 195 | # 196 | html_sidebars = { 197 | '**': [ 198 | 'about.html', 199 | 'navigation.html', 200 | 'relations.html', 201 | 'searchbox.html', 202 | 'donate.html', 203 | ] 204 | } 205 | 206 | # Additional templates that should be rendered to pages, maps page names to 207 | # template names. 208 | # 209 | # html_additional_pages = {} 210 | 211 | # If false, no module index is generated. 212 | # 213 | # html_domain_indices = True 214 | 215 | # If false, no index is generated. 216 | # 217 | # html_use_index = True 218 | 219 | # If true, the index is split into individual pages for each letter. 220 | # 221 | # html_split_index = False 222 | 223 | # If true, links to the reST sources are added to the pages. 224 | # 225 | # html_show_sourcelink = True 226 | 227 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 228 | # 229 | # html_show_sphinx = True 230 | 231 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 232 | # 233 | # html_show_copyright = True 234 | 235 | # If true, an OpenSearch description file will be output, and all pages will 236 | # contain a tag referring to it. The value of this option must be the 237 | # base URL from which the finished HTML is served. 238 | # 239 | # html_use_opensearch = '' 240 | 241 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 242 | # html_file_suffix = None 243 | 244 | # Language to be used for generating the HTML full-text search index. 245 | # Sphinx supports the following languages: 246 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 247 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' 248 | # 249 | # html_search_language = 'en' 250 | 251 | # A dictionary with options for the search language support, empty by default. 252 | # 'ja' uses this config value. 253 | # 'zh' user can custom change `jieba` dictionary path. 254 | # 255 | # html_search_options = {'type': 'default'} 256 | 257 | # The name of a javascript file (relative to the configuration directory) that 258 | # implements a search results scorer. If empty, the default will be used. 259 | # 260 | # html_search_scorer = 'scorer.js' 261 | 262 | # Output file base name for HTML help builder. 263 | htmlhelp_basename = 'multisetdoc' 264 | 265 | # -- Options for LaTeX output --------------------------------------------- 266 | 267 | latex_elements = { 268 | # The paper size ('letterpaper' or 'a4paper'). 269 | # 270 | # 'papersize': 'letterpaper', 271 | 272 | # The font size ('10pt', '11pt' or '12pt'). 273 | # 274 | # 'pointsize': '10pt', 275 | 276 | # Additional stuff for the LaTeX preamble. 277 | # 278 | # 'preamble': '', 279 | 280 | # Latex figure (float) alignment 281 | # 282 | # 'figure_align': 'htbp', 283 | } 284 | 285 | # Grouping the document tree into LaTeX files. List of tuples 286 | # (source start file, target name, title, 287 | # author, documentclass [howto, manual, or own class]). 288 | latex_documents = [ 289 | (master_doc, 'multiset.tex', 'multiset Documentation', 290 | 'Manuel Krebber', 'manual'), 291 | ] 292 | 293 | # The name of an image file (relative to this directory) to place at the top of 294 | # the title page. 295 | # 296 | # latex_logo = None 297 | 298 | # For "manual" documents, if this is true, then toplevel headings are parts, 299 | # not chapters. 300 | # 301 | # latex_use_parts = False 302 | 303 | # If true, show page references after internal links. 304 | # 305 | # latex_show_pagerefs = False 306 | 307 | # If true, show URL addresses after external links. 308 | # 309 | # latex_show_urls = False 310 | 311 | # Documents to append as an appendix to all manuals. 312 | # 313 | # latex_appendices = [] 314 | 315 | # It false, will not define \strong, \code, itleref, \crossref ... but only 316 | # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added 317 | # packages. 318 | # 319 | # latex_keep_old_macro_names = True 320 | 321 | # If false, no module index is generated. 322 | # 323 | # latex_domain_indices = True 324 | 325 | 326 | # -- Options for manual page output --------------------------------------- 327 | 328 | # One entry per manual page. List of tuples 329 | # (source start file, name, description, authors, manual section). 330 | man_pages = [ 331 | (master_doc, 'multiset', 'multiset Documentation', 332 | [author], 1) 333 | ] 334 | 335 | # If true, show URL addresses after external links. 336 | # 337 | # man_show_urls = False 338 | 339 | 340 | # -- Options for Texinfo output ------------------------------------------- 341 | 342 | # Grouping the document tree into Texinfo files. List of tuples 343 | # (source start file, target name, title, author, 344 | # dir menu entry, description, category) 345 | texinfo_documents = [ 346 | (master_doc, 'multiset', 'multiset Documentation', 347 | author, 'multiset', 'One line description of project.', 348 | 'Miscellaneous'), 349 | ] 350 | 351 | # Documents to append as an appendix to all manuals. 352 | # 353 | # texinfo_appendices = [] 354 | 355 | # If false, no module index is generated. 356 | # 357 | # texinfo_domain_indices = True 358 | 359 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 360 | # 361 | # texinfo_show_urls = 'footnote' 362 | 363 | # If true, do not generate a @detailmenu in the "Top" node's menu. 364 | # 365 | # texinfo_no_detailmenu = False 366 | 367 | 368 | intersphinx_mapping = {'python': ('https://docs.python.org/3.10', None)} 369 | 370 | napoleon_use_param = True 371 | napoleon_use_ivar = False 372 | napoleon_use_rtype = False 373 | napoleon_google_docstring = True 374 | napoleon_numpy_docstring = False -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Add files or directories to the blacklist. They should be base names, not 11 | # paths. 12 | ignore=CVS 13 | 14 | # Add files or directories matching the regex patterns to the blacklist. The 15 | # regex matches against base names, not paths. 16 | ignore-patterns= 17 | 18 | # Pickle collected data for later comparisons. 19 | persistent=yes 20 | 21 | # List of plugins (as comma separated values of python modules names) to load, 22 | # usually to register additional checkers. 23 | load-plugins=pylint.extensions.check_docs 24 | 25 | # Use multiple processes to speed up Pylint. 26 | jobs=1 27 | 28 | # Allow loading of arbitrary C extensions. Extensions are imported into the 29 | # active Python interpreter and may run arbitrary code. 30 | unsafe-load-any-extension=no 31 | 32 | # A comma-separated list of package or module names from where C extensions may 33 | # be loaded. Extensions are loading into the active Python interpreter and may 34 | # run arbitrary code 35 | extension-pkg-whitelist= 36 | 37 | # Allow optimization of some AST trees. This will activate a peephole AST 38 | # optimizer, which will apply various small optimizations. For instance, it can 39 | # be used to obtain the result of joining multiple strings with the addition 40 | # operator. Joining a lot of strings can lead to a maximum recursion error in 41 | # Pylint and this flag can prevent that. It has one side effect, the resulting 42 | # AST will be different than the one from reality. This option is deprecated 43 | # and it will be removed in Pylint 2.0. 44 | optimize-ast=no 45 | 46 | 47 | [MESSAGES CONTROL] 48 | 49 | # Only show warnings with the listed confidence levels. Leave empty to show 50 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 51 | confidence= 52 | 53 | # Enable the message, report, category or checker with the given id(s). You can 54 | # either give multiple identifier separated by comma (,) or put this option 55 | # multiple time (only on the command line, not in the configuration file where 56 | # it should appear only once). See also the "--disable" option for examples. 57 | #enable= 58 | 59 | # Disable the message, report, category or checker with the given id(s). You 60 | # can either give multiple identifiers separated by comma (,) or put this 61 | # option multiple times (only on the command line, not in the configuration 62 | # file where it should appear only once).You can also use "--disable=all" to 63 | # disable everything first and then reenable specific checks. For example, if 64 | # you want to run only the similarities checker, you can use "--disable=all 65 | # --enable=similarities". If you want to run only the classes checker, but have 66 | # no Warning level messages displayed, use"--disable=all --enable=classes 67 | # --disable=W" 68 | disable=missing-type-doc,bad-continuation,invalid-name,missing-returns-doc,missing-param-doc,assigning-non-slot 69 | 70 | 71 | [REPORTS] 72 | 73 | # Set the output format. Available formats are text, parseable, colorized, msvs 74 | # (visual studio) and html. You can also give a reporter class, eg 75 | # mypackage.mymodule.MyReporterClass. 76 | output-format=text 77 | 78 | # Put messages in a separate file for each module / package specified on the 79 | # command line instead of printing them on stdout. Reports (if any) will be 80 | # written in a file name "pylint_global.[txt|html]". This option is deprecated 81 | # and it will be removed in Pylint 2.0. 82 | files-output=no 83 | 84 | # Tells whether to display a full report or only the messages 85 | reports=yes 86 | 87 | # Python expression which should return a note less than 10 (10 is the highest 88 | # note). You have access to the variables errors warning, statement which 89 | # respectively contain the number of errors / warnings messages and the total 90 | # number of statements analyzed. This is used by the global evaluation report 91 | # (RP0004). 92 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 93 | 94 | # Template used to display messages. This is a python new-style format string 95 | # used to format the message information. See doc for all details 96 | #msg-template= 97 | 98 | 99 | [BASIC] 100 | 101 | # Good variable names which should always be accepted, separated by a comma 102 | good-names=i,j,k,ex,Run,_a,b,c,x,y,z,v,_ 103 | 104 | # Bad variable names which should always be refused, separated by a comma 105 | bad-names=foo,bar,baz,toto,tutu,tata 106 | 107 | # Colon-delimited sets of names that determine each other's naming style when 108 | # the name regexes allow several styles. 109 | name-group= 110 | 111 | # Include a hint for the correct naming format with invalid-name 112 | include-naming-hint=no 113 | 114 | # List of decorators that produce properties, such as abc.abstractproperty. Add 115 | # to this list to register other decorators that produce valid properties. 116 | property-classes=abc.abstractproperty 117 | 118 | # Regular expression matching correct constant names 119 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 120 | 121 | # Naming hint for constant names 122 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 123 | 124 | # Regular expression matching correct method names 125 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 126 | 127 | # Naming hint for method names 128 | method-name-hint=[a-z_][a-z0-9_]{2,30}$ 129 | 130 | # Regular expression matching correct class attribute names 131 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 132 | 133 | # Naming hint for class attribute names 134 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 135 | 136 | # Regular expression matching correct inline iteration names 137 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 138 | 139 | # Naming hint for inline iteration names 140 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 141 | 142 | # Regular expression matching correct function names 143 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 144 | 145 | # Naming hint for function names 146 | function-name-hint=[a-z_][a-z0-9_]{2,30}$ 147 | 148 | # Regular expression matching correct attribute names 149 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 150 | 151 | # Naming hint for attribute names 152 | attr-name-hint=[a-z_][a-z0-9_]{2,30}$ 153 | 154 | # Regular expression matching correct class names 155 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 156 | 157 | # Naming hint for class names 158 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 159 | 160 | # Regular expression matching correct variable names 161 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 162 | 163 | # Naming hint for variable names 164 | variable-name-hint=[a-z_][a-z0-9_]{2,30}$ 165 | 166 | # Regular expression matching correct module names 167 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 168 | 169 | # Naming hint for module names 170 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 171 | 172 | # Regular expression matching correct argument names 173 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 174 | 175 | # Naming hint for argument names 176 | argument-name-hint=[a-z_][a-z0-9_]{2,30}$ 177 | 178 | # Regular expression which should only match function or class names that do 179 | # not require a docstring. 180 | no-docstring-rgx=^_ 181 | 182 | # Minimum line length for functions/classes that require docstrings, shorter 183 | # ones are exempt. 184 | docstring-min-length=-1 185 | 186 | 187 | [ELIF] 188 | 189 | # Maximum number of nested blocks for function / method body 190 | max-nested-blocks=5 191 | 192 | 193 | [FORMAT] 194 | 195 | # Maximum number of characters on a single line. 196 | max-line-length=120 197 | 198 | # Regexp for a line that is allowed to be longer than the limit. 199 | ignore-long-lines=^\s*(# )??$ 200 | 201 | # Allow the body of an if to be on the same line as the test if there is no 202 | # else. 203 | single-line-if-stmt=no 204 | 205 | # List of optional constructs for which whitespace checking is disabled. `dict- 206 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 207 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 208 | # `empty-line` allows space-only lines. 209 | no-space-check=trailing-comma,dict-separator 210 | 211 | # Maximum number of lines in a module 212 | max-module-lines=1000 213 | 214 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 215 | # tab). 216 | indent-string=' ' 217 | 218 | # Number of spaces of indent required inside a hanging or continued line. 219 | indent-after-paren=4 220 | 221 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 222 | expected-line-ending-format= 223 | 224 | 225 | [LOGGING] 226 | 227 | # Logging modules to check that the string format arguments are in logging 228 | # function parameter format 229 | logging-modules=logging 230 | 231 | 232 | [MISCELLANEOUS] 233 | 234 | # List of note tags to take in consideration, separated by a comma. 235 | notes=FIXME,XXX,TODO 236 | 237 | 238 | [SIMILARITIES] 239 | 240 | # Minimum lines number of a similarity. 241 | min-similarity-lines=4 242 | 243 | # Ignore comments when computing similarities. 244 | ignore-comments=yes 245 | 246 | # Ignore docstrings when computing similarities. 247 | ignore-docstrings=yes 248 | 249 | # Ignore imports when computing similarities. 250 | ignore-imports=no 251 | 252 | 253 | [SPELLING] 254 | 255 | # Spelling dictionary name. Available dictionaries: none. To make it working 256 | # install python-enchant package. 257 | spelling-dict= 258 | 259 | # List of comma separated words that should not be checked. 260 | spelling-ignore-words= 261 | 262 | # A path to a file that contains private dictionary; one word per line. 263 | spelling-private-dict-file= 264 | 265 | # Tells whether to store unknown words to indicated private dictionary in 266 | # --spelling-private-dict-file option instead of raising a message. 267 | spelling-store-unknown-words=no 268 | 269 | 270 | [TYPECHECK] 271 | 272 | # Tells whether missing members accessed in mixin class should be ignored. A 273 | # mixin class is detected if its name ends with "mixin" (case insensitive). 274 | ignore-mixin-members=yes 275 | 276 | # List of module names for which member attributes should not be checked 277 | # (useful for modules/projects where namespaces are manipulated during runtime 278 | # and thus existing member attributes cannot be deduced by static analysis. It 279 | # supports qualified module names, as well as Unix pattern matching. 280 | ignored-modules= 281 | 282 | # List of class names for which member attributes should not be checked (useful 283 | # for classes with dynamically set attributes). This supports the use of 284 | # qualified names. 285 | ignored-classes=optparse.Values,thread._local,_thread._local 286 | 287 | # List of members which are set dynamically and missed by pylint inference 288 | # system, and so shouldn't trigger E1101 when accessed. Python regular 289 | # expressions are accepted. 290 | generated-members= 291 | 292 | # List of decorators that produce context managers, such as 293 | # contextlib.contextmanager. Add to this list to register other decorators that 294 | # produce valid context managers. 295 | contextmanager-decorators=contextlib.contextmanager 296 | 297 | 298 | [VARIABLES] 299 | 300 | # Tells whether we should check for unused import in __init__ files. 301 | init-import=no 302 | 303 | # A regular expression matching the name of dummy variables (i.e. expectedly 304 | # not used). 305 | dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy 306 | 307 | # List of additional names supposed to be defined in builtins. Remember that 308 | # you should avoid to define new builtins when possible. 309 | additional-builtins= 310 | 311 | # List of strings which can identify a callback function by name. A callback 312 | # name must start or end with one of those strings. 313 | callbacks=cb_,_cb 314 | 315 | # List of qualified module names which can have objects that can redefine 316 | # builtins. 317 | redefining-builtins-modules=six.moves,future.builtins 318 | 319 | 320 | [CLASSES] 321 | 322 | # List of method names used to declare (i.e. assign) instance attributes. 323 | defining-attr-methods=__init__,__new__,setUp 324 | 325 | # List of valid names for the first argument in a class method. 326 | valid-classmethod-first-arg=cls 327 | 328 | # List of valid names for the first argument in a metaclass class method. 329 | valid-metaclass-classmethod-first-arg=mcs 330 | 331 | # List of member names, which should be excluded from the protected access 332 | # warning. 333 | exclude-protected=_asdict,_fields,_replace,_source,_make 334 | 335 | 336 | [DESIGN] 337 | 338 | # Maximum number of arguments for function / method 339 | max-args=5 340 | 341 | # Argument names that match this expression will be ignored. Default to name 342 | # with leading underscore 343 | ignored-argument-names=_.* 344 | 345 | # Maximum number of locals for function / method body 346 | max-locals=15 347 | 348 | # Maximum number of return / yield for function / method body 349 | max-returns=6 350 | 351 | # Maximum number of branch for function / method body 352 | max-branches=12 353 | 354 | # Maximum number of statements in function / method body 355 | max-statements=50 356 | 357 | # Maximum number of parents for a class (see R0901). 358 | max-parents=7 359 | 360 | # Maximum number of attributes for a class (see R0902). 361 | max-attributes=7 362 | 363 | # Minimum number of public methods for a class (see R0903). 364 | min-public-methods=2 365 | 366 | # Maximum number of public methods for a class (see R0904). 367 | max-public-methods=20 368 | 369 | # Maximum number of boolean expressions in a if statement 370 | max-bool-expr=5 371 | 372 | 373 | [IMPORTS] 374 | 375 | # Deprecated modules which should not be used, separated by a comma 376 | deprecated-modules=optparse 377 | 378 | # Create a graph of every (i.e. internal and external) dependencies in the 379 | # given file (report RP0402 must not be disabled) 380 | import-graph= 381 | 382 | # Create a graph of external dependencies in the given file (report RP0402 must 383 | # not be disabled) 384 | ext-import-graph= 385 | 386 | # Create a graph of internal dependencies in the given file (report RP0402 must 387 | # not be disabled) 388 | int-import-graph= 389 | 390 | # Force import order to recognize a module as part of the standard 391 | # compatibility libraries. 392 | known-standard-library= 393 | 394 | # Force import order to recognize a module as part of a third party library. 395 | known-third-party=enchant 396 | 397 | # Analyse import fallback blocks. This can be used to support both Python 2 and 398 | # 3 compatible code, which means that the block might have code that exists 399 | # only in one or another interpreter, leading to false positives when analysed. 400 | analyse-fallback-blocks=no 401 | 402 | 403 | [EXCEPTIONS] 404 | 405 | # Exceptions that will emit a warning when being caught. Defaults to 406 | # "Exception" 407 | overgeneral-exceptions=Exception 408 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | multiset 3 | ======== 4 | 5 | .. toctree:: 6 | :hidden: 7 | :titlesonly: 8 | 9 | Overview 10 | api 11 | 12 | This package provides a multiset_ implementation for Python. 13 | 14 | A multiset is similar to the builtin :class:`set`, but it allows an element to occur multiple times. 15 | It is an unordered collection of element which have to be :term:`python:hashable` just like in a :class:`set`. 16 | It supports the same :ref:`methods and operations ` as :class:`set` does, e.g. membership test, 17 | :meth:`union `, :meth:`intersection `, and 18 | (:meth:`symmetric `) :meth:`difference `. Multisets can be used in 19 | combination with regular sets for those operations. 20 | 21 | The implementation is based on a :class:`dict` that maps the elements to their multiplicity in the multiset. 22 | Hence, some :ref:`dictionary operations ` are supported. 23 | 24 | In contrast to the :class:`collections.Counter` from the standard library, it has proper support for set 25 | operations and only allows positive counts. Also, elements with a zero multiplicity are automatically 26 | removed from the multiset. 27 | 28 | There is an immutable version of the multiset called :class:`FrozenMultiset`. 29 | 30 | The package also uses the :mod:`typing` module for type hints (see :pep:`484`) so you can specify the type of a 31 | multiset like ``Multiset[ElementType]``. 32 | 33 | API Overview 34 | ------------ 35 | 36 | The following is an overview over the methods of the :class:`Multiset` class. For more details on each method and some 37 | examples see the autogenerated :doc:`api`. 38 | 39 | .. class:: Multiset([mapping or iterable]) 40 | 41 | Return a new multiset object whose elements are taken from 42 | the given optional iterable or mapping. If a mapping is given, it must have positive `int` 43 | values representing each element's multiplicity. If no iterable or mapping is specified, a new empty multiset is 44 | returned. 45 | 46 | The elements of a set must be :term:`python:hashable`. In contrast to regular sets, duplicate elements 47 | will not be removed from the multiset. 48 | 49 | .. _SetMethods: 50 | 51 | The :class:`Multiset` class provides the following operations which the builtin :class:`set` also supports: 52 | 53 | .. describe:: len(s) 54 | 55 | Return the total number of elements in multiset *s* (cardinality of *s*). 56 | 57 | Note that this is the sum of the multiplicities and not not the number of distinct elements. 58 | You can use :meth:`distinct_elements` to get the number of distinct elements:: 59 | 60 | >>> len(Multiset('aab').distinct_elements()) 61 | 2 62 | 63 | .. describe:: x in s 64 | 65 | Test *x* for membership in *s*. 66 | 67 | .. describe:: x not in s 68 | 69 | Test *x* for non-membership in *s*. 70 | 71 | .. method:: isdisjoint(other) 72 | 73 | Return ``True`` if the multiset has no elements in common with *other*. 74 | 75 | .. method:: issubset(other) 76 | multiset <= other 77 | 78 | Test whether every element in the multiset is in *other* and each element's multiplicity in the 79 | multiset is less than or equal to its multiplicity in *other*. 80 | 81 | .. method:: multiset < other 82 | 83 | Test whether the multiset is a proper subset of *other*, that is, 84 | ``multiset <= other and multiset != other``. 85 | 86 | .. method:: issuperset(other) 87 | multiset >= other 88 | 89 | Test whether every element in *other* is in the multiset and each element's multiplicity in *other* 90 | is less than or equal to its multiplicity in the multiset. 91 | 92 | .. method:: multiset > other 93 | 94 | Test whether the multiset is a proper superset of *other*, that is, ``multiset >= 95 | other and multiset != other``. 96 | 97 | .. method:: union(*others) 98 | multiset | other | ... 99 | 100 | Return a new multiset with elements from the multiset and all others. The maximal multiplicity over all 101 | sets is used for each element. 102 | 103 | .. method:: combine(*others) 104 | multiset + other + ... 105 | 106 | Return a new multiset with elements from the multiset and all others. Each element's multiplicities are summed 107 | up for the new set. 108 | 109 | .. method:: intersection(*others) 110 | multiset & other & ... 111 | 112 | Return a new multiset with elements common to the multiset and all others. The minimal multiplicity over all 113 | sets is used for each element. 114 | 115 | .. method:: difference(*others) 116 | multiset - other - ... 117 | 118 | Return a new multiset with elements in the multiset that are not in the others. This will subtract all the *others'* 119 | multiplicities and remove the element from the multiset if its multiplicity reaches zero. 120 | 121 | .. method:: symmetric_difference(other) 122 | multiset ^ other 123 | 124 | Return a new multiset with elements from either the multiset or *other* where their multiplicity is the absolute 125 | difference of the two multiplicities. 126 | 127 | .. method:: copy() 128 | Multiset(multiset) 129 | 130 | Return a new shallow copy of the multiset. 131 | 132 | The following methods are not supported by :class:`FrozenMultiset`, as it is equivalent to the builtin 133 | :class:`frozenset` class and is immutable: 134 | 135 | .. method:: union_update(*others) 136 | multiset |= other | ... 137 | 138 | Update the multiset, adding elements from all others. The maximal multiplicity over all 139 | sets is used for each element. 140 | For a version of this method that works more closely to :meth:`dict.update`, see :meth:`update`. 141 | 142 | .. method:: intersection_update(*others) 143 | multiset &= other & ... 144 | 145 | Update the multiset, keeping only elements found in it and all others. The minimal multiplicity over all 146 | sets is used for each element. 147 | 148 | .. method:: difference_update(*others) 149 | multiset -= other | ... 150 | 151 | Update the multiset, removing elements found in others. This will subtract all the *others'* 152 | multiplicities and remove the element from the multiset if its multiplicity reaches zero. 153 | 154 | .. method:: symmetric_difference_update(other) 155 | multiset ^= other 156 | 157 | Update the multiset, keeping only elements found in either the multiset or *other* but not both. 158 | The new multiplicity is the absolute difference of the two original multiplicities. 159 | 160 | .. method:: add(element, multiplicity=1) 161 | s[element] += multiplicity 162 | s[element] = multiplicity 163 | 164 | Add element *element* to the multiset *s*. If the optional multiplicity is specified, more than one 165 | element can be added at the same time by adding to its. 166 | 167 | Note that adding the same element multiple times will increase its multiplicity and thus change the multiset, 168 | whereas for a regular :class:`set` this would have no effect. 169 | 170 | You can also set the element's multiplicity directly via key assignment. 171 | 172 | .. method:: remove(element, multiplicity=None) 173 | del s[element] 174 | 175 | Remove all elements *element* from the multiset. Raises :exc:`KeyError` if *element* is 176 | not contained in the set. 177 | 178 | If the optional multiplicity is specified, only the given 179 | number is subtracted from the element's multiplicity. This might still completely remove 180 | the element from the multiset depending on its original multiplicity. 181 | 182 | This method returns the original multiplicity of the element before it was removed. 183 | 184 | You can also delete the element directly via key access. 185 | 186 | .. method:: discard(element, multiplicity=None) 187 | s[element] -= multiplicity 188 | s[element] = 0 189 | 190 | Remove element *element* from the set if it is present. If the optional multiplicity is specified, only the given 191 | number is subtracted from the element's multiplicity. This might still completely remove 192 | the element from the multiset depending on its original multiplicity. 193 | 194 | This method returns the original multiplicity of the element before it was removed. 195 | 196 | You can also set the element's multiplicity directly via key assignment. 197 | 198 | .. method:: clear() 199 | 200 | Remove all elements from the multiset. 201 | 202 | Note, the non-operator versions of :meth:`union`, :meth:`intersection`, 203 | :meth:`difference`, and :meth:`symmetric_difference`, :meth:`issubset`, and 204 | :meth:`issuperset`, :meth:`update`, 205 | :meth:`intersection_update`, :meth:`difference_update`, and 206 | :meth:`symmetric_difference_update` methods will accept any iterable as an argument. In 207 | contrast, their operator based counterparts require their arguments to be 208 | sets. This precludes error-prone constructions like ``Multiset('abc') & 'cbs'`` 209 | in favor of the more readable ``Multiset('abc').intersection('cbs')``. 210 | 211 | The :class:`Multiset` supports set to set comparisons. Two 212 | multisets are equal if and only if every element of each multiset is contained in the 213 | other and each element's multiplicity is the same in both multisets (each is a subset 214 | of the other). A multiset is less than another set if and 215 | only if it is a proper subset of the second set (is a subset, but 216 | is not equal). A multiset is greater than another set if and only if it 217 | is a proper superset of the second set (is a superset, but is not equal). 218 | These comparisons work with both sets and multisets:: 219 | 220 | >>> Multiset('ab') == {'a', 'b'} 221 | True 222 | 223 | Multiset elements, like set elements and dictionary keys, must be :term:`hashable`. 224 | 225 | Binary operations that mix :class:`set` or :class:`frozenset` instances with 226 | :class:`Multiset` instances will always return a :class:`Multiset`. 227 | 228 | .. _DictMethods: 229 | 230 | Since the :class:`Multiset` internally uses a :class:`dict`, it exposes some of its methods 231 | and allows key-based access for elements: 232 | 233 | .. describe:: s[element] 234 | 235 | Return the multiplicity of *element* in *s*. Returns ``0`` for elements that are not 236 | in the multiset. 237 | 238 | .. describe:: s[element] = value 239 | 240 | Set the multiplicity of *element* to *value*. Setting the multiplicity to ``0`` removes the element. 241 | This is not supported by :class:`FrozenMultiset`. 242 | 243 | .. describe:: del s[element] 244 | 245 | See :meth:`remove`. This is not supported by :class:`FrozenMultiset`. 246 | 247 | .. describe:: iter(s) 248 | 249 | Return an iterator over the elements in the multiset. 250 | 251 | In contrast to both the :class:`dict` and :class:`set` implementations, this will repeat elements 252 | whose multiplicity is greater than ``1``:: 253 | 254 | >>> sorted(Multiset('aab')) 255 | ['a', 'a', 'b'] 256 | 257 | To only get distinct elements, use the :meth:`distinct_elements` method. 258 | 259 | .. classmethod:: from_elements(elements, multiplicity) 260 | 261 | Create a new multiset with elements from *elements* and all multiplicities set to *multiplicity*. 262 | 263 | .. method:: get(element, default) 264 | 265 | Return the multiplicity for *element* if it is in the multiset, else *default*. 266 | 267 | .. method:: items() 268 | 269 | Return a new view of the multiset's items (``(element, multiplicity)`` pairs). 270 | See the :ref:`documentation of dict view objects `. 271 | 272 | Note that this view is unordered. 273 | 274 | .. method:: distinct_elements() 275 | keys() 276 | 277 | Return a new view of the multiset's distinct elements. See the :ref:`documentation 278 | of dict view objects `. 279 | 280 | Note that this view is unordered. 281 | 282 | .. method:: update(*others) 283 | multiset += other + ... 284 | 285 | Update the multiset, adding elements from all others. Each element's multiplicities is summed up for the new multiset. 286 | This is not supported by :class:`FrozenMultiset`. 287 | 288 | For a version of this method that works more closely to the original :meth:`set.update`, see :meth:`union_update`. 289 | 290 | .. method:: pop(element, default) 291 | 292 | If *element* is in the multiset, remove it and return its multiplicity, else return 293 | *default*. 294 | This is not supported by :class:`FrozenMultiset`. 295 | 296 | .. method:: popitem() 297 | 298 | Remove and return an arbitrary ``(element, multiplicity)`` pair from the multiset. 299 | This is not supported by :class:`FrozenMultiset`. 300 | 301 | :meth:`popitem` is useful to destructively iterate over a multiset. 302 | If the multiset is empty, calling :meth:`popitem` raises a :exc:`KeyError`. 303 | 304 | .. method:: setdefault(element, default) 305 | 306 | If *element* is in the multiset, return its multiplicity. If not, insert *element* 307 | with a multiplicity of *default* and return *default*. 308 | This is not supported by :class:`FrozenMultiset`. 309 | 310 | .. method:: multiplicities() 311 | values() 312 | 313 | Return a new view of the multiset's multiplicities. See the 314 | :ref:`documentation of dict view objects `. 315 | 316 | Note that this view is unordered. 317 | 318 | The multiset also adds some new methods for multiplying a multiset with an :class:`int` factor: 319 | 320 | .. method:: times(factor) 321 | multiset * factor 322 | 323 | Return a copy of the multiset where each multiplicity is multiplied by *factor*. 324 | 325 | .. method:: times_update(factor) 326 | multiset *= factor 327 | 328 | Update the multiset, multiplying each multiplicity with *factor*. 329 | This is not supported by :class:`FrozenMultiset`. 330 | 331 | .. class:: FrozenMultiset([mapping or iterable]) 332 | 333 | This is an immutable version of the :class:`Multiset` and supports all non-mutating methods of it. 334 | 335 | Because it is immutable, it is also :term:`hashable` and can be used e.g. as a dictionary key or in a set. 336 | 337 | .. class:: BaseMultiset 338 | 339 | This is the base class of both :class:`Multiset` and :class:`FrozenMultiset`. While it cannot instantiated directly, 340 | it can be used for isinstance checks: 341 | 342 | >>> isinstance(Multiset(), BaseMultiset) 343 | True 344 | >>> isinstance(FrozenMultiset(), BaseMultiset) 345 | True 346 | 347 | 348 | .. _multiset: https://en.wikipedia.org/wiki/Multiset 349 | 350 | -------------------------------------------------------------------------------- /tests/test_multiset.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import itertools 3 | import pickle 4 | import pytest 5 | import sys 6 | try: 7 | from collections.abc import Iterable, Mapping, MutableMapping, Sized, Container 8 | except ImportError: 9 | from collections import Iterable, Mapping, MutableMapping, Sized, Container 10 | 11 | import multiset 12 | from multiset import Multiset, FrozenMultiset, BaseMultiset 13 | 14 | 15 | def test_missing(): 16 | m = Multiset() 17 | assert m[object()] == 0 18 | 19 | @pytest.mark.parametrize('iterable', 20 | [ 21 | 'abc', 22 | 'babccaad', 23 | range(100), 24 | itertools.chain.from_iterable(itertools.repeat(n, n) for n in range(1, 10)), 25 | ] 26 | ) # yapf: disable 27 | def test_iter(MultisetCls, iterable): 28 | iter1, iter2 = itertools.tee(iterable, 2) 29 | m = MultisetCls(iter1) 30 | expected = sorted(iter2) 31 | assert sorted(m) == expected 32 | assert len(m) == len(expected) 33 | 34 | 35 | def test_setitem(): 36 | m = Multiset() 37 | assert len(m) == 0 38 | 39 | with pytest.raises(TypeError): 40 | m[1] = 'a' 41 | 42 | m[1] = 2 43 | assert m[1] == 2 44 | assert 1 in m 45 | assert len(m) == 2 46 | 47 | m[1] = 0 48 | assert m[1] == 0 49 | assert 1 not in m 50 | assert len(m) == 0 51 | 52 | assert 2 not in m 53 | m[2] = 0 54 | assert m[2] == 0 55 | assert 2 not in m 56 | assert len(m) == 0 57 | 58 | m[3] = -1 59 | assert m[3] == 0 60 | assert 3 not in m 61 | assert len(m) == 0 62 | 63 | 64 | def test_len(MultisetCls): 65 | m = MultisetCls() 66 | assert len(m) == 0 67 | 68 | m = MultisetCls('abc') 69 | assert len(m) == 3 70 | 71 | m = MultisetCls('aa') 72 | assert len(m) == 2 73 | 74 | 75 | def test_bool(MultisetCls): 76 | assert bool(MultisetCls()) is False 77 | assert bool(MultisetCls('a')) is True 78 | 79 | 80 | @pytest.mark.parametrize( 81 | ' initial, add, result', 82 | [ 83 | ('aab', ['abc'], list('aaabbc')), 84 | ('aab', [''], list('aab')), 85 | ('aab', [{'a': 2, 'b': 1}], list('aaaabb')), 86 | ('aab', [{}], list('aab')), 87 | ('aab', [{'c': 0}], list('aab')), 88 | ('a', [Multiset('a')], list('aa')), 89 | ('ab', [Multiset()], list('ab')), 90 | ('ab', [], list('ab')), 91 | ('ab', ['a', 'bc'], list('aabbc')), 92 | ('ab', [{}, ''], list('ab')), 93 | ('ab', [{'c': 1}, {'d': 0}], list('abc')), 94 | ] 95 | ) # yapf: disable 96 | def test_update(initial, add, result): 97 | ms = Multiset(initial) 98 | ms.update(*add) 99 | assert sorted(ms) == result 100 | assert len(ms) == len(result) 101 | 102 | 103 | @pytest.mark.parametrize( 104 | ' initial, add, result', 105 | [ 106 | ('aab', ['abc'], list('aabc')), 107 | ('aab', [''], list('aab')), 108 | ('aab', [{'a': 2, 'b': 1}], list('aab')), 109 | ('aab', [{}], list('aab')), 110 | ('aab', [{'c': 0}], list('aab')), 111 | ('a', [Multiset('a')], list('a')), 112 | ('ab', [Multiset()], list('ab')), 113 | ('ab', [], list('ab')), 114 | ('ab', ['a', 'bc'], list('abc')), 115 | ('ab', [{}, ''], list('ab')), 116 | ('ab', [{'c': 1}, {'d': 0}], list('abc')), 117 | ('ab', ['aa'], list('aab')), 118 | ] 119 | ) # yapf: disable 120 | def test_union_update(initial, add, result): 121 | ms = Multiset(initial) 122 | ms.union_update(*add) 123 | assert sorted(ms) == result 124 | assert len(ms) == len(result) 125 | 126 | 127 | def test_union_update_error(): 128 | with pytest.raises(TypeError): 129 | Multiset().union_update(None) 130 | 131 | 132 | def test_ior(): 133 | m = Multiset('ab') 134 | 135 | with pytest.raises(TypeError): 136 | m |= 'abc' 137 | 138 | m |= Multiset('abc') 139 | assert sorted(m) == list('abc') 140 | 141 | 142 | @pytest.mark.parametrize( 143 | ' initial, args, result', 144 | [ 145 | ('aab', ['abc'], list('ab')), 146 | ('aab', [''], list()), 147 | ('aab', [{'a': 2, 'b': 1}], list('aab')), 148 | ('aab', [{}], list()), 149 | ('aab', [{'c': 0}], list()), 150 | ('a', [Multiset('a')], list('a')), 151 | ('ab', [Multiset()], list()), 152 | ('ab', [], list('ab')), 153 | ('ab', ['a', 'bc'], list()), 154 | ('ab', ['a', 'aab'], list('a')), 155 | ('ab', [{}, ''], list()), 156 | ('ab', [{'c': 1}, {'d': 0}], list()), 157 | ('ab', ['aa'], list('a')), 158 | ] 159 | ) # yapf: disable 160 | def test_intersection_update(initial, args, result): 161 | ms = Multiset(initial) 162 | ms.intersection_update(*args) 163 | assert sorted(ms) == result 164 | assert len(ms) == len(result) 165 | 166 | 167 | def test_iand(): 168 | m = Multiset('aabd') 169 | 170 | with pytest.raises(TypeError): 171 | m &= 'abc' 172 | 173 | m &= Multiset('abc') 174 | assert sorted(m) == list('ab') 175 | 176 | 177 | @pytest.mark.parametrize( 178 | ' initial, other, result', 179 | [ 180 | ('aab', 'bc', list('aa')), 181 | ('aab', 'cd', list('aab')), 182 | ('aab', 'a', list('ab')), 183 | ('aab', 'aa', list('b')), 184 | ('aab', '', list('aab')), 185 | ('aab', {'a': 2, 'b': 1}, list()), 186 | ('aab', {}, list('aab')), 187 | ('aab', {'c': 0}, list('aab')), 188 | ('a', Multiset('a'), list()), 189 | ('ab', Multiset(), list('ab')), 190 | ('ab', 'aa', list('b')), 191 | ] 192 | ) # yapf: disable 193 | def test_difference_update(initial, other, result): 194 | ms = Multiset(initial) 195 | ms.difference_update(other) 196 | assert sorted(ms) == result 197 | assert len(ms) == len(result) 198 | 199 | 200 | def test_isub(): 201 | m = Multiset('aabd') 202 | 203 | with pytest.raises(TypeError): 204 | m -= 'abc' 205 | 206 | m -= Multiset('abc') 207 | assert sorted(m) == list('ad') 208 | 209 | 210 | @pytest.mark.parametrize( 211 | ' initial, other, result', 212 | [ 213 | ('aab', 'bc', list('aac')), 214 | ('aab', 'cd', list('aabcd')), 215 | ('aab', 'a', list('ab')), 216 | ('aab', 'aa', list('b')), 217 | ('aab', '', list('aab')), 218 | ('aab', {'a': 2, 'b': 1}, list()), 219 | ('aab', {}, list('aab')), 220 | ('aab', {'c': 0}, list('aab')), 221 | ('a', Multiset('a'), list()), 222 | ('ab', Multiset(), list('ab')), 223 | ('ab', 'aa', list('ab')), 224 | ] 225 | ) # yapf: disable 226 | def test_symmetric_difference_update(initial, other, result): 227 | ms = Multiset(initial) 228 | ms.symmetric_difference_update(other) 229 | assert sorted(ms) == result 230 | assert len(ms) == len(result) 231 | 232 | 233 | def test_ixor(): 234 | m = Multiset('aabd') 235 | 236 | with pytest.raises(TypeError): 237 | m ^= 'abc' 238 | 239 | m ^= Multiset('abc') 240 | assert sorted(m) == list('acd') 241 | 242 | 243 | @pytest.mark.parametrize( 244 | ' initial, factor, result', 245 | [ 246 | ('aab', 2, list('aaaabb')), 247 | ('a', 3, list('aaa')), 248 | ('abc', 0, list()), 249 | ('abc', 1, list('abc')) 250 | ] 251 | ) # yapf: disable 252 | def test_times_update(initial, factor, result): 253 | ms = Multiset(initial) 254 | ms.times_update(factor) 255 | assert sorted(ms) == result 256 | assert len(ms) == len(result) 257 | 258 | 259 | def test_times_update_error(): 260 | with pytest.raises(ValueError): 261 | Multiset().times_update(-1) 262 | 263 | 264 | def test_imul(): 265 | m = Multiset('aab') 266 | 267 | with pytest.raises(TypeError): 268 | m *= 'a' 269 | 270 | with pytest.raises(ValueError): 271 | m *= -1 272 | 273 | m *= 2 274 | assert sorted(m) == list('aaaabb') 275 | 276 | 277 | def test_add(): 278 | m = Multiset('aab') 279 | assert len(m) == 3 280 | 281 | with pytest.raises(ValueError): 282 | m.add('a', 0) 283 | 284 | assert 'c' not in m 285 | m.add('c') 286 | assert 'c' in m 287 | assert m['c'] == 1 288 | assert len(m) == 4 289 | 290 | assert 'd' not in m 291 | m.add('d', 42) 292 | assert 'd' in m 293 | assert m['d'] == 42 294 | assert len(m) == 46 295 | 296 | m.add('c', 2) 297 | assert m['c'] == 3 298 | assert len(m) == 48 299 | 300 | 301 | def test_remove(): 302 | m = Multiset('aaaabbc') 303 | 304 | with pytest.raises(KeyError): 305 | m.remove('x') 306 | 307 | with pytest.raises(ValueError): 308 | m.remove('a', -1) 309 | 310 | assert len(m) == 7 311 | 312 | assert 'c' in m 313 | count = m.remove('c') 314 | assert 'c' not in m 315 | assert count == 1 316 | assert len(m) == 6 317 | 318 | assert 'b' in m 319 | count = m.remove('b') 320 | assert 'b' not in m 321 | assert count == 2 322 | assert len(m) == 4 323 | 324 | assert 'a' in m 325 | count = m.remove('a', 0) 326 | assert 'a' in m 327 | assert count == 4 328 | assert m['a'] == 4 329 | assert len(m) == 4 330 | 331 | assert 'a' in m 332 | count = m.remove('a', 1) 333 | assert 'a' in m 334 | assert count == 4 335 | assert m['a'] == 3 336 | assert len(m) == 3 337 | 338 | count = m.remove('a', 2) 339 | assert 'a' in m 340 | assert count == 3 341 | assert m['a'] == 1 342 | assert len(m) == 1 343 | 344 | 345 | def test_delitem(): 346 | m = Multiset('aaaabbc') 347 | 348 | with pytest.raises(KeyError): 349 | del m['x'] 350 | 351 | assert len(m) == 7 352 | 353 | assert 'c' in m 354 | del m['c'] 355 | assert 'c' not in m 356 | assert len(m) == 6 357 | 358 | assert 'b' in m 359 | del m['b'] 360 | assert 'b' not in m 361 | assert len(m) == 4 362 | 363 | assert 'a' in m 364 | del m['a'] 365 | assert 'a' not in m 366 | assert len(m) == 0 367 | 368 | 369 | def test_discard(): 370 | m = Multiset('aaaabbc') 371 | 372 | with pytest.raises(ValueError): 373 | m.discard('a', -1) 374 | 375 | assert len(m) == 7 376 | 377 | assert 'c' in m 378 | count = m.discard('c') 379 | assert 'c' not in m 380 | assert count == 1 381 | assert len(m) == 6 382 | 383 | assert 'b' in m 384 | count = m.discard('b') 385 | assert 'b' not in m 386 | assert count == 2 387 | assert len(m) == 4 388 | 389 | assert 'a' in m 390 | count = m.discard('a', 0) 391 | assert 'a' in m 392 | assert count == 4 393 | assert m['a'] == 4 394 | assert len(m) == 4 395 | 396 | assert 'a' in m 397 | count = m.discard('a', 1) 398 | assert 'a' in m 399 | assert count == 4 400 | assert m['a'] == 3 401 | assert len(m) == 3 402 | 403 | count = m.discard('a', 2) 404 | assert 'a' in m 405 | assert count == 3 406 | assert m['a'] == 1 407 | assert len(m) == 1 408 | 409 | 410 | @pytest.mark.parametrize( 411 | ' set1, set2, disjoint', 412 | [ 413 | ('aab', 'a', False), 414 | ('aab', 'ab', False), 415 | ('a', 'aab', False), 416 | ('aab', 'c', True), 417 | ('aab', '', True), 418 | ('', 'abc', True), 419 | ] 420 | ) # yapf: disable 421 | def test_isdisjoint(MultisetCls, set1, set2, disjoint): 422 | ms = MultisetCls(set1) 423 | 424 | if disjoint: 425 | assert ms.isdisjoint(set2) 426 | assert ms.isdisjoint(iter(set2)) 427 | else: 428 | assert not ms.isdisjoint(set2) 429 | assert not ms.isdisjoint(iter(set2)) 430 | 431 | 432 | 433 | @pytest.mark.parametrize( 434 | ' initial, args, expected', 435 | [ 436 | ('aab', ['abc'], list('aabc')), 437 | ('aab', [''], list('aab')), 438 | ('aab', [{'a': 2, 'b': 1}], list('aab')), 439 | ('aab', [{}], list('aab')), 440 | ('aab', [{'c': 0}], list('aab')), 441 | ('a', [Multiset('a')], list('a')), 442 | ('ab', [Multiset()], list('ab')), 443 | ('ab', [], list('ab')), 444 | ('ab', ['a', 'bc'], list('abc')), 445 | ('ab', [{}, ''], list('ab')), 446 | ('ab', [{'c': 1}, {'d': 0}], list('abc')), 447 | ('ab', ['aa'], list('aab')), 448 | ] 449 | ) # yapf: disable 450 | def test_union(MultisetCls, initial, args, expected): 451 | ms = MultisetCls(initial) 452 | result = ms.union(*args) 453 | assert sorted(result) == expected 454 | assert len(result) == len(expected) 455 | assert isinstance(result, MultisetCls) 456 | assert result is not ms 457 | 458 | 459 | def test_union_error(MultisetCls): 460 | with pytest.raises(TypeError): 461 | MultisetCls().union(None) 462 | 463 | 464 | def test_or(MultisetCls): 465 | ms = MultisetCls('ab') 466 | 467 | with pytest.raises(TypeError): 468 | _ = ms | 'abc' 469 | 470 | result = ms | MultisetCls('abc') 471 | assert sorted(result) == list('abc') 472 | assert isinstance(result, MultisetCls) 473 | assert result is not ms 474 | 475 | 476 | @pytest.mark.parametrize( 477 | ' initial, args, expected', 478 | [ 479 | ('aab', ['abc'], list('aaabbc')), 480 | ('aab', [''], list('aab')), 481 | ('aab', [{'a': 2, 'b': 1}], list('aaaabb')), 482 | ('aab', [{}], list('aab')), 483 | ('aab', [{'c': 0}], list('aab')), 484 | ('a', [Multiset('a')], list('aa')), 485 | ('ab', [Multiset()], list('ab')), 486 | ('ab', [], list('ab')), 487 | ('ab', ['a', 'bc'], list('aabbc')), 488 | ('ab', [{}, ''], list('ab')), 489 | ('ab', [{'c': 1}, {'d': 0}], list('abc')), 490 | ('aa', [{'a': -1}], list('a')), 491 | ('aa', [{'a': -2}], list()), 492 | ('aa', [{'a': -3}], list()), 493 | ] 494 | ) # yapf: disable 495 | def test_combine(MultisetCls, initial, args, expected): 496 | ms = MultisetCls(initial) 497 | result = ms.combine(*args) 498 | assert sorted(result) == expected 499 | assert len(result) == len(expected) 500 | assert isinstance(result, MultisetCls) 501 | assert result is not ms 502 | 503 | 504 | def test_add_op(MultisetCls): 505 | ms = MultisetCls('aab') 506 | 507 | with pytest.raises(TypeError): 508 | _ = ms + 'abc' 509 | 510 | result = ms + MultisetCls('abc') 511 | assert sorted(result) == list('aaabbc') 512 | assert isinstance(result, MultisetCls) 513 | assert result is not ms 514 | 515 | 516 | @pytest.mark.parametrize( 517 | ' initial, args, expected', 518 | [ 519 | ('aab', ['abc'], list('ab')), 520 | ('aab', [''], list()), 521 | ('aab', [{'a': 2, 'b': 1}], list('aab')), 522 | ('aab', [{}], list()), 523 | ('aab', [{'c': 0}], list()), 524 | ('a', [Multiset('a')], list('a')), 525 | ('ab', [Multiset()], list()), 526 | ('ab', [], list('ab')), 527 | ('ab', ['a', 'bc'], list()), 528 | ('ab', ['a', 'aab'], list('a')), 529 | ('ab', [{}, ''], list()), 530 | ('ab', [{'c': 1}, {'d': 0}], list()), 531 | ('ab', ['aa'], list('a')), 532 | ] 533 | ) # yapf: disable 534 | def test_intersection(MultisetCls, initial, args, expected): 535 | ms = MultisetCls(initial) 536 | result = ms.intersection(*args) 537 | assert sorted(result) == expected 538 | assert len(result) == len(expected) 539 | assert isinstance(result, MultisetCls) 540 | assert result is not ms 541 | 542 | 543 | def test_and(MultisetCls): 544 | ms = MultisetCls('aabd') 545 | 546 | with pytest.raises(TypeError): 547 | _ = ms & 'abc' 548 | 549 | result = ms & MultisetCls('abc') 550 | assert sorted(result) == list('ab') 551 | assert isinstance(result, MultisetCls) 552 | assert result is not ms 553 | 554 | 555 | @pytest.mark.parametrize( 556 | ' initial, other, expected', 557 | [ 558 | ('aab', 'bc', list('aa')), 559 | ('aab', 'cd', list('aab')), 560 | ('aab', 'a', list('ab')), 561 | ('aab', 'aa', list('b')), 562 | ('aab', '', list('aab')), 563 | ('aab', {'a': 2, 'b': 1}, list()), 564 | ('aab', {}, list('aab')), 565 | ('aab', {'c': 0}, list('aab')), 566 | ('a', Multiset('a'), list()), 567 | ('ab', Multiset(), list('ab')), 568 | ('ab', 'aa', list('b')), 569 | ] 570 | ) # yapf: disable 571 | def test_difference(MultisetCls, initial, other, expected): 572 | ms = MultisetCls(initial) 573 | result = ms.difference(other) 574 | assert sorted(result) == expected 575 | assert len(result) == len(expected) 576 | assert isinstance(result, MultisetCls) 577 | assert result is not ms 578 | 579 | 580 | def test_sub(MultisetCls): 581 | ms = MultisetCls('aabd') 582 | 583 | with pytest.raises(TypeError): 584 | _ = ms - 'abc' 585 | 586 | result = ms - MultisetCls('abc') 587 | assert sorted(result) == list('ad') 588 | assert isinstance(result, MultisetCls) 589 | assert result is not ms 590 | 591 | 592 | def test_rsub(MultisetCls): 593 | ms = MultisetCls('abc') 594 | 595 | with pytest.raises(TypeError): 596 | _ = 'abc' - ms 597 | 598 | result = set('abd') - ms 599 | assert sorted(result) == list('d') 600 | assert isinstance(result, MultisetCls) 601 | assert result is not ms 602 | 603 | 604 | @pytest.mark.parametrize( 605 | ' initial, other, expected', 606 | [ 607 | ('aab', 'bc', list('aac')), 608 | ('aab', 'cd', list('aabcd')), 609 | ('aab', 'a', list('ab')), 610 | ('aab', 'aa', list('b')), 611 | ('aab', '', list('aab')), 612 | ('aab', {'a': 2, 'b': 1}, list()), 613 | ('aab', {}, list('aab')), 614 | ('aab', {'c': 0}, list('aab')), 615 | ('a', Multiset('a'), list()), 616 | ('ab', Multiset(), list('ab')), 617 | ('ab', 'aa', list('ab')), 618 | ] 619 | ) # yapf: disable 620 | def test_symmetric_difference(MultisetCls, initial, other, expected): 621 | ms = MultisetCls(initial) 622 | result = ms.symmetric_difference(other) 623 | assert sorted(result) == expected 624 | assert len(result) == len(expected) 625 | assert isinstance(result, MultisetCls) 626 | assert result is not ms 627 | 628 | 629 | def test_xor(MultisetCls): 630 | ms = MultisetCls('aabd') 631 | 632 | with pytest.raises(TypeError): 633 | _ = ms ^ 'abc' 634 | 635 | result = ms ^ MultisetCls('abc') 636 | assert sorted(result) == list('acd') 637 | assert isinstance(result, MultisetCls) 638 | assert result is not ms 639 | 640 | 641 | @pytest.mark.parametrize( 642 | ' initial, factor, expected', 643 | [ 644 | ('aab', 2, list('aaaabb')), 645 | ('a', 3, list('aaa')), 646 | ('abc', 0, list()), 647 | ('abc', 1, list('abc')), 648 | ] 649 | ) # yapf: disable 650 | def test_times(MultisetCls, initial, factor, expected): 651 | ms = MultisetCls(initial) 652 | 653 | result = ms.times(factor) 654 | 655 | assert sorted(result) == expected 656 | assert len(result) == len(expected) 657 | assert isinstance(result, MultisetCls) 658 | assert result is not ms 659 | 660 | 661 | def test_times_error(MultisetCls): 662 | with pytest.raises(ValueError): 663 | _ = MultisetCls().times(-1) 664 | 665 | 666 | def test_mul(MultisetCls): 667 | ms = MultisetCls('aab') 668 | 669 | with pytest.raises(TypeError): 670 | _ = ms * 'a' 671 | 672 | result = ms * 2 673 | assert sorted(result) == list('aaaabb') 674 | assert isinstance(result, MultisetCls) 675 | assert result is not ms 676 | 677 | 678 | @pytest.mark.parametrize( 679 | ' set1, set2, issubset', 680 | [ 681 | ('a', 'abc', True), 682 | ('abc', 'abc', True), 683 | ('', 'abc', True), 684 | ('d', 'abc', False), 685 | ('abcd', 'abc', False), 686 | ('aabc', 'abc', False), 687 | ('abd', 'abc', False), 688 | ('a', '', False), 689 | ('', 'a', True) 690 | ] 691 | ) # yapf: disable 692 | def test_issubset(MultisetCls, set1, set2, issubset): 693 | ms = MultisetCls(set1) 694 | if issubset: 695 | assert ms.issubset(set2) 696 | else: 697 | assert not ms.issubset(set2) 698 | 699 | 700 | def test_le(MultisetCls): 701 | set1 = MultisetCls('ab') 702 | set2 = MultisetCls('aab') 703 | set3 = MultisetCls('ac') 704 | 705 | assert set1 <= set2 706 | assert not set2 <= set1 707 | assert set1 <= set1 708 | assert set2 <= set2 709 | assert not set1 <= set3 710 | assert not set3 <= set1 711 | 712 | 713 | def test_le_error(MultisetCls): 714 | with pytest.raises(TypeError): 715 | MultisetCls('ab') <= 'x' 716 | 717 | 718 | def test_lt(MultisetCls): 719 | set1 = MultisetCls('ab') 720 | set2 = MultisetCls('aab') 721 | set3 = MultisetCls('ac') 722 | 723 | assert set1 < set2 724 | assert not set2 < set1 725 | assert not set1 < set1 726 | assert not set2 < set2 727 | assert not set1 <= set3 728 | assert not set3 <= set1 729 | 730 | 731 | def test_lt_error(MultisetCls): 732 | with pytest.raises(TypeError): 733 | MultisetCls('ab') < 'x' 734 | 735 | 736 | @pytest.mark.parametrize( 737 | ' set1, set2, issubset', 738 | [ 739 | ('abc', 'a', True), 740 | ('abc', 'abc', True), 741 | ('abc', '', True), 742 | ('abc', 'd', False), 743 | ('abc', 'abcd', False), 744 | ('abc', 'aabc', False), 745 | ('abc', 'abd', False), 746 | ('a', '', True), 747 | ('', 'a', False) 748 | ] 749 | ) # yapf: disable 750 | def test_issuperset(MultisetCls, set1, set2, issubset): 751 | ms = MultisetCls(set1) 752 | if issubset: 753 | assert ms.issuperset(set2) 754 | else: 755 | assert not ms.issuperset(set2) 756 | 757 | 758 | def test_ge(MultisetCls): 759 | set1 = MultisetCls('ab') 760 | set2 = MultisetCls('aab') 761 | set3 = MultisetCls('ac') 762 | 763 | assert set1 >= set1 764 | assert not set1 >= set2 765 | assert not set1 >= set3 766 | 767 | assert set2 >= set1 768 | assert set2 >= set2 769 | assert not set2 >= set3 770 | 771 | assert not set3 >= set1 772 | assert not set3 >= set2 773 | assert set3 >= set3 774 | 775 | 776 | def test_ge_error(MultisetCls): 777 | with pytest.raises(TypeError): 778 | MultisetCls('ab') >= 'x' 779 | 780 | 781 | def test_gt(MultisetCls): 782 | set1 = MultisetCls('ab') 783 | set2 = MultisetCls('aab') 784 | set3 = MultisetCls('ac') 785 | 786 | assert not set1 > set1 787 | assert not set1 > set2 788 | assert not set1 > set3 789 | 790 | assert set2 > set1 791 | assert not set2 > set2 792 | assert not set2 > set3 793 | 794 | assert not set3 > set1 795 | assert not set3 > set2 796 | assert not set3 > set3 797 | 798 | 799 | def test_gt_error(MultisetCls): 800 | with pytest.raises(TypeError): 801 | MultisetCls('ab') > 'x' 802 | 803 | 804 | def test_compare_with_set(MultisetCls): 805 | assert MultisetCls('ab') <= set('ab') 806 | assert MultisetCls('b') <= set('ab') 807 | assert MultisetCls('ab') >= set('ab') 808 | assert MultisetCls('abb') >= set('ab') 809 | assert set('ab') <= MultisetCls('abb') 810 | assert set('b') <= MultisetCls('aab') 811 | assert not set('ab') >= MultisetCls('aab') 812 | assert set('ab') <= MultisetCls('aab') 813 | assert set('ab') >= MultisetCls('ab') 814 | 815 | 816 | def test_eq_set(MultisetCls): 817 | multisets = ['', 'a', 'ab', 'aa'] 818 | sets = ['', 'a', 'ab'] 819 | 820 | for i, ms in enumerate(multisets): 821 | ms = MultisetCls(ms) 822 | for j, s in enumerate(sets): 823 | s = set(s) 824 | if i == j: 825 | assert ms == s 826 | assert s == ms 827 | else: 828 | assert not ms == s 829 | assert not s == ms 830 | 831 | 832 | @pytest.mark.parametrize('MultisetCls2', [Multiset, FrozenMultiset]) 833 | def test_eq(MultisetCls, MultisetCls2): 834 | assert not MultisetCls('ab') == MultisetCls2('b') 835 | assert not MultisetCls('ab') == MultisetCls2('a') 836 | assert MultisetCls('ab') == MultisetCls2('ab') 837 | assert MultisetCls('aab') == MultisetCls2('aab') 838 | assert not MultisetCls('aab') == MultisetCls2('abb') 839 | assert not MultisetCls('ab') == 'ab' 840 | 841 | 842 | def test_ne_set(MultisetCls): 843 | multisets = ['', 'a', 'ab', 'aa'] 844 | sets = ['', 'a', 'ab'] 845 | 846 | for i, ms in enumerate(multisets): 847 | ms = MultisetCls(ms) 848 | for j, s in enumerate(sets): 849 | s = set(s) 850 | if i == j: 851 | assert not ms != s 852 | assert not s != ms 853 | else: 854 | assert ms != s 855 | assert s != ms 856 | 857 | 858 | @pytest.mark.parametrize('MultisetCls2', [Multiset, FrozenMultiset]) 859 | def test_ne(MultisetCls, MultisetCls2): 860 | assert MultisetCls('ab') != MultisetCls2('b') 861 | assert MultisetCls('ab') != MultisetCls2('a') 862 | assert not MultisetCls('ab') != MultisetCls2('ab') 863 | assert not MultisetCls('aab') != MultisetCls2('aab') 864 | assert MultisetCls('aab') != MultisetCls2('abb') 865 | assert MultisetCls('ab') != 'ab' 866 | 867 | 868 | def test_copy(MultisetCls): 869 | ms = MultisetCls('abc') 870 | 871 | ms_copy = ms.copy() 872 | 873 | assert ms == ms_copy 874 | assert ms is not ms_copy 875 | assert isinstance(ms_copy, MultisetCls) 876 | 877 | 878 | def test_bool(MultisetCls): 879 | assert MultisetCls('abc') 880 | assert not MultisetCls() 881 | assert not MultisetCls({}) 882 | assert not MultisetCls([]) 883 | 884 | 885 | def test_dict_methods(MultisetCls): 886 | ms = MultisetCls('aab') 887 | assert ms.get('a', 5) == 2 888 | assert ms.get('b', 5) == 1 889 | assert ms.get('c', 5) == 5 890 | 891 | ms = MultisetCls.from_elements('abc', 2) 892 | assert ['a', 'a', 'b', 'b', 'c', 'c'] == sorted(ms) 893 | assert isinstance(ms, MultisetCls) 894 | 895 | 896 | def test_mutating_dict_methods(): 897 | ms = Multiset('aab') 898 | assert len(ms) == len('aab') 899 | 900 | assert ms.pop('a', 5) == 2 901 | assert len(ms) == len('b') 902 | assert ms.pop('c', 3) == 3 903 | assert len(ms) == len('b') 904 | assert ['b'] == sorted(ms) 905 | 906 | assert ms.setdefault('b', 5) == 1 907 | assert len(ms) == len('b') 908 | assert ms.setdefault('c', 3) == 3 909 | assert len(ms) == len('bccc') 910 | assert ['b', 'c', 'c', 'c'] == sorted(ms) 911 | 912 | 913 | @pytest.mark.parametrize('parent', [Iterable, Mapping, Sized, Container]) 914 | def test_instance_check(MultisetCls, parent): 915 | assert isinstance(MultisetCls(), parent) 916 | 917 | 918 | def test_mutable_instance_check(): 919 | assert isinstance(Multiset(), MutableMapping) 920 | 921 | 922 | @pytest.mark.parametrize( 923 | ' elements, items', 924 | [ 925 | ('', []), 926 | ('a', [('a', 1)]), 927 | ('ab', [('a', 1), ('b', 1)]), 928 | ('aab', [('a', 2), ('b', 1)]), 929 | ] 930 | ) # yapf: disable 931 | def test_items(MultisetCls, elements, items): 932 | ms = MultisetCls(elements) 933 | 934 | assert sorted(ms.items()) == items 935 | 936 | 937 | @pytest.mark.parametrize( 938 | ' elements, distinct_elements', 939 | [ 940 | ('', []), 941 | ('a', ['a']), 942 | ('ab', ['a', 'b']), 943 | ('aab', ['a', 'b']), 944 | ('aabbb', ['a', 'b']), 945 | ] 946 | ) # yapf: disable 947 | def test_distinct_elements(MultisetCls, elements, distinct_elements): 948 | ms = MultisetCls(elements) 949 | 950 | assert sorted(ms.distinct_elements()) == distinct_elements 951 | 952 | 953 | @pytest.mark.parametrize( 954 | ' elements, multiplicities', 955 | [ 956 | ('', []), 957 | ('a', [1]), 958 | ('ab', [1, 1]), 959 | ('aab', [1, 2]), 960 | ('aabbb', [2, 3]), 961 | ] 962 | ) # yapf: disable 963 | def test_multiplicities(MultisetCls, elements, multiplicities): 964 | ms = MultisetCls(elements) 965 | 966 | assert sorted(ms.multiplicities()) == multiplicities 967 | 968 | 969 | def test_base_error(): 970 | with pytest.raises(TypeError): 971 | _ = BaseMultiset() 972 | 973 | 974 | def test_frozen_hash_equal(): 975 | ms1 = FrozenMultiset('ab') 976 | ms2 = FrozenMultiset('ba') 977 | 978 | assert hash(ms1) == hash(ms2) 979 | 980 | def test_can_be_pickled(): 981 | fms = FrozenMultiset('aabcd') 982 | 983 | pickled = pickle.dumps(fms) 984 | unpickled = pickle.loads(pickled) 985 | 986 | assert fms == unpickled 987 | 988 | 989 | def test_str(): 990 | ms = Multiset('aabc') 991 | 992 | assert str(ms) == '{a, a, b, c}' 993 | 994 | 995 | def test_repr(): 996 | ms = Multiset('aabc') 997 | 998 | assert repr(ms) == "Multiset({'a': 2, 'b': 1, 'c': 1})" 999 | 1000 | 1001 | def test_multiplicities(MultisetCls): 1002 | ms = MultisetCls('aaabbc') 1003 | assert sorted(ms.multiplicities()) == [1, 2, 3] 1004 | 1005 | 1006 | def test_distinct_elements(MultisetCls): 1007 | ms = MultisetCls('aaabbc') 1008 | assert sorted(ms.distinct_elements()) == list('abc') 1009 | -------------------------------------------------------------------------------- /multiset/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """An implementation of a multiset.""" 3 | from typing import (Generic, TypeVar, Hashable, Mapping as MappingType, Union, Optional, Iterable as IterableType, 4 | Type, ItemsView, KeysView, ValuesView, MutableMapping as MutableMappingType, AbstractSet as SetType) 5 | from collections import defaultdict 6 | from collections.abc import Iterable, Mapping, MutableMapping, Set, Sized, Container 7 | from itertools import chain, repeat, starmap 8 | 9 | _sequence_types = (tuple, list, range, set, frozenset, str) 10 | _iter_types = (type(iter([])), type((lambda: (yield))())) 11 | 12 | _all_basic_types = _sequence_types + _iter_types + (dict, ) 13 | 14 | __all__ = ['BaseMultiset', 'Multiset', 'FrozenMultiset'] 15 | 16 | _TElement = TypeVar('_TElement', bound=Hashable) 17 | _OtherType = Union[IterableType[_TElement], MappingType[_TElement, int]] 18 | _Self = TypeVar('_Self', bound='BaseMultiset') 19 | 20 | 21 | class BaseMultiset(MappingType[_TElement, int], Generic[_TElement]): 22 | """A multiset implementation. 23 | 24 | A multiset is similar to the builtin :class:`set`, but elements can occur multiple times in the multiset. 25 | It is also similar to a :class:`list` without ordering of the values and hence no index-based operations. 26 | 27 | The multiset internally uses a :class:`dict` for storage where the key is the element and the value its 28 | multiplicity. It supports all operations that the :class:`set` supports. 29 | 30 | In contrast to the builtin :class:`collections.Counter`, no negative counts are allowed, elements with 31 | zero counts are removed from the :class:`dict`, and set operations are supported. 32 | 33 | The multiset comes in two variants, `Multiset` and `FrozenMultiset` which correspond to the `set` and 34 | `frozenset` classes, respectively. 35 | 36 | .. warning:: 37 | 38 | You cannot instantiate this class directly. Use one of its variants instead. 39 | 40 | :see: https://en.wikipedia.org/wiki/Multiset 41 | """ 42 | 43 | __slots__ = ('_elements', '_total') 44 | 45 | def __init__(self, iterable: Optional[_OtherType] = None): 46 | r"""Create a new, empty Multiset object. 47 | 48 | And if given, initialize with elements from input iterable. 49 | Or, initialize from a mapping of elements to their multiplicity. 50 | 51 | Example: 52 | 53 | >>> ms = Multiset() # a new, empty multiset 54 | >>> ms = Multiset('abc') # a new multiset from an iterable 55 | >>> ms = Multiset({'a': 4, 'b': 2}) # a new multiset from a mapping 56 | 57 | Args: 58 | iterable: 59 | An optional iterable of elements or mapping of elements to multiplicity to 60 | initialize the multiset from. 61 | """ 62 | if isinstance(iterable, BaseMultiset): 63 | self._elements = iterable._elements.copy() 64 | self._total = iterable._total 65 | else: 66 | self._elements = _elements = defaultdict(int) 67 | _total = 0 68 | if iterable is not None: 69 | if isinstance(iterable, _sequence_types): 70 | for element in iterable: 71 | _elements[element] += 1 72 | _total = len(iterable) 73 | elif isinstance(iterable, dict): 74 | for element, multiplicity in iterable.items(): 75 | if multiplicity > 0: 76 | _elements[element] = multiplicity 77 | _total += multiplicity 78 | elif isinstance(iterable, _iter_types): 79 | for element in iterable: 80 | _elements[element] += 1 81 | _total += 1 82 | elif isinstance(iterable, Mapping): 83 | for element, multiplicity in iterable.items(): 84 | if multiplicity > 0: 85 | _elements[element] = multiplicity 86 | _total += multiplicity 87 | elif isinstance(iterable, Sized): 88 | for element in iterable: 89 | _elements[element] += 1 90 | _total = len(iterable) 91 | else: 92 | for element in iterable: 93 | _elements[element] += 1 94 | _total += 1 95 | self._total = _total 96 | 97 | def __new__(cls, iterable=None): 98 | if cls is BaseMultiset: 99 | raise TypeError("Cannot instantiate BaseMultiset directly, use either Multiset or FrozenMultiset.") 100 | return super(BaseMultiset, cls).__new__(cls) 101 | 102 | def __contains__(self, element: object) -> bool: 103 | return element in self._elements 104 | 105 | def __getitem__(self, element: _TElement) -> int: 106 | """The multiplicity of an element or zero if it is not in the multiset.""" 107 | return self._elements.get(element, 0) 108 | 109 | def __str__(self) -> str: 110 | return '{%s}' % ', '.join(map(str, self.__iter__())) 111 | 112 | def __repr__(self) -> str: 113 | items = ', '.join('%r: %r' % item for item in self._elements.items()) 114 | return '%s({%s})' % (self.__class__.__name__, items) 115 | 116 | def __len__(self) -> int: 117 | """Returns the total number of elements in the multiset. 118 | 119 | Note that this is equivalent to the sum of the multiplicities: 120 | 121 | >>> ms = Multiset('aab') 122 | >>> len(ms) 123 | 3 124 | >>> sum(ms.multiplicities()) 125 | 3 126 | 127 | If you need the total number of distinct elements, use either the :meth:`distinct_elements` method: 128 | >>> len(ms.distinct_elements()) 129 | 2 130 | 131 | or convert to a :class:`set`: 132 | >>> len(set(ms)) 133 | 2 134 | """ 135 | return self._total 136 | 137 | def __bool__(self) -> bool: 138 | return self._total > 0 139 | 140 | def __iter__(self) -> IterableType[_TElement]: 141 | return chain.from_iterable(starmap(repeat, self._elements.items())) 142 | 143 | def isdisjoint(self, other: _OtherType) -> bool: 144 | r"""Return True if the set has no elements in common with other. 145 | 146 | Sets are disjoint iff their intersection is the empty set. 147 | 148 | >>> ms = Multiset('aab') 149 | >>> ms.isdisjoint('bc') 150 | False 151 | >>> ms.isdisjoint(Multiset('ccd')) 152 | True 153 | 154 | Args: 155 | other: The other set to check disjointedness. Can also be an :class:`~typing.Iterable`\[~T] 156 | or :class:`~typing.Mapping`\[~T, :class:`int`] which are then converted to :class:`Multiset`\[~T]. 157 | """ 158 | if isinstance(other, _sequence_types + (BaseMultiset, )): 159 | pass 160 | elif not isinstance(other, Container): 161 | other = self._as_multiset(other) 162 | return all(element not in other for element in self._elements.keys()) 163 | 164 | def difference(self: _Self, *others: _OtherType) -> _Self: 165 | r"""Return a new multiset with all elements from the others removed. 166 | 167 | >>> ms = Multiset('aab') 168 | >>> sorted(ms.difference('bc')) 169 | ['a', 'a'] 170 | 171 | You can also use the ``-`` operator for the same effect. However, the operator version 172 | will only accept a set as other operator, not any iterable, to avoid errors. 173 | 174 | >>> ms = Multiset('aabbbc') 175 | >>> sorted(ms - Multiset('abd')) 176 | ['a', 'b', 'b', 'c'] 177 | 178 | For a variant of the operation which modifies the multiset in place see 179 | :meth:`difference_update`. 180 | 181 | Args: 182 | others: The other sets to remove from the multiset. Can also be any :class:`~typing.Iterable`\[~T] 183 | or :class:`~typing.Mapping`\[~T, :class:`int`] which are then converted to :class:`Multiset`\[~T]. 184 | 185 | Returns: 186 | The resulting difference multiset. 187 | """ 188 | result = self.__copy__() 189 | _elements = result._elements 190 | _total = result._total 191 | for other in map(self._as_multiset, others): 192 | for element, multiplicity in other.items(): 193 | if element in _elements: 194 | old_multiplicity = _elements[element] 195 | new_multiplicity = old_multiplicity - multiplicity 196 | if new_multiplicity > 0: 197 | _elements[element] = new_multiplicity 198 | _total -= multiplicity 199 | else: 200 | del _elements[element] 201 | _total -= old_multiplicity 202 | result._total = _total 203 | return result 204 | 205 | def __sub__(self: _Self, other: Union[SetType[_TElement], 'BaseMultiset[_TElement]']) -> _Self: 206 | if isinstance(other, (BaseMultiset, set, frozenset)): 207 | pass 208 | elif not isinstance(other, Set): 209 | return NotImplemented 210 | return self.difference(other) 211 | 212 | def __rsub__(self: _Self, other: Union[SetType[_TElement], 'BaseMultiset[_TElement]']) -> _Self: 213 | if not isinstance(other, (Set, BaseMultiset)): 214 | return NotImplemented 215 | return self._as_multiset(other).difference(self) 216 | 217 | def union(self: _Self, *others: _OtherType) -> _Self: 218 | r"""Return a new multiset with all elements from the multiset and the others with maximal multiplicities. 219 | 220 | >>> ms = Multiset('aab') 221 | >>> sorted(ms.union('bc')) 222 | ['a', 'a', 'b', 'c'] 223 | 224 | You can also use the ``|`` operator for the same effect. However, the operator version 225 | will only accept a set as other operator, not any iterable, to avoid errors. 226 | 227 | >>> ms = Multiset('aab') 228 | >>> sorted(ms | Multiset('aaa')) 229 | ['a', 'a', 'a', 'b'] 230 | 231 | For a variant of the operation which modifies the multiset in place see 232 | :meth:`union_update`. 233 | 234 | Args: 235 | *others: The other sets to union the multiset with. Can also be any :class:`~typing.Iterable`\[~T] 236 | or :class:`~typing.Mapping`\[~T, :class:`int`] which are then converted to :class:`Multiset`\[~T]. 237 | 238 | Returns: 239 | The multiset resulting from the union. 240 | """ 241 | result = self.__copy__() 242 | _elements = result._elements 243 | _total = result._total 244 | for other in map(self._as_mapping, others): 245 | for element, multiplicity in other.items(): 246 | old_multiplicity = _elements.get(element, 0) 247 | if multiplicity > old_multiplicity: 248 | _elements[element] = multiplicity 249 | _total += multiplicity - old_multiplicity 250 | result._total = _total 251 | return result 252 | 253 | def __or__(self: _Self, other: Union[SetType[_TElement], 'BaseMultiset[_TElement]']) -> _Self: 254 | if isinstance(other, (BaseMultiset, set, frozenset)): 255 | pass 256 | elif not isinstance(other, Set): 257 | return NotImplemented 258 | return self.union(other) 259 | 260 | __ror__ = __or__ 261 | 262 | def combine(self: _Self, *others: _OtherType) -> _Self: 263 | r"""Return a new multiset with all elements from the multiset and the others with their multiplicities summed up. 264 | 265 | >>> ms = Multiset('aab') 266 | >>> sorted(ms.combine('bc')) 267 | ['a', 'a', 'b', 'b', 'c'] 268 | 269 | You can also use the ``+`` operator for the same effect. However, the operator version 270 | will only accept a set as other operator, not any iterable, to avoid errors. 271 | 272 | >>> ms = Multiset('aab') 273 | >>> sorted(ms + Multiset('a')) 274 | ['a', 'a', 'a', 'b'] 275 | 276 | For a variant of the operation which modifies the multiset in place see 277 | :meth:`update`. 278 | 279 | Args: 280 | others: The other sets to add to the multiset. Can also be any :class:`~typing.Iterable`\[~T] 281 | or :class:`~typing.Mapping`\[~T, :class:`int`] which are then converted to :class:`Multiset`\[~T]. 282 | 283 | Returns: 284 | The multiset resulting from the addition of the sets. 285 | """ 286 | result = self.__copy__() 287 | _elements = result._elements 288 | _total = result._total 289 | for other in map(self._as_mapping, others): 290 | for element, multiplicity in other.items(): 291 | old_multiplicity = _elements.get(element, 0) 292 | new_multiplicity = old_multiplicity + multiplicity 293 | if old_multiplicity > 0 and new_multiplicity <= 0: 294 | del _elements[element] 295 | _total -= old_multiplicity 296 | elif new_multiplicity > 0: 297 | _elements[element] = new_multiplicity 298 | _total += multiplicity 299 | result._total = _total 300 | return result 301 | 302 | def __add__(self: _Self, other: Union[SetType[_TElement], 'BaseMultiset[_TElement]']) -> _Self: 303 | if isinstance(other, (BaseMultiset, set, frozenset)): 304 | pass 305 | elif not isinstance(other, Set): 306 | return NotImplemented 307 | return self.combine(other) 308 | 309 | __radd__ = __add__ 310 | 311 | def intersection(self: _Self, *others: _OtherType) -> _Self: 312 | r"""Return a new multiset with elements common to the multiset and all others. 313 | 314 | >>> ms = Multiset('aab') 315 | >>> sorted(ms.intersection('abc')) 316 | ['a', 'b'] 317 | 318 | You can also use the ``&`` operator for the same effect. However, the operator version 319 | will only accept a set as other operator, not any iterable, to avoid errors. 320 | 321 | >>> ms = Multiset('aab') 322 | >>> sorted(ms & Multiset('aaac')) 323 | ['a', 'a'] 324 | 325 | For a variant of the operation which modifies the multiset in place see 326 | :meth:`intersection_update`. 327 | 328 | Args: 329 | others: The other sets intersect with the multiset. Can also be any :class:`~typing.Iterable`\[~T] 330 | or :class:`~typing.Mapping`\[~T, :class:`int`] which are then converted to :class:`Multiset`\[~T]. 331 | 332 | Returns: 333 | The multiset resulting from the intersection of the sets. 334 | """ 335 | result = self.__copy__() 336 | _elements = result._elements 337 | _total = result._total 338 | for other in map(self._as_mapping, others): 339 | for element, multiplicity in list(_elements.items()): 340 | new_multiplicity = other.get(element, 0) 341 | if new_multiplicity < multiplicity: 342 | if new_multiplicity > 0: 343 | _elements[element] = new_multiplicity 344 | _total -= multiplicity - new_multiplicity 345 | else: 346 | del _elements[element] 347 | _total -= multiplicity 348 | result._total = _total 349 | return result 350 | 351 | def __and__(self: _Self, other: Union[SetType[_TElement], 'BaseMultiset[_TElement]']) -> _Self: 352 | if isinstance(other, (BaseMultiset, set, frozenset)): 353 | pass 354 | elif not isinstance(other, Set): 355 | return NotImplemented 356 | return self.intersection(other) 357 | 358 | __rand__ = __and__ 359 | 360 | def symmetric_difference(self: _Self, other: _OtherType) -> _Self: 361 | r"""Return a new set with elements in either the set or other but not both. 362 | 363 | >>> ms = Multiset('aab') 364 | >>> sorted(ms.symmetric_difference('abc')) 365 | ['a', 'c'] 366 | 367 | You can also use the ``^`` operator for the same effect. However, the operator version 368 | will only accept a set as other operator, not any iterable, to avoid errors. 369 | 370 | >>> ms = Multiset('aab') 371 | >>> sorted(ms ^ Multiset('aaac')) 372 | ['a', 'b', 'c'] 373 | 374 | For a variant of the operation which modifies the multiset in place see 375 | :meth:`symmetric_difference_update`. 376 | 377 | Args: 378 | other: The other set to take the symmetric difference with. Can also be any :class:`~typing.Iterable`\[~T] 379 | or :class:`~typing.Mapping`\[~T, :class:`int`] which are then converted to :class:`Multiset`\[~T]. 380 | 381 | Returns: 382 | The resulting symmetric difference multiset. 383 | """ 384 | other = self._as_multiset(other) 385 | result = self.__class__() 386 | _total = 0 387 | _elements = result._elements 388 | self_elements = self._elements 389 | other_elements = other._elements 390 | dist_elements = set(self_elements.keys()) | set(other_elements.keys()) 391 | for element in dist_elements: 392 | multiplicity = self_elements.get(element, 0) 393 | other_multiplicity = other_elements.get(element, 0) 394 | new_multiplicity = (multiplicity - other_multiplicity 395 | if multiplicity > other_multiplicity else other_multiplicity - multiplicity) 396 | _total += new_multiplicity 397 | if new_multiplicity > 0: 398 | _elements[element] = new_multiplicity 399 | result._total = _total 400 | return result 401 | 402 | def __xor__(self: _Self, other: Union[SetType[_TElement], 'BaseMultiset[_TElement]']) -> _Self: 403 | if isinstance(other, (BaseMultiset, set, frozenset)): 404 | pass 405 | elif not isinstance(other, Set): 406 | return NotImplemented 407 | return self.symmetric_difference(other) 408 | 409 | __rxor__ = __xor__ 410 | 411 | def times(self: _Self, factor: int) -> _Self: 412 | """Return a new set with each element's multiplicity multiplied with the given scalar factor. 413 | 414 | >>> ms = Multiset('aab') 415 | >>> sorted(ms.times(2)) 416 | ['a', 'a', 'a', 'a', 'b', 'b'] 417 | 418 | You can also use the ``*`` operator for the same effect: 419 | 420 | >>> sorted(ms * 3) 421 | ['a', 'a', 'a', 'a', 'a', 'a', 'b', 'b', 'b'] 422 | 423 | For a variant of the operation which modifies the multiset in place see 424 | :meth:`times_update`. 425 | 426 | Args: 427 | factor: The factor to multiply each multiplicity with. 428 | """ 429 | if factor == 0: 430 | return self.__class__() 431 | if factor < 0: 432 | raise ValueError('The factor must no be negative.') 433 | result = self.__copy__() 434 | _elements = result._elements 435 | for element in _elements: 436 | _elements[element] *= factor 437 | result._total *= factor 438 | return result 439 | 440 | def __mul__(self: _Self, factor: int) -> _Self: 441 | if not isinstance(factor, int): 442 | return NotImplemented 443 | return self.times(factor) 444 | 445 | __rmul__ = __mul__ 446 | 447 | def _issubset(self, other: _OtherType, strict: bool) -> bool: 448 | other = self._as_multiset(other) 449 | self_len = self._total 450 | other_len = len(other) 451 | if self_len > other_len: 452 | return False 453 | if self_len == other_len and strict: 454 | return False 455 | return all(multiplicity <= other[element] for element, multiplicity in self.items()) 456 | 457 | def issubset(self, other: _OtherType) -> bool: 458 | """Return True iff this set is a subset of the other. 459 | 460 | >>> Multiset('ab').issubset('aabc') 461 | True 462 | >>> Multiset('aabb').issubset(Multiset('aabc')) 463 | False 464 | 465 | You can also use the ``<=`` operator for this comparison: 466 | 467 | >>> Multiset('ab') <= Multiset('ab') 468 | True 469 | 470 | When using the ``<`` operator for comparison, the sets are checked 471 | to be unequal in addition: 472 | 473 | >>> Multiset('ab') < Multiset('ab') 474 | False 475 | 476 | Args: 477 | other: The potential superset of the multiset to be checked. 478 | 479 | Returns: 480 | True iff this set is a subset of the other. 481 | """ 482 | return self._issubset(other, False) 483 | 484 | def __le__(self, other: Union[SetType[_TElement], 'BaseMultiset[_TElement]']) -> bool: 485 | if isinstance(other, (BaseMultiset, set, frozenset)): 486 | pass 487 | elif not isinstance(other, Set): 488 | return NotImplemented 489 | return self._issubset(other, False) 490 | 491 | def __lt__(self, other: Union[SetType[_TElement], 'BaseMultiset[_TElement]']) -> bool: 492 | if isinstance(other, (BaseMultiset, set, frozenset)): 493 | pass 494 | elif not isinstance(other, Set): 495 | return NotImplemented 496 | return self._issubset(other, True) 497 | 498 | def _issuperset(self, other: _OtherType, strict: bool) -> bool: 499 | other = self._as_multiset(other) 500 | other_len = len(other) 501 | if len(self) < other_len: 502 | return False 503 | if len(self) == other_len and strict: 504 | return False 505 | for element, multiplicity in other.items(): 506 | if self[element] < multiplicity: 507 | return False 508 | return True 509 | 510 | def issuperset(self, other: _OtherType) -> bool: 511 | """Return True iff this multiset is a superset of the other. 512 | 513 | >>> Multiset('aabc').issuperset('ab') 514 | True 515 | >>> Multiset('aabc').issuperset(Multiset('abcc')) 516 | False 517 | 518 | You can also use the ``>=`` operator for this comparison: 519 | 520 | >>> Multiset('ab') >= Multiset('ab') 521 | True 522 | 523 | When using the ``>`` operator for comparison, the sets are checked 524 | to be unequal in addition: 525 | 526 | >>> Multiset('ab') > Multiset('ab') 527 | False 528 | 529 | Args: 530 | other: The potential subset of the multiset to be checked. 531 | 532 | Returns: 533 | True iff this set is a subset of the other. 534 | """ 535 | return self._issuperset(other, False) 536 | 537 | def __ge__(self, other: Union[SetType[_TElement], 'BaseMultiset[_TElement]']) -> bool: 538 | if isinstance(other, (BaseMultiset, set, frozenset)): 539 | pass 540 | elif not isinstance(other, Set): 541 | return NotImplemented 542 | return self._issuperset(other, False) 543 | 544 | def __gt__(self, other: Union[SetType[_TElement], 'BaseMultiset[_TElement]']) -> bool: 545 | if isinstance(other, (BaseMultiset, set, frozenset)): 546 | pass 547 | elif not isinstance(other, Set): 548 | return NotImplemented 549 | return self._issuperset(other, True) 550 | 551 | def __eq__(self, other: object) -> bool: 552 | if isinstance(other, BaseMultiset): 553 | return self._total == other._total and self._elements == other._elements 554 | if isinstance(other, (set, frozenset)): 555 | pass 556 | elif not isinstance(other, Set): 557 | return NotImplemented 558 | if self._total != len(other): 559 | return False 560 | return self._issubset(other, False) 561 | 562 | def __ne__(self, other: object) -> bool: 563 | if isinstance(other, BaseMultiset): 564 | return self._total != other._total or self._elements != other._elements 565 | if isinstance(other, (set, frozenset)): 566 | pass 567 | elif not isinstance(other, Set): 568 | return NotImplemented 569 | if self._total != len(other): 570 | return True 571 | return not self._issubset(other, False) 572 | 573 | def get(self, element: _TElement, default: int) -> int: 574 | """Return the multiplicity for *element* if it is in the multiset, else *default*. 575 | 576 | Makes the *default* argument of the original :meth:`dict.get` non-optional. 577 | 578 | Args: 579 | element: The element of which to get the multiplicity. 580 | default: The default value to return if the element if not in the multiset. 581 | 582 | Returns: 583 | The multiplicity for *element* if it is in the multiset, else *default*. 584 | """ 585 | return self._elements.get(element, default) 586 | 587 | @classmethod 588 | def from_elements(cls: Type[_Self], elements: IterableType[_TElement], multiplicity: int) -> _Self: 589 | """Create a new multiset with the given *elements* and each multiplicity set to *multiplicity*. 590 | 591 | Uses :meth:`dict.fromkeys` internally. 592 | 593 | Args: 594 | elements: The element for the new multiset. 595 | multiplicity: The multiplicity for all elements. 596 | 597 | Returns: 598 | The new multiset. 599 | """ 600 | return cls(dict.fromkeys(elements, multiplicity)) 601 | 602 | def copy(self: _Self) -> _Self: 603 | """Return a shallow copy of the multiset.""" 604 | return self.__class__(self) 605 | 606 | __copy__ = copy 607 | 608 | def items(self) -> ItemsView[_TElement, int]: 609 | return self._elements.items() 610 | 611 | def distinct_elements(self) -> KeysView[_TElement]: 612 | return self._elements.keys() 613 | 614 | def multiplicities(self) -> ValuesView[int]: 615 | return self._elements.values() 616 | 617 | values = multiplicities 618 | 619 | @classmethod 620 | def _as_multiset(cls, other): 621 | if isinstance(other, BaseMultiset): 622 | return other 623 | if isinstance(other, _all_basic_types): 624 | pass 625 | elif not isinstance(other, Iterable): 626 | raise TypeError("'%s' object is not iterable" % type(other)) # pragma: no cover 627 | return cls(other) 628 | 629 | @staticmethod 630 | def _as_mapping(iterable): 631 | if isinstance(iterable, BaseMultiset): 632 | return iterable._elements 633 | if isinstance(iterable, dict): 634 | return iterable 635 | if isinstance(iterable, _all_basic_types): 636 | pass # create dictionary below 637 | elif isinstance(iterable, Mapping): 638 | return iterable 639 | elif not isinstance(iterable, Iterable): 640 | raise TypeError("'%s' object is not iterable" % type(iterable)) 641 | mapping = dict() 642 | for element in iterable: 643 | if element in mapping: 644 | mapping[element] += 1 645 | else: 646 | mapping[element] = 1 647 | return mapping 648 | 649 | def __getstate__(self): 650 | return self._total, self._elements 651 | 652 | def __setstate__(self, state): 653 | self._total, self._elements = state 654 | 655 | 656 | class Multiset(BaseMultiset[_TElement], MutableMappingType[_TElement, int], Generic[_TElement]): 657 | """The mutable multiset variant.""" 658 | __slots__ = () 659 | 660 | def __setitem__(self, element: _TElement, multiplicity: int): 661 | """Set the element's multiplicity. 662 | 663 | This will remove the element if the multiplicity is less than or equal to zero. 664 | '""" 665 | if not isinstance(multiplicity, int): 666 | raise TypeError('multiplicity must be an integer') 667 | _elements = self._elements 668 | if element in _elements: 669 | old_multiplicity = _elements[element] 670 | if multiplicity > 0: 671 | _elements[element] = multiplicity 672 | self._total += multiplicity - old_multiplicity 673 | else: 674 | del _elements[element] 675 | self._total -= old_multiplicity 676 | elif multiplicity > 0: 677 | _elements[element] = multiplicity 678 | self._total += multiplicity 679 | 680 | def __delitem__(self, element: _TElement): 681 | _elements = self._elements 682 | if element in _elements: 683 | self._total -= _elements[element] 684 | del _elements[element] 685 | else: 686 | raise KeyError("Could not delete {!r} from the multiset, because it is not in it.".format(element)) 687 | 688 | def update(self, *others: _OtherType, **kwargs): 689 | r"""Like :meth:`dict.update` but add multiplicities instead of replacing them. 690 | 691 | >>> ms = Multiset('aab') 692 | >>> ms.update('abc') 693 | >>> sorted(ms) 694 | ['a', 'a', 'a', 'b', 'b', 'c'] 695 | 696 | Note that the operator ``+=`` is equivalent to :meth:`update`, except that the operator will only 697 | accept sets to avoid accidental errors. 698 | 699 | >>> ms += Multiset('bc') 700 | >>> sorted(ms) 701 | ['a', 'a', 'a', 'b', 'b', 'b', 'c', 'c'] 702 | 703 | For a variant of the operation which does not modify the multiset, but returns a new 704 | multiset instead see :meth:`combine`. 705 | 706 | Any keyword arguments are also added to the multiset: 707 | 708 | >>> ms = Multiset('ab') 709 | >>> ms.update(a=1, e=2) 710 | >>> sorted(ms) 711 | ['a', 'a', 'b', 'e', 'e'] 712 | 713 | Args: 714 | others: The other sets to add to this multiset. Can also be any :class:`~typing.Iterable`\[~T] 715 | or :class:`~typing.Mapping`\[~T, :class:`int`] which are then converted to :class:`Multiset`\[~T]. 716 | kwargs: Additional pairs of values and multiplicities to be added to multiset. 717 | """ 718 | _elements = self._elements 719 | _total = self._total 720 | for other in map(self._as_mapping, others + (kwargs, )): 721 | for element, multiplicity in other.items(): 722 | if multiplicity > 0: 723 | old_multiplicity = _elements.get(element, 0) 724 | _elements[element] = multiplicity + old_multiplicity 725 | _total += multiplicity 726 | self._total = _total 727 | 728 | def union_update(self, *others: _OtherType): 729 | r"""Update the multiset, adding elements from all others using the maximum multiplicity. 730 | 731 | >>> ms = Multiset('aab') 732 | >>> ms.union_update('bc') 733 | >>> sorted(ms) 734 | ['a', 'a', 'b', 'c'] 735 | 736 | You can also use the ``|=`` operator for the same effect. However, the operator version 737 | will only accept a set as other operator, not any iterable, to avoid errors. 738 | 739 | >>> ms = Multiset('aab') 740 | >>> ms |= Multiset('bccd') 741 | >>> sorted(ms) 742 | ['a', 'a', 'b', 'c', 'c', 'd'] 743 | 744 | For a variant of the operation which does not modify the multiset, but returns a new 745 | multiset instead see :meth:`union`. 746 | 747 | Args: 748 | others: The other sets to union this multiset with. Can also be any :class:`~typing.Iterable`\[~T] 749 | or :class:`~typing.Mapping`\[~T, :class:`int`] which are then converted to :class:`Multiset`\[~T]. 750 | """ 751 | _elements = self._elements 752 | _total = self._total 753 | for other in map(self._as_mapping, others): 754 | for element, multiplicity in other.items(): 755 | old_multiplicity = _elements.get(element, 0) 756 | if multiplicity > old_multiplicity: 757 | _elements[element] = multiplicity 758 | _total += multiplicity - old_multiplicity 759 | self._total = _total 760 | 761 | def __ior__(self: _Self, other: Union[SetType[_TElement], 'BaseMultiset[_TElement]']) -> _Self: 762 | if isinstance(other, (BaseMultiset, set, frozenset)): 763 | pass 764 | elif not isinstance(other, Set): 765 | return NotImplemented 766 | self.union_update(other) 767 | return self 768 | 769 | def intersection_update(self, *others: _OtherType): 770 | r"""Update the multiset, keeping only elements found in it and all others. 771 | 772 | >>> ms = Multiset('aab') 773 | >>> ms.intersection_update('bc') 774 | >>> sorted(ms) 775 | ['b'] 776 | 777 | You can also use the ``&=`` operator for the same effect. However, the operator version 778 | will only accept a set as other operator, not any iterable, to avoid errors. 779 | 780 | >>> ms = Multiset('aabc') 781 | >>> ms &= Multiset('abbd') 782 | >>> sorted(ms) 783 | ['a', 'b'] 784 | 785 | For a variant of the operation which does not modify the multiset, but returns a new 786 | multiset instead see :meth:`intersection`. 787 | 788 | Args: 789 | others: The other sets to intersect this multiset with. Can also be any :class:`~typing.Iterable`\[~T] 790 | or :class:`~typing.Mapping`\[~T, :class:`int`] which are then converted to :class:`Multiset`\[~T]. 791 | """ 792 | for other in map(self._as_mapping, others): 793 | for element, current_count in list(self.items()): 794 | multiplicity = other.get(element, 0) 795 | if multiplicity < current_count: 796 | self[element] = multiplicity 797 | 798 | def __iand__(self: _Self, other: Union[SetType[_TElement], 'BaseMultiset[_TElement]']) -> _Self: 799 | if isinstance(other, (BaseMultiset, set, frozenset)): 800 | pass 801 | elif not isinstance(other, Set): 802 | return NotImplemented 803 | self.intersection_update(other) 804 | return self 805 | 806 | def difference_update(self, *others: _OtherType): 807 | r"""Remove all elements contained the others from this multiset. 808 | 809 | >>> ms = Multiset('aab') 810 | >>> ms.difference_update('abc') 811 | >>> sorted(ms) 812 | ['a'] 813 | 814 | You can also use the ``-=`` operator for the same effect. However, the operator version 815 | will only accept a set as other operator, not any iterable, to avoid errors. 816 | 817 | >>> ms = Multiset('aabbbc') 818 | >>> ms -= Multiset('abd') 819 | >>> sorted(ms) 820 | ['a', 'b', 'b', 'c'] 821 | 822 | For a variant of the operation which does not modify the multiset, but returns a new 823 | multiset instead see :meth:`difference`. 824 | 825 | Args: 826 | others: The other sets to remove from this multiset. Can also be any :class:`~typing.Iterable`\[~T] 827 | or :class:`~typing.Mapping`\[~T, :class:`int`] which are then converted to :class:`Multiset`\[~T]. 828 | """ 829 | for other in map(self._as_multiset, others): 830 | for element, multiplicity in other.items(): 831 | self.discard(element, multiplicity) 832 | 833 | def __isub__(self: _Self, other: Union[SetType[_TElement], 'BaseMultiset[_TElement]']) -> _Self: 834 | if isinstance(other, (BaseMultiset, set, frozenset)): 835 | pass 836 | elif not isinstance(other, Set): 837 | return NotImplemented 838 | self.difference_update(other) 839 | return self 840 | 841 | def symmetric_difference_update(self, other: _OtherType): 842 | r"""Update the multiset to contain only elements in either this multiset or the other but not both. 843 | 844 | >>> ms = Multiset('aab') 845 | >>> ms.symmetric_difference_update('abc') 846 | >>> sorted(ms) 847 | ['a', 'c'] 848 | 849 | You can also use the ``^=`` operator for the same effect. However, the operator version 850 | will only accept a set as other operator, not any iterable, to avoid errors. 851 | 852 | >>> ms = Multiset('aabbbc') 853 | >>> ms ^= Multiset('abd') 854 | >>> sorted(ms) 855 | ['a', 'b', 'b', 'c', 'd'] 856 | 857 | For a variant of the operation which does not modify the multiset, but returns a new 858 | multiset instead see :meth:`symmetric_difference`. 859 | 860 | Args: 861 | other: The other set to take the symmetric difference with. Can also be any :class:`~typing.Iterable`\[~T] 862 | or :class:`~typing.Mapping`\[~T, :class:`int`] which are then converted to :class:`Multiset`\[~T]. 863 | """ 864 | other = self._as_multiset(other) 865 | elements = set(self.distinct_elements()) | set(other.distinct_elements()) 866 | for element in elements: 867 | multiplicity = self[element] 868 | other_count = other[element] 869 | self[element] = (multiplicity - other_count if multiplicity > other_count else other_count - multiplicity) 870 | 871 | def __ixor__(self: _Self, other: Union[SetType[_TElement], 'BaseMultiset[_TElement]']) -> _Self: 872 | if isinstance(other, (BaseMultiset, set, frozenset)): 873 | pass 874 | elif not isinstance(other, Set): 875 | return NotImplemented 876 | self.symmetric_difference_update(other) 877 | return self 878 | 879 | def times_update(self, factor: int): 880 | """Update each this multiset by multiplying each element's multiplicity with the given scalar factor. 881 | 882 | >>> ms = Multiset('aab') 883 | >>> ms.times_update(2) 884 | >>> sorted(ms) 885 | ['a', 'a', 'a', 'a', 'b', 'b'] 886 | 887 | You can also use the ``*=`` operator for the same effect: 888 | 889 | >>> ms = Multiset('ac') 890 | >>> ms *= 3 891 | >>> sorted(ms) 892 | ['a', 'a', 'a', 'c', 'c', 'c'] 893 | 894 | For a variant of the operation which does not modify the multiset, but returns a new 895 | multiset instead see :meth:`times`. 896 | 897 | Args: 898 | factor: The factor to multiply each multiplicity with. 899 | """ 900 | if factor < 0: 901 | raise ValueError("The factor must not be negative.") 902 | elif factor == 0: 903 | self.clear() 904 | else: 905 | _elements = self._elements 906 | for element in _elements: 907 | _elements[element] *= factor 908 | self._total *= factor 909 | 910 | def __imul__(self: _Self, factor: int) -> _Self: 911 | if not isinstance(factor, int): 912 | raise TypeError("factor must be an integer.") 913 | self.times_update(factor) 914 | return self 915 | 916 | def add(self, element: _TElement, multiplicity: int = 1): 917 | """Adds an element to the multiset. 918 | 919 | >>> ms = Multiset() 920 | >>> ms.add('a') 921 | >>> sorted(ms) 922 | ['a'] 923 | 924 | An optional multiplicity can be specified to define how many of the element are added: 925 | 926 | >>> ms.add('b', 2) 927 | >>> sorted(ms) 928 | ['a', 'b', 'b'] 929 | 930 | This extends the :meth:`MutableSet.add` signature to allow specifying the multiplicity. 931 | 932 | Args: 933 | element: 934 | The element to add to the multiset. 935 | multiplicity: 936 | The multiplicity i.e. count of elements to add. 937 | """ 938 | if multiplicity < 1: 939 | raise ValueError("Multiplicity must be positive") 940 | self._elements[element] += multiplicity 941 | self._total += multiplicity 942 | 943 | def remove(self, element: _TElement, multiplicity: Optional[int] = None) -> int: 944 | """Removes an element from the multiset. 945 | 946 | If no multiplicity is specified, the element is completely removed from the multiset: 947 | 948 | >>> ms = Multiset('aabbbc') 949 | >>> ms.remove('a') 950 | 2 951 | >>> sorted(ms) 952 | ['b', 'b', 'b', 'c'] 953 | 954 | If the multiplicity is given, it is subtracted from the element's multiplicity in the multiset: 955 | 956 | >>> ms.remove('b', 2) 957 | 3 958 | >>> sorted(ms) 959 | ['b', 'c'] 960 | 961 | It is not an error to remove more elements than are in the set: 962 | 963 | >>> ms.remove('b', 2) 964 | 1 965 | >>> sorted(ms) 966 | ['c'] 967 | 968 | This extends the :meth:`MutableSet.remove` signature to allow specifying the multiplicity. 969 | 970 | Args: 971 | element: 972 | The element to remove from the multiset. 973 | multiplicity: 974 | An optional multiplicity i.e. count of elements to remove. 975 | 976 | Returns: 977 | The multiplicity of the element in the multiset before 978 | the removal. 979 | 980 | Raises: 981 | KeyError: if the element is not contained in the set. Use :meth:`discard` if 982 | you do not want an exception to be raised. 983 | """ 984 | _elements = self._elements 985 | if element not in _elements: 986 | raise KeyError 987 | old_multiplicity = _elements.get(element, 0) 988 | if multiplicity is None or multiplicity >= old_multiplicity: 989 | del _elements[element] 990 | self._total -= old_multiplicity 991 | elif multiplicity < 0: 992 | raise ValueError("Multiplicity must be not be negative") 993 | elif multiplicity > 0: 994 | _elements[element] -= multiplicity 995 | self._total -= multiplicity 996 | return old_multiplicity 997 | 998 | def discard(self, element: _TElement, multiplicity: Optional[int] = None) -> int: 999 | """Removes the `element` from the multiset. 1000 | 1001 | If multiplicity is ``None``, all occurrences of the element are removed: 1002 | 1003 | >>> ms = Multiset('aab') 1004 | >>> ms.discard('a') 1005 | 2 1006 | >>> sorted(ms) 1007 | ['b'] 1008 | 1009 | Otherwise, the multiplicity is subtracted from the one in the multiset and the 1010 | old multiplicity is removed: 1011 | 1012 | >>> ms = Multiset('aab') 1013 | >>> ms.discard('a', 1) 1014 | 2 1015 | >>> sorted(ms) 1016 | ['a', 'b'] 1017 | 1018 | In contrast to :meth:`remove`, this does not raise an error if the 1019 | element is not in the multiset: 1020 | 1021 | >>> ms = Multiset('a') 1022 | >>> ms.discard('b') 1023 | 0 1024 | >>> sorted(ms) 1025 | ['a'] 1026 | 1027 | It is also not an error to remove more elements than are in the set: 1028 | 1029 | >>> ms.remove('a', 2) 1030 | 1 1031 | >>> sorted(ms) 1032 | [] 1033 | 1034 | Args: 1035 | element: 1036 | The element to remove from the multiset. 1037 | multiplicity: 1038 | An optional multiplicity i.e. count of elements to remove. 1039 | 1040 | Returns: 1041 | The multiplicity of the element in the multiset before 1042 | the removal. 1043 | """ 1044 | _elements = self._elements 1045 | if element in _elements: 1046 | old_multiplicity = _elements[element] 1047 | if multiplicity is None or multiplicity >= old_multiplicity: 1048 | del _elements[element] 1049 | self._total -= old_multiplicity 1050 | elif multiplicity < 0: 1051 | raise ValueError("Multiplicity must not be negative") 1052 | elif multiplicity > 0: 1053 | _elements[element] -= multiplicity 1054 | self._total -= multiplicity 1055 | return old_multiplicity 1056 | else: 1057 | return 0 1058 | 1059 | def pop(self, element: _TElement, default: int) -> int: 1060 | """If *element* is in the multiset, remove it and return its multiplicity, else return *default*. 1061 | 1062 | Makes the *default* argument of the original :meth:`dict.pop` non-optional. 1063 | 1064 | Args: 1065 | element: The element which is removed. 1066 | default: The default value to return if the element if not in the multiset. 1067 | 1068 | Returns: 1069 | The multiplicity for *element* if it is in the multiset, else *default*. 1070 | """ 1071 | rm_size = self._elements.get(element) 1072 | if rm_size is not None: 1073 | self._total -= rm_size 1074 | return self._elements.pop(element, default) 1075 | 1076 | def setdefault(self, element: _TElement, default: int) -> int: 1077 | """If *element* is in the multiset, return its multiplicity. 1078 | Else add it with a multiplicity of *default* and return *default*. 1079 | 1080 | Makes the *default* argument of the original :meth:`dict.setdefault` non-optional. 1081 | 1082 | Args: 1083 | element: The element which is added if not already present. 1084 | default: The default multiplicity to add the element with if not in the multiset. 1085 | 1086 | Returns: 1087 | The multiplicity for *element* if it is in the multiset, else *default*. 1088 | """ 1089 | 1090 | mul = self._elements.get(element) 1091 | if mul is None: 1092 | if default < 1: 1093 | raise ValueError("Multiplicity must be positive") 1094 | self._total += default 1095 | return self._elements.setdefault(element, default) 1096 | 1097 | def clear(self): 1098 | """Empty the multiset.""" 1099 | self._elements.clear() 1100 | self._total = 0 1101 | 1102 | 1103 | class FrozenMultiset(BaseMultiset[_TElement], Generic[_TElement]): 1104 | """The frozen multiset variant that is immutable and hashable.""" 1105 | __slots__ = () 1106 | 1107 | def __hash__(self): 1108 | return hash(frozenset(self._elements.items())) 1109 | 1110 | 1111 | Mapping.register(BaseMultiset) # type: ignore 1112 | MutableMapping.register(Multiset) # type: ignore 1113 | 1114 | if __name__ == '__main__': 1115 | import doctest 1116 | doctest.testmod() 1117 | --------------------------------------------------------------------------------