├── 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 |
--------------------------------------------------------------------------------