pax_global_header00006660000000000000000000000064143541635460014525gustar00rootroot0000000000000052 comment=da526c4c450287466c15e5e914e0948ac4cf00a1 bidict-0.22.1/000077500000000000000000000000001435416354600130455ustar00rootroot00000000000000bidict-0.22.1/.coveragerc000066400000000000000000000002611435416354600151650ustar00rootroot00000000000000[run] branch = True source = bidict dynamic_context = test_function [report] precision = 1 exclude_lines = pragma: no cover def __repr__ @.*overload if .*TYPE_CHECKING bidict-0.22.1/.devcontainer/000077500000000000000000000000001435416354600156045ustar00rootroot00000000000000bidict-0.22.1/.devcontainer/Dockerfile000066400000000000000000000033621435416354600176020ustar00rootroot00000000000000# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.223.0/containers/python-3/.devcontainer/base.Dockerfile # [Choice] Python version: 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster # Note: If on arm64/Apple Silicon, must use -bullseye variant. ARG VARIANT="3.10-bullseye" FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 ARG NODE_VERSION="none" RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi # Install dev dependencies globally in the image. # (Still easy to experiment with upgrades/new dependencies by installing them in --user scope, # and if you want to keep them, add them to the requirements file and rebuild the container.) COPY dev-deps/*.txt /tmp/bidict-dev-deps/ RUN pip3 --disable-pip-version-check install -U pip && \ pip3 --disable-pip-version-check --no-cache-dir install \ -r /tmp/bidict-dev-deps/test.txt \ -r /tmp/bidict-dev-deps/lint.txt \ -r /tmp/bidict-dev-deps/docs.txt \ -r /tmp/bidict-dev-deps/dev.txt \ && rm -rf /tmp/bidict-dev-deps # [Optional] Install additional OS packages. RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ && apt-get -y install --no-install-recommends \ graphviz optipng `# for docs/_static/build-bidict-types-diagram.sh` \ fish `# make fish shell available for development` # [Optional] Uncomment this line to install global node packages. # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 bidict-0.22.1/.devcontainer/devcontainer.json000066400000000000000000000052361435416354600211660ustar00rootroot00000000000000/* vim: set ft=json5: */ // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: // https://github.com/microsoft/vscode-dev-containers/tree/v0.223.0/containers/python-3 { "name": "Python 3", "build": { "dockerfile": "Dockerfile", "context": "..", // ARGS set in ./Dockerfile can be overridden here: "args": {} }, // Default, container-specific settings here are set on container create. // Note: The project's .vscode/settings.json is still the right place // for non-container-specific settings. "settings": { "python.defaultInterpreterPath": "/usr/local/bin/python", "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", "python.formatting.blackPath": "/usr/local/py-utils/bin/black", "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint", "esbonio.sphinx.confDir": "${workspaceFolder}/docs", "restructuredtext.pythonRecommendation.disabled": true, "terminal.integrated.defaultProfile.linux": "fish" }, // Add the IDs of extensions you want installed when the container is created. "extensions": [ "EditorConfig.EditorConfig", "GitHub.vscode-pull-request-github", "charliermarsh.ruff", "eamodio.gitlens", "joaompinto.vscode-graphviz", "lextudio.restructuredtext", "ms-python.python", "ms-python.vscode-pylance", "mutantdino.resource-monitor", "tamasfe.even-better-toml", "trond-snekvik.simple-rst" ], // Use 'forwardPorts' to make a list of ports inside the container available locally by default. // (Even without this, by default VS Code will notice when a process starts listening on an // unforwarded port, and will start forwarding that port automatically. Ref: remote.autoForwardPorts) // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. // Note: esbonio and rstcheck are just for the restructuredtext vscode extension, // so 'pip install' them with --user in a postCreateCommand: "postCreateCommand": "pip3 install --user esbonio rstcheck", // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "vscode", "features": { "git": "os-provided", "github-cli": "latest" } } bidict-0.22.1/.editorconfig000066400000000000000000000002631435416354600155230ustar00rootroot00000000000000# http://EditorConfig.org # top-most EditorConfig file root = true [*] end_of_line = lf insert_final_newline = true indent_style = space indent_size = 2 [*.py] indent_size = 4 bidict-0.22.1/.envrc000066400000000000000000000000321435416354600141560ustar00rootroot00000000000000source .venv/bin/activate bidict-0.22.1/.github/000077500000000000000000000000001435416354600144055ustar00rootroot00000000000000bidict-0.22.1/.github/FUNDING.yml000066400000000000000000000013271435416354600162250ustar00rootroot00000000000000github: "jab" custom: - https://gumroad.com/l/bidict - https://www.paypal.com/cgi-bin/webscr?cmd=_xclick&business=jabronson%40gmail%2ecom&lc=US&item_name=Support%20bidict&button_subtype=services¤cy_code=USD&bn=PP%2dBuyNowBF%3aPaypal%2dBuy%2520a%2520Drink%2dblue%2esvg%3aNonHosted tidelift: "pypi/bidict" patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username bidict-0.22.1/.github/dependabot.yml000066400000000000000000000006571435416354600172450ustar00rootroot00000000000000# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: - package-ecosystem: "pip" open-pull-requests-limit: 1 directory: "/requirements" schedule: interval: "monthly" - package-ecosystem: "github-actions" open-pull-requests-limit: 1 directory: "/.github/workflows" schedule: interval: "monthly" bidict-0.22.1/.github/workflows/000077500000000000000000000000001435416354600164425ustar00rootroot00000000000000bidict-0.22.1/.github/workflows/lint.yml000066400000000000000000000012501435416354600201310ustar00rootroot00000000000000name: Lint "on": push: branches: - main - dev - deps pull_request: branches: - main workflow_dispatch: jobs: main: runs-on: ubuntu-latest steps: - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b - uses: actions/setup-python@5ccb29d8773c3f3f653e1705f474dfaa8a06a912 with: python-version: '3.11' - run: python -m pip install -U pip setuptools wheel - name: install dependencies run: pip install -r dev-deps/lint.txt - uses: pre-commit/action@646c83fcd040023954eafda54b4db0192ce70507 with: extra_args: --verbose permissions: contents: read bidict-0.22.1/.github/workflows/release-to-pypi.yml000066400000000000000000000012011435416354600221760ustar00rootroot00000000000000name: Release to PyPI "on": push: tags: - "v[0-9]+.[0-9]+.[0-9]+" jobs: main: runs-on: ubuntu-latest steps: - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b - uses: actions/setup-python@5ccb29d8773c3f3f653e1705f474dfaa8a06a912 with: python-version: '3.11' - run: python -m pip install -U pip setuptools build - run: python -m build - name: Publish uses: pypa/gh-action-pypi-publish@c7f29f7adef1a245bd91520e94867e5c6eedddcc with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} permissions: contents: read bidict-0.22.1/.github/workflows/release-to-test-pypi.yml000066400000000000000000000023651435416354600231670ustar00rootroot00000000000000name: Release to Test PyPI "on": workflow_dispatch: push: tags: # To publish a test release to test.pypi.org, # create and push a tag as follows: # git tag -a 0.21.3.rc1 -m "Tag 0.21.3.rc1 for release to test.pypi.org" # git push --tags # Go to https://github.com/jab/bidict/actions?query=workflow%3A%22Release+to+Test+PyPI%22 # and watch for a new run of this workflow to publish to test.pypi.org. # IMPORTANT: Run the following to clean up after: # git tag -d 0.21.3.rc1 # git push origin :0.21.3.rc1 - "[0-9]+.[0-9]+.[0-9]+.rc[0-9]+" jobs: main: runs-on: ubuntu-latest steps: - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b - uses: actions/setup-python@5ccb29d8773c3f3f653e1705f474dfaa8a06a912 with: python-version: '3.11' - run: python -m pip install -U pip setuptools build - run: python -m build - name: Publish uses: pypa/gh-action-pypi-publish@c7f29f7adef1a245bd91520e94867e5c6eedddcc with: user: __token__ password: ${{ secrets.TEST_PYPI_API_TOKEN }} repository_url: https://test.pypi.org/legacy/ verbose: true permissions: contents: read bidict-0.22.1/.github/workflows/test.yml000066400000000000000000000044041435416354600201460ustar00rootroot00000000000000name: Test "on": workflow_dispatch: schedule: - cron: "15 16 * * *" push: branches: - main - dev - deps pull_request: branches: - main env: FORCE_COLOR: "1" jobs: main: name: ${{ matrix.pyversion }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: # https://github.com/actions/setup-python/blob/main/docs/advanced-usage.md#available-versions-of-python-and-pypy - pyversion: "3.11" enable_coverage: true - pyversion: "3.10" more_hypothesis_examples_if_cron: true - pyversion: "3.9" - pyversion: "3.8" - pyversion: "3.7" - pyversion: pypy-3.9 - pyversion: pypy-3.8 - pyversion: pypy-3.7 steps: - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b - uses: actions/setup-python@5ccb29d8773c3f3f653e1705f474dfaa8a06a912 with: python-version: ${{ matrix.pyversion }} cache: pip cache-dependency-path: dev-deps/test.txt - name: Set --hypothesis-profile=more-examples # See tests/conftest.py if: ${{ github.event_name == 'schedule' && matrix.more_hypothesis_examples_if_cron }} run: | echo PYTEST_ADDOPTS="${PYTEST_ADDOPTS} --hypothesis-profile=more-examples" >> "${GITHUB_ENV}" - run: python -m pip install -U pip setuptools wheel tox==4.0.16 - name: cache .hypothesis dir uses: actions/cache@c1a5de879eb890d062a85ee0252d6036480b1fe2 with: path: .hypothesis key: hypothesis|${{ runner.os }}|${{ matrix.pyversion }} - name: Configure pytest to enable coverage if: matrix.enable_coverage run: | echo PYTEST_ADDOPTS="${PYTEST_ADDOPTS} --cov=bidict --cov-config=.coveragerc --cov-report=xml" >> "${GITHUB_ENV}" - run: tox -e py - name: Upload coverage to Codecov # https://github.com/codecov/codecov-action uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 if: matrix.enable_coverage with: verbose: true files: ./coverage.xml fail_ci_if_error: false # https://github.com/codecov/codecov-action/issues/557 permissions: contents: read bidict-0.22.1/.github/workflows/update_actions.yml000066400000000000000000000012501435416354600221650ustar00rootroot00000000000000# https://github.com/marketplace/actions/github-actions-version-updater name: Update GitHub Actions "on": workflow_dispatch: schedule: # First day of each month at noon - cron: "0 12 1 * *" jobs: main: runs-on: ubuntu-latest steps: - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b with: token: ${{ secrets.ACTIONS_VERSION_UPDATER_TOKEN }} - uses: saadmk11/github-actions-version-updater@5d7d1286cb239c77a611861d710cfffaeea05fd2 with: token: ${{ secrets.ACTIONS_VERSION_UPDATER_TOKEN }} update_version_with: release-commit-sha permissions: contents: read pull-requests: write bidict-0.22.1/.gitignore000066400000000000000000000002731435416354600150370ustar00rootroot00000000000000*.pyc .DS_Store .benchmarks .cache .coverage* .eggs .hypothesis .idea .mypy_cache .tox .devenv .venv __pycache__ _build bidict.egg-info build coverage.xml dist htmlcov pip-wheel-metadata bidict-0.22.1/.gitpod.yml000066400000000000000000000021651435416354600151400ustar00rootroot00000000000000# https://www.gitpod.io/docs/prebuilds github: prebuilds: # enable for the master/default branch (defaults to true) master: true # enable for all branches in this repo (defaults to false) branches: true # enable for pull requests coming from this repo (defaults to true) pullRequests: true # enable for pull requests coming from forks (defaults to false) pullRequestsFromForks: true # add a "Review in Gitpod" button as a comment to pull requests (defaults to true) addComment: true # add a "Review in Gitpod" badge to pull requests (defaults to false) addBadge: false # add a label once the prebuild is ready to pull requests (defaults to false) addLabel: prebuilt-in-gitpod image: gitpod/workspace-python:latest # https://www.gitpod.io/docs/languages/python tasks: - init: | pyenv install 3.10.2 pyenv global 3.10.2 pip install -U pip pip install -r requirements/dev.txt pre-commit install --install-hooks vscode: extensions: - ms-python.python - EditorConfig.EditorConfig - lextudio.restructuredtext - trond-snekvik.simple-rst bidict-0.22.1/.lgtm.yml000066400000000000000000000002241435416354600146070ustar00rootroot00000000000000queries: - exclude: py/missing-equals - exclude: py/conflicting-attributes - exclude: py/unused-import - exclude: py/import-and-import-from bidict-0.22.1/.pre-commit-config.yaml000066400000000000000000000017521435416354600173330ustar00rootroot00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: mixed-line-ending - id: check-toml - id: check-yaml - repo: https://gitlab.com/bmares/check-json5 rev: v1.0.0 hooks: - id: check-json5 - repo: https://github.com/Lucas-C/pre-commit-hooks rev: v1.3.1 hooks: - id: forbid-crlf - id: remove-crlf - id: forbid-tabs - id: remove-tabs - repo: https://github.com/codespell-project/codespell rev: v2.2.2 hooks: - id: codespell args: ["--uri-ignore-words-list", "*"] # https://github.com/codespell-project/codespell/issues/2473 - repo: https://github.com/pre-commit/mirrors-mypy rev: v0.991 hooks: - id: mypy additional_dependencies: - hypothesis - pytest - repo: https://github.com/charliermarsh/ruff-pre-commit rev: v0.0.192 hooks: - id: ruff bidict-0.22.1/.pydocstyle.ini000066400000000000000000000006751435416354600160330ustar00rootroot00000000000000# https://pydocstyle.readthedocs.io/en/latest/snippets/config.html [pydocstyle] ignore_decorators = (overload|property) add_ignore = D107, # missing docstring in __init__ D105, # missing docstring in magic method D205, # 1 blank line required between summary line and description D400, # first line should end with a period D401, # first line should be in imperative mood D402, # first line should be the function's signature bidict-0.22.1/.readthedocs.yml000066400000000000000000000004141435416354600161320ustar00rootroot00000000000000--- # https://docs.readthedocs.io/en/latest/config-file/ version: 2 formats: - htmlzip build: os: "ubuntu-22.04" tools: python: "3.11" python: install: - requirements: dev-deps/docs.txt sphinx: configuration: docs/conf.py fail_on_warning: true bidict-0.22.1/.vscode/000077500000000000000000000000001435416354600144065ustar00rootroot00000000000000bidict-0.22.1/.vscode/extensions.json000066400000000000000000000005141435416354600175000ustar00rootroot00000000000000// vim: set ft=json5 { // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. "recommendations": [ "charliermarsh.ruff", // "tamasfe.even-better-toml", // "lextudio.restructuredtext", // "trond-snekvik.simple-rst", ], "unwantedRecommendations": [], } bidict-0.22.1/.vscode/settings.json000066400000000000000000000004601435416354600171410ustar00rootroot00000000000000// vim: set ft=json5 { "files.insertFinalNewline": true, "files.trimFinalNewlines": true, "files.trimTrailingWhitespace": true, // Prefer mypy over Pyright: "python.analysis.typeCheckingMode": "off", "python.linting.mypyEnabled": true, "python.testing.pytestEnabled": true, } bidict-0.22.1/.vscode/tasks.json000066400000000000000000000050211435416354600164240ustar00rootroot00000000000000{ "version": "2.0.0", "tasks": [ { "label": "Run tox (all targets)", "type": "process", "command": "tox", "group": "test", "presentation": { "echo": true, "reveal": "always", "focus": false, "panel": "dedicated", "showReuseMessage": false, "clear": false } }, { "label": "Lint", "type": "process", "command": "pre-commit", "args": [ "run", "--all-files" ], "group": "test", "presentation": { "echo": true, "reveal": "always", "focus": false, "panel": "dedicated", "showReuseMessage": false, "clear": false } }, { "label": "Run tests", "type": "process", "command": "python", "args": [ "run_tests.py" ], "isTestCommand": true, "presentation": { "echo": true, "reveal": "always", "focus": false, "panel": "dedicated", "showReuseMessage": false, "clear": false } }, { "label": "Build docs", "type": "process", "command": "tox", "args": [ "-e", "docs" ], "group": "build", "presentation": { "echo": true, "reveal": "silent", "focus": false, "panel": "dedicated", "showReuseMessage": false, "clear": true } }, { "label": "Host docs", "type": "process", "command": "python", "args": [ "-m", "http.server" ], "isBackground": true, "options": { "cwd": "${workspaceFolder}/docs/_build/html" }, "dependsOn": "Build docs", "presentation": { "echo": true, "reveal": "silent", "focus": false, "panel": "dedicated", "showReuseMessage": false, "clear": true }, "problemMatcher": [] } ] } bidict-0.22.1/CHANGELOG.rst000066400000000000000000001227021435416354600150720ustar00rootroot00000000000000.. Forward declarations for all the custom interpreted text roles that Sphinx defines and that are used below. This helps Sphinx-unaware tools (e.g. rst2html, PyPI's and GitHub's renderers, etc.). .. role:: doc .. role:: ref Changelog ========= .. image:: https://img.shields.io/badge/GitHub-sponsor-ff69b4 :target: https://github.com/sponsors/jab :alt: Sponsor through GitHub .. image:: https://img.shields.io/github/sponsors/jab :target: https://github.com/sponsors/jab :alt: Sponsors on GitHub :sup:`If you or your organization depends on bidict, please consider sponsoring bidict on GitHub.` .. tip:: Watch `bidict releases on GitHub `__ to be notified when new versions of bidict are published. Click the "Watch" dropdown, choose "Custom", and then choose "Releases". 0.22.1 (2022-12-31) ------------------- - Only include the source code in the source distribution. This reduces the size of the source distribution from ~200kB to ~30kB. - Fix the return type hint of :func:`bidict.inverted` to return an :class:`~collections.abc.Iterator`, rather than an :class:`~collections.abc.Iterable`. 0.22.0 (2022-03-23) ------------------- - Drop support for Python 3.6, which reached end of life on 2021-12-23 and is no longer supported by pip as of pip version 22. Take advantage of this to reduce bidict's maintenance costs. - Use mypy-appeasing explicit re-exports in ``__init__.py`` (e.g. ``import x as x``) so that mypy no longer gives you an implicit re-export error if you run it with ``--no-implicit-reexport`` (or ``--strict``) against code that imports from :mod:`bidict`. - Update the implementations and type annotations of :meth:`bidict.BidictBase.keys` and :meth:`bidict.BidictBase.values` to make use of the new :class:`~bidict.BidictKeysView` type, which works a bit better with type checkers. - Inverse bidict instances are now computed lazily the first time the :attr:`~bidict.BidictBase.inverse` attribute is accessed rather than being computed eagerly during initialization. (A bidict's backing, inverse, one-way mapping is still kept in sync eagerly as any mutations are made, to preserve key- and value-uniqueness.) - Optimize initializing a bidict with another bidict. In a microbenchmark on Python 3.10, this now performs over **2x faster**. - Optimize updating an empty bidict with another bidict. In a microbenchmark on Python 3.10, this now performs **60-75% faster**. - Optimize :meth:`~bidict.BidictBase.copy`. In a microbenchmark on Python 3.10, this now performs **10-20x faster**. - Optimize rolling back :ref:`failed updates to a bidict ` in the case that the number of items passed to the update call can be determined to be larger than the bidict being updated. Previously this rollback was O(n) in the number of items passed. Now it is O(1), i.e. **unboundedly faster**. - Optimize :meth:`bidict.BidictBase.__contains__` (the method called when you run ``key in mybidict``). In a microbenchmark on Python 3.10, this now performs over **3-10x faster** in the False case, and at least **50% faster** in the True case. - Optimize :meth:`bidict.BidictBase.__eq__` (the method called when you run ``mybidict == other``). In a microbenchmark on Python 3.10, this now performs **15-25x faster** for ordered bidicts, and **7-12x faster** for unordered bidicts. - Optimize :meth:`~bidict.BidictBase.equals_order_sensitive`. In a microbenchmark on Python 3.10, this now performs **2x faster** for ordered bidicts and **60-90% faster** for unordered bidicts. - Optimize the :class:`~collections.abc.MappingView` objects returned by :meth:`bidict.OrderedBidict.keys`, :meth:`bidict.OrderedBidict.values`, and :meth:`bidict.OrderedBidict.items` to delegate to backing ``dict_keys`` and ``dict_items`` objects if available, which are much faster in CPython. For example, in a microbenchmark on Python 3.10, ``orderedbi.items() == d.items()`` now performs **30-50x faster**. - Fix a bug where :meth:`bidict.BidictBase.__eq__` was always returning False rather than :obj:`NotImplemented` in the case that the argument was not a :class:`~collections.abc.Mapping`, defeating the argument's own ``__eq__()`` if implemented. As a notable example, bidicts now correctly compare equal to :obj:`unittest.mock.ANY`. - :class:`bidict.BidictBase` now adds a ``__reversed__`` implementation to subclasses that don't have an overridden implementation depending on whether both their backing mappings are :class:`~collections.abc.Reversible`. Previously, a ``__reversed__`` implementation was only added to :class:`~bidict.BidictBase` when ``BidictBase._fwdm_cls`` was :class:`~collections.abc.Reversible`. So if a :class:`~bidict.BidictBase` subclass set its ``_fwdm_cls`` to a non-reversible mutable mapping, it would also have to manually set its ``__reversed__`` attribute to None to override the implementation inherited from :class:`~bidict.BidictBase`. This is no longer necessary thanks to bidict's new :meth:`object.__init_subclass__` logic. - The :class:`~collections.abc.MappingView` objects returned by :meth:`bidict.OrderedBidict.keys`, :meth:`bidict.OrderedBidict.values`, and :meth:`bidict.OrderedBidict.items` are now :class:`~collections.abc.Reversible`. (This was already the case for unordered bidicts when running on Python 3.8+.) - Add support for Python 3.9-style dict merge operators (`PEP 584 `__). See `the tests `__ for examples. - Update docstrings for :meth:`bidict.BidictBase.keys`, :meth:`bidict.BidictBase.values`, and :meth:`bidict.BidictBase.items` to include more details. - :func:`~bidict.namedbidict` now exposes the passed-in *keyname* and *valname* in the corresponding properties on the generated class. - :func:`~bidict.namedbidict` now requires *base_type* to be a subclass of :class:`~bidict.BidictBase`, but no longer requires *base_type* to provide an ``_isinv`` attribute, which :class:`~bidict.BidictBase` subclasses no longer provide. - When attempting to pickle a bidict's inverse whose class was :ref:`dynamically generated `, and no reference to the dynamically-generated class has been stored anywhere in :data:`sys.modules` where :mod:`pickle` can find it, the pickle call is now more likely to succeed rather than failing with a :class:`~pickle.PicklingError`. - Remove the use of slots from (non-ABC) bidict types. This better matches the mapping implementations in Python's standard library, and significantly reduces code complexity and maintenance burden. The memory savings conferred by using slots are not noticeable unless you're creating millions of bidict instances anyway, which is an extremely unusual usage pattern. Of course, bidicts can still contain millions (or more) items (which is not an unusual usage pattern) without using any more memory than before these changes. Notably, slots are still used in the internal linked list nodes of ordered bidicts to save memory, since as many node instances are created as there are items inserted. 0.21.4 (2021-10-23) ------------------- Explicitly declare support for Python 3.10 as well as some minor internal improvements. 0.21.3 (2021-09-05) ------------------- - All bidicts now provide the :meth:`~bidict.BidictBase.equals_order_sensitive` method, not just :class:`~bidict.OrderedBidict`\s. Since support for Python < 3.6 was dropped in v0.21.0, :class:`dict`\s provide a deterministic ordering on all supported Python versions, and as a result, all bidicts do too. So now even non-:class:`Ordered ` bidicts might as well provide :meth:`~bidict.BidictBase.equals_order_sensitive`. See the updated :ref:`other-bidict-types:What about order-preserving dicts?` docs for more info. - Take better advantage of the fact that dicts became :class:`reversible ` in Python 3.8. Specifically, now even non-:class:`Ordered ` bidicts provide a :meth:`~bidict.BidictBase.__reversed__` implementation on Python 3.8+ that calls :func:`reversed` on the backing ``_fwdm`` mapping. As a result, if you are using Python 3.8+, :class:`~bidict.frozenbidict` now gives you everything that :class:`~bidict.FrozenOrderedBidict` gives you, but with less space overhead. - Drop `setuptools_scm `__ as a ``setup_requires`` dependency. - Remove the ``bidict.__version_info__`` attribute. 0.21.2 (2020-09-07) ------------------- - Include `py.typed `__ file to mark :mod:`bidict` as type hinted. 0.21.1 (2020-09-07) ------------------- This release was yanked and replaced with the 0.21.2 release, which actually provides the intended changes. 0.21.0 (2020-08-22) ------------------- - :mod:`bidict` now provides `type hints `__! ⌨️ ✅ Adding type hints to :mod:`bidict` poses particularly interesting challenges due to the combination of generic types, dynamically-generated types (such as :ref:`inverse bidict classes ` and :func:`namedbidicts `), and complicating optimizations such as the use of slots and weakrefs. It didn't take long to hit bugs and missing features in the state of the art for type hinting in Python today, e.g. missing higher-kinded types support (`python/typing#548 `__), too-narrow type hints for :class:`collections.abc.Mapping` (`python/typeshed#4435 `__), a :class:`typing.Generic` bug in Python 3.6 (`BPO-41451 `__), etc. That said, this release should provide a solid foundation for code using :mod:`bidict` that enables static type checking. As always, if you spot any opportunities to improve :mod:`bidict` (including its new type hints), please don't hesitate to submit a PR! - Add :class:`bidict.MutableBidirectionalMapping` ABC. The :ref:`other-bidict-types:Bidict Types Diagram` has been updated accordingly. - Drop support for Python 3.5, which reaches end of life on 2020-09-13, represents a tiny percentage of bidict downloads on `PyPI Stats `__, and lacks support for `variable type hint syntax `__, `ordered dicts `__, and :attr:`object.__init_subclass__`. - Remove the no-longer-needed ``bidict.compat`` module. - Move :ref:`inverse bidict class access ` from a property to an attribute set in :attr:`~bidict.BidictBase.__init_subclass__`, to save function call overhead on repeated access. - :meth:`bidict.OrderedBidictBase.__iter__` no longer accepts a ``reverse`` keyword argument so that it matches the signature of :meth:`container.__iter__`. - Set the ``__module__`` attribute of various :mod:`bidict` types (using :func:`sys._getframe` when necessary) so that private, internal modules are not exposed e.g. in classes' repr strings. - :func:`~bidict.namedbidict` now immediately raises :class:`TypeError` if the provided ``base_type`` does not provide ``_isinv`` or :meth:`~object.__getstate__`, rather than succeeding with a class whose instances may raise :class:`AttributeError` when these attributes are accessed. 0.20.0 (2020-07-23) ------------------- The following breaking changes are expected to affect few if any users. Remove APIs deprecated in the previous release: - ``bidict.OVERWRITE`` and ``bidict.IGNORE``. - The ``on_dup_key``, ``on_dup_val``, and ``on_dup_kv`` arguments of :meth:`~bidict.bidict.put` and :meth:`~bidict.bidict.putall`. - The ``on_dup_key``, ``on_dup_val``, and ``on_dup_kv`` :class:`~bidict.bidict` class attributes. - Remove :meth:`bidict.BidirectionalMapping.__subclasshook__` due to lack of use and maintenance cost. Fixes a bug introduced in 0.15.0 that caused any class with an ``inverse`` attribute to be incorrectly considered a subclass of :class:`collections.abc.Mapping`. :issue:`111` 0.19.0 (2020-01-09) ------------------- - Drop support for Python 2 :ref:`as promised in v0.18.2 `. The ``bidict.compat`` module has been pruned accordingly. This makes bidict more efficient on Python 3 and enables further improvement to bidict in the future. - Deprecate ``bidict.OVERWRITE`` and ``bidict.IGNORE``. A :class:`UserWarning` will now be emitted if these are used. :attr:`bidict.DROP_OLD` and :attr:`bidict.DROP_NEW` should be used instead. - Rename ``DuplicationPolicy`` to :class:`~bidict.OnDupAction` (and implement it via an :class:`~enum.Enum`). An :class:`~bidict.OnDupAction` may be one of :attr:`~bidict.RAISE`, :attr:`~bidict.DROP_OLD`, or :attr:`~bidict.DROP_NEW`. - Expose the new :class:`~bidict.OnDup` class to contain the three :class:`~bidict.OnDupAction`\s that should be taken upon encountering the three kinds of duplication that can occur (*key*, *val*, *kv*). - Provide the :attr:`~bidict.ON_DUP_DEFAULT`, :attr:`~bidict.ON_DUP_RAISE`, and :attr:`~bidict.ON_DUP_DROP_OLD` :class:`~bidict.OnDup` convenience instances. - Deprecate the ``on_dup_key``, ``on_dup_val``, and ``on_dup_kv`` arguments of :meth:`~bidict.bidict.put` and :meth:`~bidict.bidict.putall`. A :class:`UserWarning` will now be emitted if these are used. These have been subsumed by the new *on_dup* argument, which takes an :class:`~bidict.OnDup` instance. Use it like this: ``bi.put(1, 2, OnDup(key=RAISE, val=...))``. Or pass one of the instances already provided, such as :attr:`~bidict.ON_DUP_DROP_OLD`. Or just don't pass an *on_dup* argument to use the default value of :attr:`~bidict.ON_DUP_RAISE`. The :ref:`basic-usage:Values Must Be Unique` docs have been updated accordingly. - Deprecate the ``on_dup_key``, ``on_dup_val``, and ``on_dup_kv`` :class:`~bidict.bidict` class attributes. A :class:`UserWarning` will now be emitted if these are used. These have been subsumed by the new :attr:`~bidict.bidict.on_dup` class attribute, which takes an :class:`~bidict.OnDup` instance. See the updated :doc:`extending` docs for example usage. - Improve the more efficient implementations of :meth:`~bidict.BidirectionalMapping.keys`, :meth:`~bidict.BidirectionalMapping.values`, and :meth:`~bidict.BidirectionalMapping.items`, and now also provide a more efficient implementation of :meth:`~bidict.BidirectionalMapping.__iter__` by delegating to backing :class:`dict`\s in the bidict types for which this is possible. - Move :meth:`bidict.BidictBase.values` to :meth:`bidict.BidirectionalMapping.values`, since the implementation is generic. - No longer use ``__all__`` in :mod:`bidict`'s ``__init__.py``. 0.18.4 (2020-11-02) ------------------- - Backport fix from v0.20.0 that removes :meth:`bidict.BidirectionalMapping.__subclasshook__` due to lack of use and maintenance cost. 0.18.3 (2019-09-22) ------------------- - Improve validation of names passed to :func:`~bidict.namedbidict`: Use :meth:`str.isidentifier` on Python 3, and a better regex on Python 2. - On Python 3, set :attr:`~definition.__qualname__` on :func:`~bidict.namedbidict` classes based on the provided ``typename`` argument. 0.18.2 (2019-09-08) ------------------- - Warn that Python 2 support will be dropped in a future release when Python 2 is detected. 0.18.1 (2019-09-03) ------------------- - Fix a regression introduced by the memory optimizations added in 0.15.0 which caused :func:`deepcopied ` and :func:`unpickled ` bidicts to have their inverses set incorrectly. :issue:`94` 0.18.0 (2019-02-14) ------------------- - Rename ``bidict.BidirectionalMapping.inv`` to :attr:`~bidict.BidirectionalMapping.inverse` and make :attr:`bidict.BidictBase.inv` an alias for :attr:`~bidict.BidictBase.inverse`. :issue:`86` - :meth:`bidict.BidirectionalMapping.__subclasshook__` now requires an ``inverse`` attribute rather than an ``inv`` attribute for a class to qualify as a virtual subclass. This breaking change is expected to affect few if any users. - Add Python 2/3-compatible ``bidict.compat.collections_abc`` alias. - Stop testing Python 3.4 on CI, and warn when Python 3 < 3.5 is detected rather than Python 3 < 3.3. Python 3.4 reaches `end of life `__ on 2019-03-18. As of January 2019, 3.4 represents only about 3% of bidict downloads on `PyPI Stats `__. 0.17.5 (2018-11-19) ------------------- Improvements to performance and delegation logic, with minor breaking changes to semi-private APIs. - Remove the ``__delegate__`` instance attribute added in the previous release. It was overly general and not worth the cost. Instead of checking ``self.__delegate__`` and delegating accordingly each time a possibly-delegating method is called, revert back to using "delegated-to-fwdm" mixin classes (now found in ``bidict._delegating_mixins``), and resurrect a mutable bidict parent class that omits the mixins as :class:`bidict.MutableBidict`. - Rename ``__repr_delegate__`` to :class:`~bidict.BidictBase._repr_delegate`. 0.17.4 (2018-11-14) ------------------- Minor code, interop, and (semi-)private API improvements. - :class:`~bidict.OrderedBidict` optimizations and code improvements. Use ``bidict``\s for the backing ``_fwdm`` and ``_invm`` mappings, obviating the need to store key and value data in linked list nodes. - Refactor proxied- (i.e. delegated-) to-``_fwdm`` logic for better composability and interoperability. Drop the ``_Proxied*`` mixin classes and instead move their methods into :class:`~bidict.BidictBase`, which now checks for an object defined by the ``BidictBase.__delegate__`` attribute. The ``BidictBase.__delegate__`` object will be delegated to if the method is available on it, otherwise a default implementation (e.g. inherited from :class:`~collections.abc.Mapping`) will be used otherwise. Subclasses may set ``__delegate__ = None`` to opt out. Consolidate ``_MutableBidict`` into :class:`bidict.bidict` now that the dropped mixin classes make it unnecessary. - Change ``__repr_delegate__`` to simply take a type like :class:`dict` or :class:`list`. - Upgrade to latest major `sortedcontainers `__ version (from v1 to v2) for the :ref:`extending:\`\`SortedBidict\`\` Recipes`. - ``bidict.compat.{view,iter}{keys,values,items}`` on Python 2 no longer assumes the target object implements these methods, as they're not actually part of the :class:`~collections.abc.Mapping` interface, and provides fallback implementations when the methods are unavailable. This allows the :ref:`extending:\`\`SortedBidict\`\` Recipes` to continue to work with sortedcontainers v2 on Python 2. 0.17.3 (2018-09-18) ------------------- - Improve packaging by adding a pyproject.toml and by including more supporting files in the distribution. `#81 `__ - Drop pytest-runner and support for running tests via ``python setup.py test`` in preference to ``pytest`` or ``python -m pytest``. 0.17.2 (2018-04-30) ------------------- **Memory usage improvements** - Use less memory in the linked lists that back :class:`~bidict.OrderedBidict`\s by storing node data unpacked rather than in (key, value) tuple objects. 0.17.1 (2018-04-28) ------------------- **Bugfix Release** Fix a regression in 0.17.0 that could cause erroneous behavior when updating items of an :class:`~bidict.Orderedbidict`'s inverse, e.g. ``some_ordered_bidict.inv[foo] = bar``. 0.17.0 (2018-04-25) ------------------- **Speedups and memory usage improvements** - Pass :meth:`~bidict.bidict.keys`, :meth:`~bidict.bidict.values`, and :meth:`~bidict.bidict.items` calls (as well as their ``iter*`` and ``view*`` counterparts on Python 2) through to the backing ``_fwdm`` and ``_invm`` dicts so that they run as fast as possible (i.e. at C speed on CPython), rather than using the slower implementations inherited from :class:`collections.abc.Mapping`. - Use weakrefs in the linked lists that back :class:`~bidict.OrderedBidict`\s to avoid creating strong reference cycles. Memory for an ordered bidict that you create can now be reclaimed in CPython as soon as you no longer hold any references to it, rather than having to wait until the next garbage collection. `#71 `__ **Misc** - Add ``bidict.__version_info__`` attribute to complement :attr:`bidict.__version__`. 0.16.0 (2018-04-06) ------------------- Minor code and efficiency improvements to :func:`~bidict.inverted` and ``bidict._iter._iteritems_args_kw`` (formerly ``bidict.pairs()``). **Minor Breaking API Changes** The following breaking changes are expected to affect few if any users. - Rename ``bidict.pairs()`` → ``bidict._iter._iteritems_args_kw``. 0.15.0 (2018-03-29) ------------------- **Speedups and memory usage improvements** - Use :ref:`slots` to speed up bidict attribute access and reduce memory usage. On Python 3, instantiating a large number of bidicts now uses ~57% the amount of memory that it used before, and on Python 2 only ~33% the amount of memory that it used before, in a simple but representative `benchmark `__. - Use weakrefs to refer to a bidict's inverse internally, no longer creating a strong reference cycle. Memory for a bidict that you create can now be reclaimed in CPython as soon as you no longer hold any references to it, rather than having to wait for the next garbage collection. See the new :ref:`addendum:\`\`bidict\`\` Avoids Reference Cycles` documentation. :issue:`24` - Make :func:`bidict.BidictBase.__eq__` significantly more speed- and memory-efficient when comparing to a non-:class:`dict` :class:`~collections.abc.Mapping`. (``Mapping.__eq__()``\'s inefficient implementation will now never be used.) The implementation is now more reusable as well. - Make :func:`bidict.OrderedBidictBase.__iter__` as well as equality comparison slightly faster for ordered bidicts. **Minor Bugfixes** - :func:`~bidict.namedbidict` now verifies that the provided ``keyname`` and ``valname`` are distinct, raising :class:`ValueError` if they are equal. - :func:`~bidict.namedbidict` now raises :class:`TypeError` if the provided ``base_type`` is not a :class:`~bidict.BidirectionalMapping`. - If you create a custom bidict subclass whose ``_fwdm_cls`` differs from its ``_invm_cls`` (as in the ``FwdKeySortedBidict`` example from the :ref:`extending:\`\`SortedBidict\`\` Recipes`), the inverse bidirectional mapping type (with ``_fwdm_cls`` and ``_invm_cls`` swapped) is now correctly computed and used automatically for your custom bidict's :attr:`~bidict.BidictBase.inverse` bidict. **Misc** - Classes no longer have to provide an ``__inverted__`` attribute to be considered virtual subclasses of :class:`~bidict.BidirectionalMapping`. - If :func:`bidict.inverted` is passed an object with an ``__inverted__`` attribute, it now ensures it is :func:`callable` before returning the result of calling it. - :func:`~bidict.BidictBase.__repr__` no longer checks for a ``__reversed__`` method to determine whether to use an ordered or unordered-style repr. It now calls the new ``__repr_delegate__`` instead (which may be overridden if needed), for better composability. **Minor Breaking API Changes** The following breaking changes are expected to affect few if any users. - Split back out the :class:`~bidict.BidictBase` class from :class:`~bidict.frozenbidict` and :class:`~bidict.OrderedBidictBase` from :class:`~bidict.FrozenOrderedBidict`, reverting the merging of these in 0.14.0. Having e.g. ``issubclass(bidict, frozenbidict) == True`` was confusing, so this change restores ``issubclass(bidict, frozenbidict) == False``. See the updated :ref:`other-bidict-types:Bidict Types Diagram` and :ref:`other-bidict-types:Polymorphism` documentation. - Rename: - ``bidict.BidictBase.fwdm`` → ``._fwdm`` - ``bidict.BidictBase.invm`` → ``._invm`` - ``bidict.BidictBase.fwd_cls`` → ``._fwdm_cls`` - ``bidict.BidictBase.inv_cls`` → ``._invm_cls`` - ``bidict.BidictBase.isinv`` → ``._isinv`` Though overriding ``_fwdm_cls`` and ``_invm_cls`` remains supported (see :doc:`extending`), this is not a common enough use case to warrant public names. Most users do not need to know or care about any of these. - The :attr:`~bidict.RAISE`, ``OVERWRITE``, and ``IGNORE`` duplication policies are no longer available as attributes of ``DuplicationPolicy``, and can now only be accessed as attributes of the :mod:`bidict` module namespace, which was the canonical way to refer to them anyway. It is now no longer possible to create an infinite chain like ``DuplicationPolicy.RAISE.RAISE.RAISE...`` - Make ``bidict.pairs()`` and :func:`bidict.inverted` no longer importable from ``bidict.util``, and now only importable from the top-level :mod:`bidict` module. (``bidict.util`` was renamed ``bidict._util``.) - Pickling ordered bidicts now requires at least version 2 of the pickle protocol. If you are using Python 3, :attr:`pickle.DEFAULT_PROTOCOL` is 3 anyway, so this will not affect you. However if you are using in Python 2, :attr:`~pickle.DEFAULT_PROTOCOL` is 0, so you must now explicitly specify the version in your :func:`pickle.dumps` calls, e.g. ``pickle.dumps(ob, 2)``. 0.14.2 (2017-12-06) ------------------- - Make initializing (or updating an empty bidict) from only another :class:`~bidict.BidirectionalMapping` more efficient by skipping unnecessary duplication checking. - Fix accidental ignoring of specified ``base_type`` argument when (un)pickling a :func:`~bidict.namedbidict`. - Fix incorrect inversion of ``some_named_bidict.inv._for`` and ``some_named_bidict.inv._for``. - Only warn when an unsupported Python version is detected (e.g. Python < 2.7) rather than raising :class:`AssertionError`. 0.14.1 (2017-11-28) ------------------- - Fix a bug introduced in 0.14.0 where hashing a :class:`~bidict.frozenbidict`\’s inverse (e.g. ``f = frozenbidict(); {f.inv: '...'}``) would cause an ``AttributeError``. - Fix a bug introduced in 0.14.0 for Python 2 users where attempting to call ``viewitems()`` would cause a ``TypeError``. :issue:`48` 0.14.0 (2017-11-20) ------------------- - Fix a bug where :class:`~bidict.bidict`\’s default *on_dup_kv* policy was set to :attr:`~bidict.RAISE`, rather than matching whatever *on_dup_val* policy was in effect as was :ref:`documented `. - Fix a bug that could happen when using Python's optimization (``-O``) flag that could leave an ordered bidict in an inconsistent state when dealing with duplicated, overwritten keys or values. If you do not use optimizations (specifically, skipping ``assert`` statements), this would not have affected you. - Fix a bug introduced by the optimizations in 0.13.0 that could cause a frozen bidict that compared equal to another mapping to have a different hash value from the other mapping, violating Python's object model. This would only have affected you if you were inserting a frozen bidict and some other immutable mapping that it compared equal to into the same set or mapping. - Add :meth:`~bidict.OrderedBidictBase.equals_order_sensitive`. - Reduce the memory usage of ordered bidicts. - Make copying of ordered bidicts faster. - Improvements to tests and CI, including: - Test on Windows - Test with PyPy3 - Test with CPython 3.7-dev - Test with optimization flags - Require pylint to pass **Breaking API Changes** This release includes multiple API simplifications and improvements. - Rename: - ``orderedbidict`` → :class:`~bidict.OrderedBidict` - ``frozenorderedbidict`` → :class:`~bidict.FrozenOrderedBidict` so that these now match the case of :class:`collections.OrderedDict`. The names of the :class:`~bidict.bidict`, :func:`~bidict.namedbidict`, and :class:`~bidict.frozenbidict` classes have been retained as all-lowercase so that they continue to match the case of :class:`dict`, :func:`~collections.namedtuple`, and :class:`frozenset`, respectively. - The ``ON_DUP_VAL`` duplication policy value for *on_dup_kv* has been removed. Use ``None`` instead. - Merge :class:`~bidict.frozenbidict` and ``BidictBase`` together and remove ``BidictBase``. :class:`~bidict.frozenbidict` is now the concrete base class that all other bidict types derive from. See the updated :ref:`other-bidict-types:Bidict Types Diagram`. - Merge :class:`~bidict.frozenbidict` and ``FrozenBidictBase`` together and remove ``FrozenBidictBase``. See the updated :ref:`other-bidict-types:Bidict Types Diagram`. - Merge ``frozenorderedbidict`` and ``OrderedBidictBase`` together into a single :class:`~bidict.FrozenOrderedBidict` class and remove ``OrderedBidictBase``. :class:`~bidict.OrderedBidict` now extends :class:`~bidict.FrozenOrderedBidict` to add mutable behavior. See the updated :ref:`other-bidict-types:Bidict Types Diagram`. - Make :meth:`~bidict.OrderedBidictBase.__eq__` always perform an order-insensitive equality test, even if the other mapping is ordered. Previously, :meth:`~bidict.OrderedBidictBase.__eq__` was only order-sensitive for other ``OrderedBidictBase`` subclasses, and order-insensitive otherwise. Use the new :meth:`~bidict.OrderedBidictBase.equals_order_sensitive` method for order-sensitive equality comparison. - ``orderedbidict._should_compare_order_sensitive()`` has been removed. - ``frozenorderedbidict._HASH_NITEMS_MAX`` has been removed. Since its hash value must be computed from all contained items (so that hash results are consistent with equality comparisons against unordered mappings), the number of items that influence the hash value should not be limitable. - ``frozenbidict._USE_ITEMSVIEW_HASH`` has been removed, and ``frozenbidict.compute_hash()`` now uses ``collections.ItemsView._hash()`` to compute the hash always, not just when running on PyPy. Override ``frozenbidict.compute_hash()`` to return ``hash(frozenset(iteritems(self)))`` if you prefer the old default behavior on CPython, which takes linear rather than constant space, but which uses the ``frozenset_hash`` routine (implemented in ``setobject.c``) rather than the pure Python ``ItemsView._hash()`` routine. - ``loosebidict`` and ``looseorderedbidict`` have been removed. A simple recipe to implement equivalents yourself is now given in :doc:`extending`. - Rename ``FrozenBidictBase._compute_hash()`` → ``frozenbidict.compute_hash()``. - Rename ``DuplicationBehavior`` → ``DuplicationPolicy``. - Rename: - ``BidictBase._fwd_class`` → ``.fwd_cls`` - ``BidictBase._inv_class`` → ``.inv_cls`` - ``BidictBase._on_dup_key`` → ``on_dup_key`` - ``BidictBase._on_dup_val`` → ``on_dup_val`` - ``BidictBase._on_dup_kv`` → ``on_dup_kv`` 0.13.1 (2017-03-15) ------------------- - Fix regression introduced by the new :meth:`~bidict.BidirectionalMapping.__subclasshook__` functionality in 0.13.0 so that ``issubclass(OldStyleClass, BidirectionalMapping)`` once again works with old-style classes, returning ``False`` rather than raising :class:`AttributeError` `#41 `__ 0.13.0 (2017-01-19) ------------------- - Support Python 3.6. (Earlier versions of bidict should work fine on 3.6, but it is officially supported starting in this version.) - :class:`~bidict.BidirectionalMapping` has been refactored into an abstract base class, following the way :class:`collections.abc.Mapping` works. The concrete method implementations it used to provide have been moved into a new ``BidictBase`` subclass. :class:`~bidict.BidirectionalMapping` now also implements :meth:`~bidict.BidirectionalMapping.__subclasshook__`, so any class that provides a conforming set of attributes (enumerated in :attr:`~bidict.BidirectionalMapping._subclsattrs`) will be considered a :class:`~bidict.BidirectionalMapping` subclass automatically. - ``OrderedBidirectionalMapping`` has been renamed to ``OrderedBidictBase``, to better reflect its function. (It is not an ABC.) - A new ``FrozenBidictBase`` class has been factored out of :class:`~bidict.frozenbidict` and :class:`frozenorderedbidict `. This implements common behavior such as caching the result of ``__hash__`` after the first call. - The hash implementations of :class:`~bidict.frozenbidict` and :class:`frozenorderedbidict `. have been reworked to improve performance and flexibility. :class:`frozenorderedbidict `\’s hash implementation is now order-sensitive. See ``frozenbidict._compute_hash()`` and ``frozenorderedbidict._compute_hash`` for more documentation of the changes, including the new ``frozenbidict._USE_ITEMSVIEW_HASH`` and ``frozenorderedbidict._HASH_NITEMS_MAX`` attributes. If you have an interesting use case that requires overriding these, or suggestions for an alternative implementation, please `share your feedback `__. - Add ``_fwd_class`` and ``_inv_class`` attributes representing the backing :class:`~collections.abc.Mapping` types used internally to store the forward and inverse dictionaries, respectively. This allows creating custom bidict types with extended functionality simply by overriding these attributes in a subclass. See the new :doc:`extending` documentation for examples. - Pass any parameters passed to :meth:`~bidict.bidict.popitem` through to ``_fwd.popitem`` for greater extensibility. - More concise repr strings for empty bidicts. e.g. ``bidict()`` rather than ``bidict({})`` and ``orderedbidict()`` rather than ``orderedbidict([])``. - Add ``bidict.compat.PYPY`` and remove unused ``bidict.compat.izip_longest``. 0.12.0 (2016-07-03) ------------------- - New/renamed exceptions: - :class:`~bidict.KeyDuplicationError` - :class:`~bidict.ValueDuplicationError` - :class:`~bidict.KeyAndValueDuplicationError` - :class:`~bidict.DuplicationError` (base class for the above) - :func:`~bidict.bidict.put` now accepts ``on_dup_key``, ``on_dup_val``, and ``on_dup_kv`` keyword args which allow you to override the default policy when the key or value of a given item duplicates any existing item's. These can take the following values: - :attr:`~bidict.RAISE` - ``OVERWRITE`` - ``IGNORE`` ``on_dup_kv`` can also take ``ON_DUP_VAL``. If not provided, :func:`~bidict.bidict.put` uses the :attr:`~bidict.RAISE` policy by default. - New :func:`~bidict.bidict.putall` method provides a bulk :func:`~bidict.bidict.put` API, allowing you to override the default duplication handling policy that :func:`~bidict.bidict.update` uses. - :func:`~bidict.bidict.update` now fails clean, so if an :func:`~bidict.bidict.update` call raises a :class:`~bidict.DuplicationError`, you can now be sure that none of the given items was inserted. Previously, all of the given items that were processed before the one causing the failure would have been inserted, and no facility was provided to recover which items were inserted and which weren't, nor to revert any changes made by the failed :func:`~bidict.bidict.update` call. The new behavior makes it easier to reason about and control the effects of failed :func:`~bidict.bidict.update` calls. The new :func:`~bidict.bidict.putall` method also fails clean. Internally, this is implemented by storing a log of changes made while an update is being processed, and rolling back the changes when one of them is found to cause an error. This required reimplementing :class:`orderedbidict ` on top of two dicts and a linked list, rather than two OrderedDicts, since :class:`~collections.OrderedDict` does not expose its backing linked list. - :func:`orderedbidict.move_to_end() ` now works on Python < 3.2 as a result of the new :class:`orderedbidict ` implementation. - Add - ``bidict.compat.viewkeys`` - ``bidict.compat.viewvalues`` - ``bidict.compat.iterkeys`` - ``bidict.compat.itervalues`` - ``bidict.compat.izip`` - ``bidict.compat.izip_longest`` to complement the existing ``bidict.compat.iteritems`` and ``bidict.compat.viewitems`` compatibility helpers. - More efficient implementations of ``bidict.pairs()``, :func:`~bidict.inverted`, and :func:`~bidict.BidictBase.copy`. - Implement :func:`~bidict.BidictBase.__copy__` for use with the :mod:`copy` module. - Fix issue preventing a client class from inheriting from ``loosebidict``. :issue:`34` - Add benchmarking to tests. - Drop official support for CPython 3.3. (It may continue to work, but is no longer being tested.) **Breaking API Changes** - Rename ``KeyExistsException`` → :class:`~bidict.KeyDuplicationError` and ``ValueExistsException`` → :class:`~bidict.ValueDuplicationError`. - When overwriting the key of an existing value in an :class:`orderedbidict `, the position of the existing item is now preserved, overwriting the key of the existing item in place, rather than moving the item to the end. This now matches the behavior of overwriting the value of an existing key, which has always preserved the position of the existing item. (If inserting an item whose key duplicates that of one existing item and whose value duplicates that of another, the existing item whose value is duplicated is still dropped, and the existing item whose key is duplicated still gets its value overwritten in place, as before.) For example: .. code:: python >>> from bidict import orderedbidict # doctest: +SKIP >>> o = orderedbidict([(0, 1), (2, 3)]) # doctest: +SKIP >>> o.forceput(4, 1) # doctest: +SKIP previously would have resulted in: .. code:: python >>> o # doctest: +SKIP orderedbidict([(2, 3), (4, 1)]) but now results in: .. code:: python >>> o # doctest: +SKIP orderedbidict([(4, 1), (2, 3)]) 0.11.0 (2016-02-05) ------------------- - Add :class:`orderedbidict `, ``looseorderedbidict``, and :class:`frozenorderedbidict `. - Add :doc:`code-of-conduct`. - Drop official support for pypy3. (It still may work but is no longer being tested. Support may be added back once pypy3 has made more progress.) 0.10.0.post1 (2015-12-23) ------------------------- - Minor documentation fixes and improvements. 0.10.0 (2015-12-23) ------------------- - Remove several features in favor of keeping the API simpler and the code more maintainable. - In the interest of protecting data safety more proactively, by default bidict now raises an error on attempting to insert a non-unique value, rather than allowing its associated key to be silently overwritten. See discussion in :issue:`21`. - New :meth:`~bidict.bidict.forceupdate` method provides a bulk :meth:`~bidict.bidict.forceput` operation. - Fix bugs in :attr:`~bidict.bidict.pop` and :attr:`~bidict.bidict.setdefault` which could leave a bidict in an inconsistent state. **Breaking API Changes** - Remove ``bidict.__invert__``, and with it, support for the ``~b`` syntax. Use :attr:`~bidict.BidictBase.inv` instead. :issue:`19` - Remove support for the slice syntax. Use ``b.inv[val]`` rather than ``b[:val]``. :issue:`19` - Remove ``bidict.invert``. Use :attr:`~bidict.BidictBase.inv` rather than inverting a bidict in place. :issue:`20` - Raise ``ValueExistsException`` when attempting to insert a mapping with a non-unique key. :issue:`21` - Rename ``collapsingbidict`` → ``loosebidict`` now that it suppresses ``ValueExistsException`` rather than the less general ``CollapseException``. :issue:`21` - ``CollapseException`` has been subsumed by ``ValueExistsException``. :issue:`21` - :meth:`~bidict.bidict.put` now raises ``KeyExistsException`` when attempting to insert an already-existing key, and ``ValueExistsException`` when attempting to insert an already-existing value. 0.9.0.post1 (2015-06-06) ------------------------ - Fix metadata missing in the 0.9.0rc0 release. 0.9.0rc0 (2015-05-30) --------------------- - Add this changelog, `Contributors' Guide `__, `Gitter chat room `__, and other community-oriented improvements. - Adopt Pytest. - Add property-based tests via `hypothesis `__. - Other code, tests, and docs improvements. **Breaking API Changes** - Move ``bidict.iteritems()`` and ``bidict.viewitems()`` to new ``bidict.compat`` module. - Move :class:`bidict.inverted` to new ``bidict.util`` module (still available from top-level :mod:`bidict` module as well). - Move ``bidict.fancy_iteritems()`` → ``bidict.util.pairs()`` (also available from top level as ``bidict.pairs()``). - Rename :func:`bidict.namedbidict`\'s ``bidict_type`` argument → ``base_type``. bidict-0.22.1/CODE_OF_CONDUCT.md000066400000000000000000000002211435416354600156370ustar00rootroot00000000000000Please see CODE_OF_CONDUCT.rst This file is here to work around the GitHub bug discussed in https://github.com/github/feedback/discussions/9442 bidict-0.22.1/CODE_OF_CONDUCT.rst000066400000000000000000000063171435416354600160630ustar00rootroot00000000000000Code of Conduct =============== Our Pledge ---------- In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. Our Standards ------------- Examples of behavior that contributes to creating a positive environment include: - Using welcoming and inclusive language - Being respectful of differing viewpoints and experiences - Gracefully accepting constructive criticism - Focusing on what is best for the community - Showing empathy towards other community members Examples of unacceptable behavior by participants include: - The use of sexualized language or imagery and unwelcome sexual attention or advances - Trolling, insulting/derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or electronic address, without explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting Our Responsibilities -------------------- Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. Scope ----- This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. Enforcement ----------- Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at . All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. Attribution ----------- This Code of Conduct is adapted from the `Contributor Covenant `__, version 1.4, available at `https://www.contributor-covenant.org/version/1/4 `__. bidict-0.22.1/CONTRIBUTING.rst000066400000000000000000000164671435416354600155240ustar00rootroot00000000000000.. Forward declarations for all the custom interpreted text roles that Sphinx defines and that are used below. This helps Sphinx-unaware tools (e.g. rst2html, PyPI's and GitHub's renderers, etc.). .. role:: doc .. role:: ref Contributors' Guide =================== Bug reports, feature requests, patches, and other contributions are warmly welcomed. Contribution should be as easy and friendly as possible. Below are a few guidelines contributors should follow to facilitate the process. Getting Started --------------- - `Create a GitHub account `__ if you don't have one already. - Search through the `tracker `__ to see if an issue or pull request has already been created for what you're interested in. If so, feel free to add comments to it or just hit the "subscribe" button to follow progress. If not, you can `join the chat room `__ to discuss there, post in the `GitHub Discussions forum `__, or go ahead and `create a new issue `__: - Clearly describe the issue giving as much relevant context as possible. - If it is a bug, include reproduction steps, all known environments in which the bug is exhibited, and ideally a failing test case. - If you would like to contribute a patch, make sure you've `created your own fork `__ and have cloned it to your computer. Making Changes -------------- .. note:: You can now use `GitPod.io `__ to get an already-configured development environment inside your browser in which you can make, test, and submit your changes to bidict. .. note:: You can also work on bidict in a Visual Studio Code `devcontainer environment `__ which will install development dependencies and some helpful VS Code extensions for you. Try ``Remote-Containers: Clone Repository in Container Volume...`` on this repository. You may need to reload your VS Code window after it finishes cloning and installing extensions, which it should prompt you to do. In a devcontainer, you don't need to worry about the below steps of making a virtualenv or configuring EditorConfig or pre-commit, those will be part of your development environment by default. - Before making changes, please (create a `virtualenv `__ and) install the extra packages required for development if you haven't already: ``pip install -r requirements/test.txt -r requirements/lint.txt -r requirements/docs.txt`` We use `EditorConfig `__ and `pre-commit `__ to help achieve uniform style and quality standards across a diversity of development environments. pre-commit gets installed when you run the command above and ensures that various code checks are run before every commit (look in ``.pre-commit-config.yaml`` to see which hooks are run). Ensure the configured hooks are installed by running ``pre-commit install --install-hooks``. EditorConfig allows us to provide a single ``.editorconfig`` file to configure settings like indentation consistently across a variety of supported editors. See https://editorconfig.org/#download to install the plugin for your editor. - Create a topic branch off of main for your changes: ``git checkout -b main`` - Make commits of logical units. - Match the existing code style and quality standards. If you're adding a feature, include accompanying tests and documentation demonstrating its correctness and usage. - Run the tests locally with `tox `__ to make sure they pass for all supported Python versions (see ``envlist`` in ``tox.ini`` for the complete list). If you do not have all the referenced Python versions available locally, you can also push the changes on your branch to GitHub to automatically trigger a new `GitHub Actions `__ build, which should run the tests for all supported Python versions. - Create a concise but comprehensive commit message in the following style:: Include an example commit message in CONTRIBUTING guide #9999 Without this patch the CONTRIBUTING guide would contain no examples of a model commit message. This is a problem because the contributor is left to imagine what the commit message should look like and may not get it right. This patch fixes the problem by providing a concrete example. The first line is an imperative statement summarizing the changes with an issue number from the tracker. The body describes the behavior without the patch, why it's a problem, and how the patch fixes the problem. Submitting Changes ------------------ - Push your changes to a topic branch in your fork of the repository: ``git push --set-upstream origin `` - Submit a pull request providing any additional relevant details necessary. - Acknowledgment should typically be fast but please allow 1-2 weeks for a full response / code review. - The code review process often involves some back-and-forth to get everything right before merging. This is typical of quality software projects that accept patches. - All communication should be supportive and appreciative of good faith efforts to contribute, creating a welcoming and inclusive community. Sponsoring ---------- .. image:: https://img.shields.io/badge/GitHub-sponsor-ff69b4 :target: https://github.com/sponsors/jab :alt: Sponsor through GitHub .. image:: https://img.shields.io/badge/Gumroad-sponsor-55a0a4.svg :target: https://gumroad.com/l/bidict :alt: Sponsor through Gumroad .. image:: https://img.shields.io/badge/PayPal-sponsor-blue.svg :target: https://www.paypal.com/cgi-bin/webscr?cmd=_xclick&business=jabronson%40gmail%2ecom&lc=US&item_name=Sponsor%20bidict :alt: Sponsor through PayPal .. image:: https://img.shields.io/github/sponsors/jab :target: https://github.com/sponsors/jab :alt: Sponsors on GitHub .. duplicated in README.rst (would use `.. include::` but GitHub doesn't understand it) Bidict is the product of thousands of hours of my unpaid work over the ~15 years that I've been the sole maintainer. If bidict has helped you or your company accomplish your work, please `sponsor my work through GitHub `__, and/or ask your company to do the same. Choose a tier and GitHub handles everything else. Your GitHub sponsorship will automatically go on the same bill you already have set up with GitHub, so after the one-click signup, there's nothing else to do. You can also sponsor my work through `Gumroad `__ or `PayPal `__, or through a support engagement with my LLC. See `Enterprise Support `__ for details. Code of Conduct --------------- All participation in this project should respect the :doc:`code-of-conduct`. [#fn-coc]_ By participating, you are expected to honor this code. .. [#fn-coc] ``__ | ``__ bidict-0.22.1/LICENSE000066400000000000000000000406201435416354600140540ustar00rootroot00000000000000Mozilla Public License Version 2.0 ================================== Copyright 2009-2022 Joshua Bronson. All rights reserved. 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. bidict-0.22.1/MANIFEST.in000066400000000000000000000001131435416354600145760ustar00rootroot00000000000000include bidict/py.typed recursive-exclude docs * recursive-exclude tests * bidict-0.22.1/PYPI_DOWNLOAD_STATS.rst000066400000000000000000000003341435416354600166250ustar00rootroot00000000000000Retrieving PyPI Download Stats ------------------------------ * https://pepy.tech/project/bidict * https://pypi.org/project/pypistats/ * https://packaging.python.org/en/latest/guides/analyzing-pypi-package-downloads/ bidict-0.22.1/README.rst000066400000000000000000000175731435416354600145510ustar00rootroot00000000000000.. role:: doc .. (Forward declaration for the "doc" role that Sphinx defines for interop with renderers that are often used to show this doc and that are unaware of Sphinx (GitHub.com, PyPI.org, etc.). Use :doc: rather than :ref: here for better interop as well.) bidict ====== *The bidirectional mapping library for Python.* Status ------ .. image:: https://img.shields.io/pypi/v/bidict.svg :target: https://pypi.org/project/bidict :alt: Latest release .. image:: https://img.shields.io/readthedocs/bidict/main.svg :target: https://bidict.readthedocs.io/en/main/ :alt: Documentation .. image:: https://github.com/jab/bidict/workflows/Tests/badge.svg :target: https://github.com/jab/bidict/actions :alt: GitHub Actions CI status .. image:: https://img.shields.io/pypi/l/bidict.svg :target: https://raw.githubusercontent.com/jab/bidict/main/LICENSE :alt: License .. image:: https://static.pepy.tech/badge/bidict :target: https://pepy.tech/project/bidict :alt: PyPI Downloads .. image:: https://img.shields.io/github/sponsors/jab :target: https://github.com/sponsors/jab :alt: Sponsors on GitHub .. image:: https://img.shields.io/badge/GitHub-sponsor-ff69b4 :target: https://github.com/sponsors/jab :alt: Sponsor on GitHub Features -------- - Depended on by Google, Venmo, CERN, Baidu, Tencent, and teams across the world since 2009 - Familiar, Pythonic APIs that are carefully designed for safety, simplicity, flexibility, and ergonomics - Lightweight, with no runtime dependencies outside Python's standard library - Implemented in concise, well-factored, fully type-hinted Python code that is optimized for running efficiently as well as for long-term maintenance and stability (not to mention `joy <#learning-from-bidict>`__ :) - Extensively `documented `__ - 100% test coverage running continuously across all supported Python versions Installation ------------ ``pip install bidict`` Quick Start ----------- .. code:: python >>> from bidict import bidict >>> element_by_symbol = bidict({'H': 'hydrogen'}) >>> element_by_symbol['H'] 'hydrogen' >>> element_by_symbol.inverse['hydrogen'] 'H' For more usage documentation, head to the :doc:`intro` [#fn-intro]_ and proceed from there. Enterprise Support ------------------ Enterprise-level support for bidict can be obtained via the `Tidelift subscription `__ or by `contacting me directly `__. I have a US-based LLC set up for invoicing, and I have 15+ years of professional experience delivering software and support to companies successfully. You can also sponsor my work through platforms like GitHub Sponsors. See the `Sponsoring <#sponsoring>`__ section below for details, including rationale and examples of companies supporting the open source projects they depend on. Voluntary Community Support --------------------------- Please search through already-asked questions and answers in `GitHub Discussions `__ and the `issue tracker `__ in case your question has already been addressed. Otherwise, please feel free to `start a new discussion `__ or `create a new issue `__ on GitHub, or ask in the `bidict chatroom `__ for voluntary community support. Notice of Usage --------------- If you use bidict, and especially if your usage or your organization is significant in some way, please let me know in any of the following ways: - `star bidict on GitHub `__ - post in `GitHub Discussions `__ - leave a message in the `chat room `__ - `email me `__ Changelog --------- For bidict release notes, see the :doc:`changelog`. [#fn-changelog]_ Release Notifications --------------------- .. duplicated in CHANGELOG.rst: (would use `.. include::` but GitHub doesn't understand it) Watch `bidict releases on GitHub `__ to be notified when new versions of bidict are published. Click the "Watch" dropdown, choose "Custom", and then choose "Releases". Learning from bidict -------------------- One of the best things about bidict is that it touches a surprising number of interesting Python corners, especially given its small size and scope. Check out :doc:`learning-from-bidict` [#fn-learning]_ if you're interested in learning more. Contributing ------------ I have been bidict's sole maintainer and `active contributor `__ since I started the project ~15 years ago. Your help would be most welcome! See the :doc:`contributors-guide` [#fn-contributing]_ for more information. Sponsoring ---------- .. duplicated in CONTRIBUTING.rst (would use `.. include::` but GitHub doesn't understand it) .. image:: https://img.shields.io/badge/GitHub-sponsor-ff69b4 :target: https://github.com/sponsors/jab :alt: Sponsor through GitHub .. image:: https://img.shields.io/github/sponsors/jab :target: https://github.com/sponsors/jab :alt: Sponsors on GitHub Bidict is the product of thousands of hours of my unpaid work over the ~15 years that I've been the sole maintainer. If bidict has helped you or your company accomplish your work, please `sponsor my work through GitHub `__ and/or ask your company to do the same. Choose a tier and GitHub handles everything else. Your GitHub sponsorship will automatically go on the same bill you already have set up with GitHub, so after the one-click signup, there's nothing else to do. See the following for rationale and examples of companies supporting the open source projects they depend on in this manner: - ``__ - ``__ - ``__ .. - ``__ .. - ``__ .. - ``__ .. - ``__ You can also support my work through `Gumroad `__ or `PayPal `__, or through a support engagement with my LLC. See `Enterprise Support <#enterprise-support>`__ above for details. Finding Documentation --------------------- If you're viewing this on ``__, note that multiple versions of the documentation are available, and you can choose a different version using the popup menu at the bottom-right. Please make sure you're viewing the version of the documentation that corresponds to the version of bidict you'd like to use. If you're viewing this on GitHub, PyPI, or some other place that can't render and link this documentation properly and are seeing broken links, try these alternate links instead: .. [#fn-intro] ``__ | ``__ .. [#fn-changelog] ``__ | ``__ .. [#fn-learning] ``__ | ``__ .. [#fn-contributing] ``__ | ``__ .. image:: https://static.scarf.sh/a.png?x-pxid=05e3c4e4-eaa7-41a1-84c2-ec14413115f8 bidict-0.22.1/SECURITY.rst000066400000000000000000000002061435416354600150440ustar00rootroot00000000000000Security ======== Please see `tidelift.com/security `__ for how to report a security issue in bidict. bidict-0.22.1/bidict/000077500000000000000000000000001435416354600143035ustar00rootroot00000000000000bidict-0.22.1/bidict/__init__.py000066400000000000000000000104621435416354600164170ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. #============================================================================== # * Welcome to the bidict source code * #============================================================================== # Reading through the code? You'll find a "Code review nav" comment like the one # below at the top and bottom of the key source files. Follow these cues to take # a path through the code that's optimized for familiarizing yourself with it. # # If you're not reading this on https://github.com/jab/bidict already, go there # to ensure you have the latest version of the code. While there, you can also # star the project, watch it for updates, fork the code, and submit an issue or # pull request with any proposed changes. More information can be found linked # from README.rst, which is also shown on https://github.com/jab/bidict. # * Code review nav * #============================================================================== # Current: __init__.py Next: _abc.py → #============================================================================== """The bidirectional mapping library for Python. ---- bidict by example: .. code-block:: python >>> from bidict import bidict >>> element_by_symbol = bidict({'H': 'hydrogen'}) >>> element_by_symbol['H'] 'hydrogen' >>> element_by_symbol.inverse['hydrogen'] 'H' Please see https://github.com/jab/bidict for the most up-to-date code and https://bidict.readthedocs.io for the most up-to-date documentation if you are reading this elsewhere. ---- .. :copyright: (c) 2009-2022 Joshua Bronson. .. :license: MPLv2. See LICENSE for details. """ # Use private aliases to not re-export these publicly (for Sphinx automodule with imported-members). from __future__ import annotations as _annotations from sys import version_info as _version_info if _version_info < (3, 7): # pragma: no cover raise ImportError('Python 3.7+ is required.') from contextlib import suppress as _suppress from ._abc import BidirectionalMapping as BidirectionalMapping, MutableBidirectionalMapping as MutableBidirectionalMapping from ._base import BidictBase as BidictBase, GeneratedBidictInverse as GeneratedBidictInverse, BidictKeysView as BidictKeysView from ._bidict import MutableBidict as MutableBidict, bidict as bidict from ._frozenbidict import frozenbidict as frozenbidict from ._frozenordered import FrozenOrderedBidict as FrozenOrderedBidict from ._named import NamedBidictBase as NamedBidictBase, namedbidict as namedbidict from ._orderedbase import OrderedBidictBase as OrderedBidictBase from ._orderedbidict import OrderedBidict as OrderedBidict from ._dup import ON_DUP_DEFAULT as ON_DUP_DEFAULT, ON_DUP_RAISE as ON_DUP_RAISE, ON_DUP_DROP_OLD as ON_DUP_DROP_OLD from ._dup import RAISE as RAISE, DROP_OLD as DROP_OLD, DROP_NEW as DROP_NEW, OnDup as OnDup, OD as OD from ._exc import BidictException as BidictException, DuplicationError as DuplicationError from ._exc import KeyDuplicationError as KeyDuplicationError, ValueDuplicationError as ValueDuplicationError, KeyAndValueDuplicationError as KeyAndValueDuplicationError from ._iter import inverted as inverted from .metadata import ( __author__ as __author__, __copyright__ as __copyright__, __description__ as __description__, __license__ as __license__, __url__ as __url__, __version__ as __version__, ) #: Alias OnDupAction = OD # Set __module__ of re-exported classes to the 'bidict' top-level module, so that e.g. # 'bidict.bidict' shows up as 'bidict.bidict` rather than 'bidict._bidict.bidict'. for _obj in tuple(locals().values()): # pragma: no cover if not getattr(_obj, '__module__', '').startswith('bidict.'): continue with _suppress(AttributeError): _obj.__module__ = 'bidict' # * Code review nav * #============================================================================== # Current: __init__.py Next: _abc.py → #============================================================================== bidict-0.22.1/bidict/_abc.py000066400000000000000000000061311435416354600155420ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # * Code review nav * # (see comments in __init__.py) #============================================================================== # ← Prev: __init__.py Current: _abc.py Next: _base.py → #============================================================================== """Provide the :class:`BidirectionalMapping` abstract base class.""" from __future__ import annotations from abc import abstractmethod import typing as t from ._typing import KT, VT class BidirectionalMapping(t.Mapping[KT, VT]): """Abstract base class for bidirectional mapping types. Extends :class:`collections.abc.Mapping` primarily by adding the (abstract) :attr:`inverse` property, which implementors of :class:`BidirectionalMapping` should override to return a reference to the inverse :class:`BidirectionalMapping` instance. """ __slots__ = () @property @abstractmethod def inverse(self) -> BidirectionalMapping[VT, KT]: """The inverse of this bidirectional mapping instance. *See also* :attr:`bidict.BidictBase.inverse`, :attr:`bidict.BidictBase.inv` :raises NotImplementedError: Meant to be overridden in subclasses. """ # The @abstractmethod decorator prevents BidirectionalMapping subclasses from being # instantiated unless they override ``.inverse``. So this implementation of ``.inverse`` # should never be unintentionally resolved from subclass instances. But raise here # anyway, so it's extra clear that this implementation should never be called. raise NotImplementedError def __inverted__(self) -> t.Iterator[tuple[VT, KT]]: """Get an iterator over the items in :attr:`inverse`. This is functionally equivalent to iterating over the items in the forward mapping and inverting each one on the fly, but this provides a more efficient implementation: Assuming the already-inverted items are stored in :attr:`inverse`, just return an iterator over them directly. Providing this default implementation enables external functions, particularly :func:`~bidict.inverted`, to use this optimized implementation when available, instead of having to invert on the fly. *See also* :func:`bidict.inverted` """ return iter(self.inverse.items()) class MutableBidirectionalMapping(BidirectionalMapping[KT, VT], t.MutableMapping[KT, VT]): """Abstract base class for mutable bidirectional mapping types.""" __slots__ = () # * Code review nav * #============================================================================== # ← Prev: __init__.py Current: _abc.py Next: _base.py → #============================================================================== bidict-0.22.1/bidict/_base.py000066400000000000000000000575131435416354600157410ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # * Code review nav * # (see comments in __init__.py) #============================================================================== # ← Prev: _abc.py Current: _base.py Next: _frozenbidict.py → #============================================================================== """Provide :class:`BidictBase`.""" from __future__ import annotations from functools import partial from itertools import starmap from operator import eq from types import MappingProxyType import typing as t import weakref from ._abc import BidirectionalMapping from ._dup import ON_DUP_DEFAULT, RAISE, DROP_OLD, DROP_NEW, OnDup from ._exc import DuplicationError, KeyDuplicationError, ValueDuplicationError, KeyAndValueDuplicationError from ._iter import iteritems, inverted from ._typing import KT, VT, MISSING, OKT, OVT, Items, MapOrItems, TypeAlias OldKV: TypeAlias = 'tuple[OKT[KT], OVT[VT]]' DedupResult: TypeAlias = 'OldKV[KT, VT] | None' Write: TypeAlias = 'list[t.Callable[[], None]]' Unwrite: TypeAlias = Write PreparedWrite: TypeAlias = 'tuple[Write, Unwrite]' BT = t.TypeVar('BT', bound='BidictBase[t.Any, t.Any]') class BidictKeysView(t.KeysView[KT], t.ValuesView[KT]): """Since the keys of a bidict are the values of its inverse (and vice versa), the :class:`~collections.abc.ValuesView` result of calling *bi.values()* is also a :class:`~collections.abc.KeysView` of *bi.inverse*. """ def get_arg(*args: MapOrItems[KT, VT]) -> MapOrItems[KT, VT]: """Ensure there's only a single arg in *args*, then return it.""" if len(args) > 1: raise TypeError(f'Expected at most 1 positional argument, got {len(args)}') return args[0] if args else () class BidictBase(BidirectionalMapping[KT, VT]): """Base class implementing :class:`BidirectionalMapping`.""" #: The default :class:`~bidict.OnDup` #: that governs behavior when a provided item #: duplicates the key or value of other item(s). #: #: *See also* #: :ref:`basic-usage:Values Must Be Unique` (https://bidict.rtfd.io/basic-usage.html#values-must-be-unique), #: :doc:`extending` (https://bidict.rtfd.io/extending.html) on_dup = ON_DUP_DEFAULT _fwdm: t.MutableMapping[KT, VT] #: the backing forward mapping (*key* → *val*) _invm: t.MutableMapping[VT, KT] #: the backing inverse mapping (*val* → *key*) # Use Any rather than KT/VT in the following to avoid "ClassVar cannot contain type variables" errors: _fwdm_cls: t.ClassVar[t.Type[t.MutableMapping[t.Any, t.Any]]] = dict #: class of the backing forward mapping _invm_cls: t.ClassVar[t.Type[t.MutableMapping[t.Any, t.Any]]] = dict #: class of the backing inverse mapping #: The class of the inverse bidict instance. _inv_cls: t.ClassVar[t.Type[BidictBase[t.Any, t.Any]]] #: Used by :meth:`__repr__` for the contained items. _repr_delegate: t.ClassVar[t.Any] = dict def __init_subclass__(cls) -> None: super().__init_subclass__() cls._init_class() @classmethod def _init_class(cls) -> None: cls._ensure_inv_cls() cls._set_reversed() __reversed__: t.Any @classmethod def _set_reversed(cls) -> None: """Set __reversed__ for subclasses that do not set it explicitly according to whether backing mappings are reversible. """ if cls is not BidictBase: resolved = cls.__reversed__ overridden = resolved is not BidictBase.__reversed__ if overridden: # E.g. OrderedBidictBase, OrderedBidict, FrozenOrderedBidict return # The following will be False for MutableBidict, bidict, and frozenbidict on Python < 3.8, # and True for them on 3.8+ (where dicts are reversible). Will also be True for custom # subclasses like SortedBidict (see https://bidict.rtfd.io/extending.html#sortedbidict-recipes). backing_reversible = all(issubclass(i, t.Reversible) for i in (cls._fwdm_cls, cls._invm_cls)) cls.__reversed__ = _fwdm_reversed if backing_reversible else None @classmethod def _ensure_inv_cls(cls) -> None: """Ensure :attr:`_inv_cls` is set, computing it dynamically if necessary. See: :ref:`extending:Dynamic Inverse Class Generation` (https://bidict.rtfd.io/extending.html#dynamic-inverse-class-generation) Most subclasses will be their own inverse classes, but some (e.g. those created via namedbidict) will have distinct inverse classes. """ if cls.__dict__.get('_inv_cls'): return # Already set, nothing to do. cls._inv_cls = cls._make_inv_cls() @classmethod def _make_inv_cls(cls: t.Type[BT], _miss: t.Any = object()) -> t.Type[BT]: diff = cls._inv_cls_dict_diff() cls_is_own_inv = all(getattr(cls, k, _miss) == v for (k, v) in diff.items()) if cls_is_own_inv: return cls # Suppress auto-calculation of _inv_cls's _inv_cls since we know it already. # Works with the guard in BidictBase._ensure_inv_cls() to prevent infinite recursion. diff['_inv_cls'] = cls inv_cls = type(f'{cls.__name__}Inv', (cls, GeneratedBidictInverse), diff) inv_cls.__module__ = cls.__module__ return t.cast(t.Type[BT], inv_cls) @classmethod def _inv_cls_dict_diff(cls) -> dict[str, t.Any]: return { '_fwdm_cls': cls._invm_cls, '_invm_cls': cls._fwdm_cls, } @t.overload def __init__(self, **kw: VT) -> None: ... @t.overload def __init__(self, __m: t.Mapping[KT, VT], **kw: VT) -> None: ... @t.overload def __init__(self, __i: Items[KT, VT], **kw: VT) -> None: ... def __init__(self, *args: MapOrItems[KT, VT], **kw: VT) -> None: """Make a new bidirectional mapping. The signature behaves like that of :class:`dict`. Items passed in are added in the order they are passed, respecting the :attr:`on_dup` class attribute in the process. """ self._fwdm = self._fwdm_cls() self._invm = self._invm_cls() if args or kw: self._update(get_arg(*args), kw, rbof=False) # If Python ever adds support for higher-kinded types, `inverse` could use them, e.g. # def inverse(self: BT[KT, VT]) -> BT[VT, KT]: # Ref: https://github.com/python/typing/issues/548#issuecomment-621571821 @property def inverse(self) -> BidictBase[VT, KT]: """The inverse of this bidirectional mapping instance.""" # When `bi.inverse` is called for the first time, this method # computes the inverse instance, stores it for subsequent use, and then # returns it. It also stores a reference on `bi.inverse` back to `bi`, # but uses a weakref to avoid creating a reference cycle. Strong references # to inverse instances are stored in ._inv, and weak references are stored # in ._invweak. # First check if a strong reference is already stored. inv: BidictBase[VT, KT] | None = getattr(self, '_inv', None) if inv is not None: return inv # Next check if a weak reference is already stored. invweak = getattr(self, '_invweak', None) if invweak is not None: inv = invweak() # Try to resolve a strong reference and return it. if inv is not None: return inv # No luck. Compute the inverse reference and store it for subsequent use. inv = self._make_inverse() self._inv: BidictBase[VT, KT] | None = inv self._invweak: weakref.ReferenceType[BidictBase[VT, KT]] | None = None # Also store a weak reference back to `instance` on its inverse instance, so that # the second `.inverse` access in `bi.inverse.inverse` hits the cached weakref. inv._inv = None inv._invweak = weakref.ref(self) # In e.g. `bidict().inverse.inverse`, this design ensures that a strong reference # back to the original instance is retained before its refcount drops to zero, # avoiding an unintended potential deallocation. return inv def _make_inverse(self) -> BidictBase[VT, KT]: inv: BidictBase[VT, KT] = self._inv_cls() inv._fwdm = self._invm inv._invm = self._fwdm return inv @property def inv(self) -> BidictBase[VT, KT]: """Alias for :attr:`inverse`.""" return self.inverse def __repr__(self) -> str: """See :func:`repr`.""" clsname = self.__class__.__name__ items = self._repr_delegate(self.items()) if self else '' return f'{clsname}({items})' def values(self) -> BidictKeysView[VT]: """A set-like object providing a view on the contained values. Since the values of a bidict are equivalent to the keys of its inverse, this method returns a set-like object for this bidict's values rather than just a collections.abc.ValuesView. This object supports set operations like union and difference, and constant- rather than linear-time containment checks, and is no more expensive to provide than the less capable collections.abc.ValuesView would be. See :meth:`keys` for more information. """ return t.cast(BidictKeysView[VT], self.inverse.keys()) def keys(self) -> t.KeysView[KT]: """A set-like object providing a view on the contained keys. When *b._fwdm* is a :class:`dict`, *b.keys()* returns a *dict_keys* object that behaves exactly the same as *collections.abc.KeysView(b)*, except for - offering better performance - being reversible on Python 3.8+ - having a .mapping attribute in Python 3.10+ that exposes a mappingproxy to *b._fwdm*. """ fwdm = self._fwdm kv = fwdm.keys() if isinstance(fwdm, dict) else BidictKeysView(self) return kv def items(self) -> t.ItemsView[KT, VT]: """A set-like object providing a view on the contained items. When *b._fwdm* is a :class:`dict`, *b.items()* returns a *dict_items* object that behaves exactly the same as *collections.abc.ItemsView(b)*, except for: - offering better performance - being reversible on Python 3.8+ - having a .mapping attribute in Python 3.10+ that exposes a mappingproxy to *b._fwdm*. """ return self._fwdm.items() if isinstance(self._fwdm, dict) else super().items() # The inherited collections.abc.Mapping.__contains__() method is implemented by doing a `try` # `except KeyError` around `self[key]`. The following implementation is much faster, # especially in the missing case. def __contains__(self, key: t.Any) -> bool: """True if the mapping contains the specified key, else False.""" return key in self._fwdm # The inherited collections.abc.Mapping.__eq__() method is implemented in terms of an inefficient # `dict(self.items()) == dict(other.items())` comparison, so override it with a # more efficient implementation. def __eq__(self, other: object) -> bool: """*x.__eq__(other) ⟺ x == other* Equivalent to *dict(x.items()) == dict(other.items())* but more efficient. Note that :meth:`bidict's __eq__() ` implementation is inherited by subclasses, in particular by the ordered bidict subclasses, so even with ordered bidicts, :ref:`== comparison is order-insensitive ` (https://bidict.rtfd.io/other-bidict-types.html#eq-is-order-insensitive). *See also* :meth:`equals_order_sensitive` """ if isinstance(other, t.Mapping): return self._fwdm.items() == other.items() # Ref: https://docs.python.org/3/library/constants.html#NotImplemented return NotImplemented def equals_order_sensitive(self, other: object) -> bool: """Order-sensitive equality check. *See also* :ref:`eq-order-insensitive` (https://bidict.rtfd.io/other-bidict-types.html#eq-is-order-insensitive) """ if not isinstance(other, t.Mapping) or len(self) != len(other): return False return all(starmap(eq, zip(self.items(), other.items()))) def _dedup(self, key: KT, val: VT, on_dup: OnDup) -> DedupResult[KT, VT]: """Check *key* and *val* for any duplication in self. Handle any duplication as per the passed in *on_dup*. If (key, val) is already present, return None since writing (key, val) would be a no-op. If duplication is found and the corresponding :class:`~bidict.OnDupAction` is :attr:`~bidict.DROP_NEW`, return None. If duplication is found and the corresponding :class:`~bidict.OnDupAction` is :attr:`~bidict.RAISE`, raise the appropriate exception. If duplication is found and the corresponding :class:`~bidict.OnDupAction` is :attr:`~bidict.DROP_OLD`, or if no duplication is found, return *(oldkey, oldval)*. """ fwdm, invm = self._fwdm, self._invm oldval: OVT[VT] = fwdm.get(key, MISSING) oldkey: OKT[KT] = invm.get(val, MISSING) isdupkey, isdupval = oldval is not MISSING, oldkey is not MISSING if isdupkey and isdupval: if key == oldkey: assert val == oldval # (key, val) duplicates an existing item -> no-op. return None # key and val each duplicate a different existing item. if on_dup.kv is RAISE: raise KeyAndValueDuplicationError(key, val) if on_dup.kv is DROP_NEW: return None assert on_dup.kv is DROP_OLD # Fall through to the return statement on the last line. elif isdupkey: if on_dup.key is RAISE: raise KeyDuplicationError(key) if on_dup.key is DROP_NEW: return None assert on_dup.key is DROP_OLD # Fall through to the return statement on the last line. elif isdupval: if on_dup.val is RAISE: raise ValueDuplicationError(val) if on_dup.val is DROP_NEW: return None assert on_dup.val is DROP_OLD # Fall through to the return statement on the last line. # else neither isdupkey nor isdupval. return oldkey, oldval def _prep_write(self, newkey: KT, newval: VT, oldkey: OKT[KT], oldval: OVT[VT], save_unwrite: bool) -> PreparedWrite: """Given (newkey, newval) to insert, return the list of operations necessary to perform the write. *oldkey* and *oldval* are as returned by :meth:`_dedup`. If *save_unwrite* is true, also return the list of inverse operations necessary to undo the write. This design allows :meth:`_update` to roll back a partially applied update that fails part-way through when necessary. This design also allows subclasses that require additional operations to complete a write to easily extend this implementation. For example, :class:`bidict.OrderedBidictBase` calls this inherited implementation, and then extends the list of ops returned with additional operations needed to keep its internal linked list nodes consistent with its items' order as changes are made. """ fwdm, invm = self._fwdm, self._invm write: list[t.Callable[[], None]] = [ partial(fwdm.__setitem__, newkey, newval), partial(invm.__setitem__, newval, newkey), ] unwrite: list[t.Callable[[], None]] if oldval is MISSING and oldkey is MISSING: # no key or value duplication # {0: 1, 2: 3} + (4, 5) => {0: 1, 2: 3, 4: 5} unwrite = [ partial(fwdm.__delitem__, newkey), partial(invm.__delitem__, newval), ] if save_unwrite else [] elif oldval is not MISSING and oldkey is not MISSING: # key and value duplication across two different items # {0: 1, 2: 3} + (0, 3) => {0: 3} write.extend(( partial(fwdm.__delitem__, oldkey), partial(invm.__delitem__, oldval), )) unwrite = [ partial(fwdm.__setitem__, newkey, oldval), partial(invm.__setitem__, oldval, newkey), partial(fwdm.__setitem__, oldkey, newval), partial(invm.__setitem__, newval, oldkey), ] if save_unwrite else [] elif oldval is not MISSING: # just key duplication # {0: 1, 2: 3} + (2, 4) => {0: 1, 2: 4} write.append(partial(invm.__delitem__, oldval)) unwrite = [ partial(fwdm.__setitem__, newkey, oldval), partial(invm.__setitem__, oldval, newkey), partial(invm.__delitem__, newval), ] if save_unwrite else [] else: assert oldkey is not MISSING # just value duplication # {0: 1, 2: 3} + (4, 3) => {0: 1, 4: 3} write.append(partial(fwdm.__delitem__, oldkey)) unwrite = [ partial(fwdm.__setitem__, oldkey, newval), partial(invm.__setitem__, newval, oldkey), partial(fwdm.__delitem__, newkey), ] if save_unwrite else [] return write, unwrite def _update( self, arg: MapOrItems[KT, VT], kw: t.Mapping[str, VT] = MappingProxyType({}), *, rbof: bool | None = None, on_dup: OnDup | None = None, ) -> None: """Update, possibly rolling back on failure as per *rbof*.""" # Must process input in a single pass, since arg may be a generator. if not arg and not kw: return if on_dup is None: on_dup = self.on_dup if rbof is None: rbof = RAISE in on_dup if not self and not kw: if isinstance(arg, BidictBase): # can skip dup check self._init_from(arg) return # If arg is not a BidictBase, fall through to the general treatment below, # which includes duplication checking. (If arg is some BidirectionalMapping # that does not inherit from BidictBase, it's a foreign implementation, so we # perform duplication checking to err on the safe side.) # If we roll back on failure and we know that there are more updates to process than # already-contained items, our rollback strategy is to update a copy of self (without # rolling back on failure), and then to become the copy if all updates succeed. if rbof and isinstance(arg, t.Sized) and len(arg) + len(kw) > len(self): target = self.copy() target._update(arg, kw, rbof=False, on_dup=on_dup) self._init_from(target) return # There are more already-contained items than updates to process, or we don't know # how many updates there are to process. If we need to roll back on failure, # save a log of Unwrites as we update so we can undo changes if the update fails. unwrites: list[Unwrite] = [] append_unwrite = unwrites.append prep_write = self._prep_write for (key, val) in iteritems(arg, **kw): try: dedup_result = self._dedup(key, val, on_dup) except DuplicationError: if rbof: while unwrites: # apply saved unwrites unwrite = unwrites.pop() for unwriteop in unwrite: unwriteop() raise if dedup_result is None: # no-op continue write, unwrite = prep_write(key, val, *dedup_result, save_unwrite=rbof) for writeop in write: # apply the write writeop() if rbof and unwrite: # save the unwrite for later application if needed append_unwrite(unwrite) def copy(self: BT) -> BT: """Make a (shallow) copy of this bidict.""" # Could just `return self.__class__(self)` here, but the below is faster. The former # would copy this bidict's items into a new instance one at a time (checking for duplication # for each item), whereas the below copies from the backing mappings all at once, and foregoes # item-by-item duplication checking since the backing mappings have been checked already. return self._from_other(self.__class__, self) @staticmethod def _from_other(bt: t.Type[BT], other: MapOrItems[KT, VT], inv: bool = False) -> BT: """Fast, private constructor based on :meth:`_init_from`. If *inv* is true, return the inverse of the instance instead of the instance itself. (Useful for pickling with dynamically-generated inverse classes -- see :meth:`__reduce__`.) """ inst = bt() inst._init_from(other) return t.cast(BT, inst.inverse) if inv else inst def _init_from(self, other: MapOrItems[KT, VT]) -> None: """Fast init from *other*, bypassing item-by-item duplication checking.""" self._fwdm.clear() self._invm.clear() self._fwdm.update(other) # If other is a bidict, use its existing backing inverse mapping, otherwise # other could be a generator that's now exhausted, so invert self._fwdm on the fly. inv = other.inverse if isinstance(other, BidictBase) else inverted(self._fwdm) self._invm.update(inv) #: Used for the copy protocol. #: *See also* the :mod:`copy` module __copy__ = copy def __or__(self: BT, other: t.Mapping[KT, VT]) -> BT: """Return self|other.""" if not isinstance(other, t.Mapping): return NotImplemented new = self.copy() new._update(other, rbof=False) return new def __ror__(self: BT, other: t.Mapping[KT, VT]) -> BT: """Return other|self.""" if not isinstance(other, t.Mapping): return NotImplemented new = self.__class__(other) new._update(self, rbof=False) return new def __len__(self) -> int: """The number of contained items.""" return len(self._fwdm) def __iter__(self) -> t.Iterator[KT]: """Iterator over the contained keys.""" return iter(self._fwdm) def __getitem__(self, key: KT) -> VT: """*x.__getitem__(key) ⟺ x[key]*""" return self._fwdm[key] def __reduce__(self) -> tuple[t.Any, ...]: """Return state information for pickling.""" # If this bidict's class is dynamically generated, pickle the inverse instead, whose # (presumably not dynamically generated) class the caller is more likely to have a reference to # somewhere in sys.modules that pickle can discover. should_invert = isinstance(self, GeneratedBidictInverse) cls, init_from = (self._inv_cls, self.inverse) if should_invert else (self.__class__, self) return self._from_other, (cls, dict(init_from), should_invert) # type: ignore [call-overload] # See BidictBase._set_reversed() above. def _fwdm_reversed(self: BidictBase[KT, t.Any]) -> t.Iterator[KT]: """Iterator over the contained keys in reverse order.""" assert isinstance(self._fwdm, t.Reversible) return reversed(self._fwdm) BidictBase._init_class() class GeneratedBidictInverse: """Base class for dynamically-generated inverse bidict classes.""" # * Code review nav * #============================================================================== # ← Prev: _abc.py Current: _base.py Next: _frozenbidict.py → #============================================================================== bidict-0.22.1/bidict/_bidict.py000066400000000000000000000161751435416354600162640ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # * Code review nav * # (see comments in __init__.py) #============================================================================== # ← Prev: _frozenbidict.py Current: _bidict.py Next: _orderedbase.py → #============================================================================== """Provide :class:`MutableBidict`.""" from __future__ import annotations import typing as t from ._abc import MutableBidirectionalMapping from ._base import BidictBase, get_arg from ._dup import OnDup, ON_DUP_RAISE, ON_DUP_DROP_OLD from ._typing import KT, VT, DT, ODT, MISSING, Items, MapOrItems class MutableBidict(BidictBase[KT, VT], MutableBidirectionalMapping[KT, VT]): """Base class for mutable bidirectional mappings.""" if t.TYPE_CHECKING: @property def inverse(self) -> MutableBidict[VT, KT]: ... def _pop(self, key: KT) -> VT: val = self._fwdm.pop(key) del self._invm[val] return val def __delitem__(self, key: KT) -> None: """*x.__delitem__(y) ⟺ del x[y]*""" self._pop(key) def __setitem__(self, key: KT, val: VT) -> None: """Set the value for *key* to *val*. If *key* is already associated with *val*, this is a no-op. If *key* is already associated with a different value, the old value will be replaced with *val*, as with dict's :meth:`__setitem__`. If *val* is already associated with a different key, an exception is raised to protect against accidental removal of the key that's currently associated with *val*. Use :meth:`put` instead if you want to specify different behavior in the case that the provided key or value duplicates an existing one. Or use :meth:`forceput` to unconditionally associate *key* with *val*, replacing any existing items as necessary to preserve uniqueness. :raises bidict.ValueDuplicationError: if *val* duplicates that of an existing item. :raises bidict.KeyAndValueDuplicationError: if *key* duplicates the key of an existing item and *val* duplicates the value of a different existing item. """ self.put(key, val, on_dup=self.on_dup) def put(self, key: KT, val: VT, on_dup: OnDup = ON_DUP_RAISE) -> None: """Associate *key* with *val*, honoring the :class:`OnDup` given in *on_dup*. For example, if *on_dup* is :attr:`~bidict.ON_DUP_RAISE`, then *key* will be associated with *val* if and only if *key* is not already associated with an existing value and *val* is not already associated with an existing key, otherwise an exception will be raised. If *key* is already associated with *val*, this is a no-op. :raises bidict.KeyDuplicationError: if attempting to insert an item whose key only duplicates an existing item's, and *on_dup.key* is :attr:`~bidict.RAISE`. :raises bidict.ValueDuplicationError: if attempting to insert an item whose value only duplicates an existing item's, and *on_dup.val* is :attr:`~bidict.RAISE`. :raises bidict.KeyAndValueDuplicationError: if attempting to insert an item whose key duplicates one existing item's, and whose value duplicates another existing item's, and *on_dup.kv* is :attr:`~bidict.RAISE`. """ self._update([(key, val)], on_dup=on_dup) def forceput(self, key: KT, val: VT) -> None: """Associate *key* with *val* unconditionally. Replace any existing mappings containing key *key* or value *val* as necessary to preserve uniqueness. """ self.put(key, val, on_dup=ON_DUP_DROP_OLD) def clear(self) -> None: """Remove all items.""" self._fwdm.clear() self._invm.clear() @t.overload def pop(self, __key: KT) -> VT: ... @t.overload def pop(self, __key: KT, __default: DT = ...) -> VT | DT: ... def pop(self, key: KT, default: ODT[DT] = MISSING) -> VT | DT: """*x.pop(k[, d]) → v* Remove specified key and return the corresponding value. :raises KeyError: if *key* is not found and no *default* is provided. """ try: return self._pop(key) except KeyError: if default is MISSING: raise return default def popitem(self) -> tuple[KT, VT]: """*x.popitem() → (k, v)* Remove and return some item as a (key, value) pair. :raises KeyError: if *x* is empty. """ key, val = self._fwdm.popitem() del self._invm[val] return key, val @t.overload # type: ignore [override] # https://github.com/jab/bidict/pull/242#discussion_r825464731 def update(self, __m: t.Mapping[KT, VT], **kw: VT) -> None: ... @t.overload def update(self, __i: Items[KT, VT], **kw: VT) -> None: ... @t.overload def update(self, **kw: VT) -> None: ... def update(self, *args: MapOrItems[KT, VT], **kw: VT) -> None: """Like calling :meth:`putall` with *self.on_dup* passed for *on_dup*.""" if args or kw: self._update(get_arg(*args), kw) @t.overload def forceupdate(self, __m: t.Mapping[KT, VT], **kw: VT) -> None: ... @t.overload def forceupdate(self, __i: Items[KT, VT], **kw: VT) -> None: ... @t.overload def forceupdate(self, **kw: VT) -> None: ... def forceupdate(self, *args: MapOrItems[KT, VT], **kw: VT) -> None: """Like a bulk :meth:`forceput`.""" if args or kw: self._update(get_arg(*args), kw, on_dup=ON_DUP_DROP_OLD) def __ior__(self, other: t.Mapping[KT, VT]) -> MutableBidict[KT, VT]: """Return self|=other.""" self.update(other) return self @t.overload def putall(self, items: t.Mapping[KT, VT], on_dup: OnDup) -> None: ... @t.overload def putall(self, items: Items[KT, VT], on_dup: OnDup = ...) -> None: ... def putall(self, items: MapOrItems[KT, VT], on_dup: OnDup = ON_DUP_RAISE) -> None: """Like a bulk :meth:`put`. If one of the given items causes an exception to be raised, none of the items is inserted. """ if items: self._update(items, on_dup=on_dup) class bidict(MutableBidict[KT, VT]): """The main bidirectional mapping type. See :ref:`intro:Introduction` and :ref:`basic-usage:Basic Usage` to get started (also available at https://bidict.rtfd.io). """ if t.TYPE_CHECKING: @property def inverse(self) -> bidict[VT, KT]: ... # * Code review nav * #============================================================================== # ← Prev: _frozenbidict.py Current: _bidict.py Next: _orderedbase.py → #============================================================================== bidict-0.22.1/bidict/_dup.py000066400000000000000000000037021435416354600156060ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """Provide :class:`OnDup` and related functionality.""" from __future__ import annotations from enum import Enum import typing as t class OD(Enum): """An action to take to prevent duplication from occurring.""" #: Raise a :class:`~bidict.DuplicationError`. RAISE = 'RAISE' #: Overwrite existing items with new items. DROP_OLD = 'DROP_OLD' #: Keep existing items and drop new items. DROP_NEW = 'DROP_NEW' def __repr__(self) -> str: return f'{self.__class__.__name__}.{self.name}' RAISE: t.Final[OD] = OD.RAISE DROP_OLD: t.Final[OD] = OD.DROP_OLD DROP_NEW: t.Final[OD] = OD.DROP_NEW class OnDup(t.NamedTuple('_OnDup', [('key', OD), ('val', OD), ('kv', OD)])): r"""A 3-tuple of :class:`OD`\s specifying how to handle the 3 kinds of duplication. *See also* :ref:`basic-usage:Values Must Be Unique` (https://bidict.rtfd.io/basic-usage.html#values-must-be-unique) If *kv* is not specified, *val* will be used for *kv*. """ __slots__ = () def __new__(cls, key: OD = DROP_OLD, val: OD = RAISE, kv: OD | None = None) -> OnDup: """Override to provide user-friendly default values.""" return super().__new__(cls, key, val, kv or val) #: Default :class:`OnDup` used for the #: :meth:`~bidict.bidict.__init__`, #: :meth:`~bidict.bidict.__setitem__`, and #: :meth:`~bidict.bidict.update` methods. ON_DUP_DEFAULT: t.Final[OnDup] = OnDup(key=DROP_OLD, val=RAISE, kv=RAISE) #: An :class:`OnDup` whose members are all :obj:`RAISE`. ON_DUP_RAISE: t.Final[OnDup] = OnDup(key=RAISE, val=RAISE, kv=RAISE) #: An :class:`OnDup` whose members are all :obj:`DROP_OLD`. ON_DUP_DROP_OLD: t.Final[OnDup] = OnDup(key=DROP_OLD, val=DROP_OLD, kv=DROP_OLD) bidict-0.22.1/bidict/_exc.py000066400000000000000000000020511435416354600155710ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """Provide all bidict exceptions.""" from __future__ import annotations class BidictException(Exception): """Base class for bidict exceptions.""" class DuplicationError(BidictException): """Base class for exceptions raised when uniqueness is violated as per the :attr:~bidict.RAISE` :class:`~bidict.OnDupAction`. """ class KeyDuplicationError(DuplicationError): """Raised when a given key is not unique.""" class ValueDuplicationError(DuplicationError): """Raised when a given value is not unique.""" class KeyAndValueDuplicationError(KeyDuplicationError, ValueDuplicationError): """Raised when a given item's key and value are not unique. That is, its key duplicates that of another item, and its value duplicates that of a different other item. """ bidict-0.22.1/bidict/_frozenbidict.py000066400000000000000000000034461435416354600175050ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # * Code review nav * # (see comments in __init__.py) #============================================================================== # ← Prev: _base.py Current: _frozenbidict.py Next: _bidict.py → #============================================================================== """Provide :class:`frozenbidict`, an immutable, hashable bidirectional mapping type.""" from __future__ import annotations import typing as t from ._base import BidictBase from ._typing import KT, VT class frozenbidict(BidictBase[KT, VT]): """Immutable, hashable bidict type.""" _hash: int # Work around lack of support for higher-kinded types in Python. # Ref: https://github.com/python/typing/issues/548#issuecomment-621571821 if t.TYPE_CHECKING: @property def inverse(self) -> frozenbidict[VT, KT]: ... def __hash__(self) -> int: """The hash of this bidict as determined by its items.""" if getattr(self, '_hash', None) is None: # The following is like hash(frozenset(self.items())) # but more memory efficient. See also: https://bugs.python.org/issue46684 self._hash = t.ItemsView(self)._hash() return self._hash # * Code review nav * #============================================================================== # ← Prev: _base.py Current: _frozenbidict.py Next: _bidict.py → #============================================================================== bidict-0.22.1/bidict/_frozenordered.py000066400000000000000000000041031435416354600176620ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # * Code review nav * # (see comments in __init__.py) #============================================================================== #← Prev: _orderedbase.py Current: _frozenordered.py Next: _orderedbidict.py → #============================================================================== """Provide :class:`FrozenOrderedBidict`, an immutable, hashable, ordered bidict.""" from __future__ import annotations import typing as t from ._frozenbidict import frozenbidict from ._orderedbase import OrderedBidictBase from ._typing import KT, VT class FrozenOrderedBidict(OrderedBidictBase[KT, VT]): """Hashable, immutable, ordered bidict type. Like a hashable :class:`bidict.OrderedBidict` without the mutating APIs, or like a reversible :class:`bidict.frozenbidict` even on Python < 3.8. (All bidicts are order-preserving when never mutated, so frozenbidict is already order-preserving, but only on Python 3.8+, where dicts are reversible, are all bidicts (including frozenbidict) also reversible.) If you are using Python 3.8+, frozenbidict gives you everything that FrozenOrderedBidict gives you, but with less space overhead. On the other hand, using FrozenOrderedBidict when you are depending on the ordering of the items can make the ordering dependence more explicit. """ __hash__: t.Callable[[t.Any], int] = frozenbidict.__hash__ if t.TYPE_CHECKING: @property def inverse(self) -> FrozenOrderedBidict[VT, KT]: ... # * Code review nav * #============================================================================== #← Prev: _orderedbase.py Current: _frozenordered.py Next: _orderedbidict.py → #============================================================================== bidict-0.22.1/bidict/_iter.py000066400000000000000000000030011435416354600157510ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """Functions for iterating over items in a mapping.""" from __future__ import annotations from operator import itemgetter import typing as t from ._typing import KT, VT, ItemsIter, MapOrItems def iteritems_mapping_or_iterable(arg: MapOrItems[KT, VT]) -> ItemsIter[KT, VT]: """Yield the items in *arg* based on whether it's a mapping.""" yield from arg.items() if isinstance(arg, t.Mapping) else arg def iteritems(__arg: MapOrItems[KT, VT], **kw: VT) -> ItemsIter[KT, VT]: """Yield the items from *arg* and then any from *kw* in the order given.""" yield from iteritems_mapping_or_iterable(__arg) yield from kw.items() # type: ignore [misc] swap = itemgetter(1, 0) def inverted(arg: MapOrItems[KT, VT]) -> ItemsIter[VT, KT]: """Yield the inverse items of the provided object. If *arg* has a :func:`callable` ``__inverted__`` attribute, return the result of calling it. Otherwise, return an iterator over the items in `arg`, inverting each item on the fly. *See also* :attr:`bidict.BidirectionalMapping.__inverted__` """ invattr = getattr(arg, '__inverted__', None) if callable(invattr): inv: ItemsIter[VT, KT] = invattr() return inv return map(swap, iteritems_mapping_or_iterable(arg)) bidict-0.22.1/bidict/_named.py000066400000000000000000000076461435416354600161150ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """Provide :func:`bidict.namedbidict`.""" from __future__ import annotations from sys import _getframe import typing as t from ._base import BidictBase from ._bidict import bidict from ._typing import KT, VT class NamedBidictBase: """Base class that namedbidicts derive from.""" def namedbidict( typename: str, keyname: str, valname: str, *, base_type: t.Type[BidictBase[KT, VT]] = bidict, ) -> t.Type[BidictBase[KT, VT]]: r"""Create a new subclass of *base_type* with custom accessors. Like :func:`collections.namedtuple` for bidicts. The new class's ``__name__`` and ``__qualname__`` will be set to *typename*, and its ``__module__`` will be set to the caller's module. Instances of the new class will provide access to their :attr:`inverse ` instances via the custom *keyname*\_for property, and access to themselves via the custom *valname*\_for property. *See also* the :ref:`namedbidict usage documentation ` (https://bidict.rtfd.io/other-bidict-types.html#namedbidict) :raises ValueError: if any of the *typename*, *keyname*, or *valname* strings is not a valid Python identifier, or if *keyname == valname*. :raises TypeError: if *base_type* is not a :class:`bidict.BidictBase` subclass. Any of the concrete bidict types pictured in the :ref:`other-bidict-types:Bidict Types Diagram` may be provided (https://bidict.rtfd.io/other-bidict-types.html#bidict-types-diagram). """ if not issubclass(base_type, BidictBase): raise TypeError(f'{base_type} is not a BidictBase subclass') names = (typename, keyname, valname) if not all(map(str.isidentifier, names)) or keyname == valname: raise ValueError(names) basename = base_type.__name__ get_keyname = property(lambda self: keyname, doc='The keyname of this namedbidict.') get_valname = property(lambda self: valname, doc='The valname of this namedbidict.') val_by_key_name = f'{valname}_for' key_by_val_name = f'{keyname}_for' val_by_key_doc = f'{typename} forward {basename}: {keyname} -> {valname}' key_by_val_doc = f'{typename} inverse {basename}: {valname} -> {keyname}' get_val_by_key = property(lambda self: self, doc=val_by_key_doc) get_key_by_val = property(lambda self: self.inverse, doc=key_by_val_doc) class NamedBidict(base_type, NamedBidictBase): # type: ignore [valid-type,misc] # https://github.com/python/mypy/issues/5865 """NamedBidict.""" keyname = get_keyname valname = get_valname @classmethod def _inv_cls_dict_diff(cls) -> dict[str, t.Any]: base_diff = super()._inv_cls_dict_diff() return { **base_diff, 'keyname': get_valname, 'valname': get_keyname, val_by_key_name: get_key_by_val, key_by_val_name: get_val_by_key, } NamedInv = NamedBidict._inv_cls assert NamedInv is not NamedBidict, 'namedbidict classes are not their own inverses' setattr(NamedBidict, val_by_key_name, get_val_by_key) setattr(NamedBidict, key_by_val_name, get_key_by_val) NamedBidict.__name__ = NamedBidict.__qualname__ = typename NamedInv.__name__ = NamedInv.__qualname__ = f'{typename}Inv' NamedBidict.__doc__ = f'NamedBidict({basename}) {typename!r}: {keyname} -> {valname}' NamedInv.__doc__ = f'NamedBidictInv({basename}) {typename!r}: {valname} -> {keyname}' caller_module = _getframe(1).f_globals.get('__name__', '__main__') NamedBidict.__module__ = NamedInv.__module__ = caller_module return NamedBidict bidict-0.22.1/bidict/_orderedbase.py000066400000000000000000000220401435416354600172710ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # * Code review nav * # (see comments in __init__.py) #============================================================================== # ← Prev: _bidict.py Current: _orderedbase.py Next: _frozenordered.py → #============================================================================== """Provide :class:`OrderedBidictBase`.""" from __future__ import annotations from functools import partial from weakref import ref as weakref import typing as t from ._base import BidictBase, PreparedWrite from ._bidict import bidict from ._iter import iteritems from ._typing import KT, VT, OKT, OVT, MISSING, Items, MapOrItems IT = t.TypeVar('IT') # instance type AT = t.TypeVar('AT') # attr type class WeakAttr(t.Generic[IT, AT]): """Descriptor to automatically manage (de)referencing the given slot as a weakref. See https://docs.python.org/3/howto/descriptor.html#managed-attributes for an intro to using descriptors like this for managed attributes. """ def __init__(self, *, slot: str) -> None: self.slot = slot def __set__(self, instance: IT, value: AT) -> None: setattr(instance, self.slot, weakref(value)) def __get__(self, instance: IT, owner: t.Any) -> AT: return getattr(instance, self.slot)() # type: ignore [no-any-return] class Node: """A node in a circular doubly-linked list used to encode the order of items in an ordered bidict. A weak reference to the previous node is stored to avoid creating strong reference cycles. Referencing/dereferencing the weakref is handled automatically by :class:`WeakAttr`. """ prv: WeakAttr[Node, Node] = WeakAttr(slot='_prv_weak') __slots__ = ('_prv_weak', 'nxt', '__weakref__') def __init__(self, prv: Node, nxt: Node) -> None: self.prv = prv self.nxt = nxt def unlink(self) -> None: """Remove self from in between prv and nxt. Self's references to prv and nxt are retained so it can be relinked (see below). """ self.prv.nxt = self.nxt self.nxt.prv = self.prv def relink(self) -> None: """Restore self between prv and nxt after unlinking (see above).""" self.prv.nxt = self.nxt.prv = self class SentinelNode(Node): """Special node in a circular doubly-linked list that links the first node with the last node. When its next and previous references point back to itself it represents an empty list. """ nxt: WeakAttr['SentinelNode', Node] = WeakAttr(slot='_nxt_weak') # type: ignore [assignment] __slots__ = ('_nxt_weak',) def __init__(self) -> None: super().__init__(self, self) def iternodes(self, *, reverse: bool = False) -> t.Iterator[Node]: """Iterator yielding nodes in the requested order.""" attr = 'prv' if reverse else 'nxt' node = getattr(self, attr) while node is not self: yield node node = getattr(node, attr) def new_last_node(self) -> Node: """Create and return a new terminal node.""" old_last = self.prv new_last = Node(old_last, self) old_last.nxt = self.prv = new_last return new_last class OrderedBidictBase(BidictBase[KT, VT]): """Base class implementing an ordered :class:`BidirectionalMapping`.""" _repr_delegate: t.ClassVar[t.Any] = list _node_by_korv: bidict[t.Any, Node] _bykey: bool @t.overload def __init__(self, __m: t.Mapping[KT, VT], **kw: VT) -> None: ... @t.overload def __init__(self, __i: Items[KT, VT], **kw: VT) -> None: ... @t.overload def __init__(self, **kw: VT) -> None: ... def __init__(self, *args: MapOrItems[KT, VT], **kw: VT) -> None: """Make a new ordered bidirectional mapping. The signature behaves like that of :class:`dict`. Items passed in are added in the order they are passed, respecting the :attr:`on_dup` class attribute in the process. The order in which items are inserted is remembered, similar to :class:`collections.OrderedDict`. """ self._sntl = SentinelNode() self._node_by_korv = bidict() self._bykey = True super().__init__(*args, **kw) if t.TYPE_CHECKING: @property def inverse(self) -> OrderedBidictBase[VT, KT]: ... def _make_inverse(self) -> OrderedBidictBase[VT, KT]: inv = t.cast(OrderedBidictBase[VT, KT], super()._make_inverse()) inv._sntl = self._sntl inv._node_by_korv = self._node_by_korv inv._bykey = not self._bykey return inv def _assoc_node(self, node: Node, key: KT, val: VT) -> None: korv = key if self._bykey else val self._node_by_korv.forceput(korv, node) def _dissoc_node(self, node: Node) -> None: del self._node_by_korv.inverse[node] node.unlink() def _init_from(self, other: MapOrItems[KT, VT]) -> None: """See :meth:`BidictBase._init_from`.""" super()._init_from(other) bykey = self._bykey korv_by_node = self._node_by_korv.inverse korv_by_node.clear() korv_by_node_set = korv_by_node.__setitem__ self._sntl.nxt = self._sntl.prv = self._sntl new_node = self._sntl.new_last_node for (k, v) in iteritems(other): korv_by_node_set(new_node(), k if bykey else v) def _prep_write(self, newkey: KT, newval: VT, oldkey: OKT[KT], oldval: OVT[VT], save_unwrite: bool) -> PreparedWrite: """See :meth:`bidict.BidictBase._prep_write`.""" write, unwrite = super()._prep_write(newkey, newval, oldkey, oldval, save_unwrite) assoc, dissoc = self._assoc_node, self._dissoc_node node_by_korv, bykey = self._node_by_korv, self._bykey if oldval is MISSING and oldkey is MISSING: # no key or value duplication # {0: 1, 2: 3} + (4, 5) => {0: 1, 2: 3, 4: 5} newnode = self._sntl.new_last_node() write.append(partial(assoc, newnode, newkey, newval)) if save_unwrite: unwrite.append(partial(dissoc, newnode)) elif oldval is not MISSING and oldkey is not MISSING: # key and value duplication across two different items # {0: 1, 2: 3} + (0, 3) => {0: 3} # n1, n2 => n1 (collapse n1 and n2 into n1) # oldkey: 2, oldval: 1, oldnode: n2, newkey: 0, newval: 3, newnode: n1 if bykey: oldnode = node_by_korv[oldkey] newnode = node_by_korv[newkey] else: oldnode = node_by_korv[newval] newnode = node_by_korv[oldval] write.extend(( partial(dissoc, oldnode), partial(assoc, newnode, newkey, newval), )) if save_unwrite: unwrite.extend(( partial(assoc, newnode, newkey, oldval), partial(assoc, oldnode, oldkey, newval), partial(oldnode.relink,), )) elif oldval is not MISSING: # just key duplication # {0: 1, 2: 3} + (2, 4) => {0: 1, 2: 4} # oldkey: MISSING, oldval: 3, newkey: 2, newval: 4 node = node_by_korv[newkey if bykey else oldval] write.append(partial(assoc, node, newkey, newval)) if save_unwrite: unwrite.append(partial(assoc, node, newkey, oldval)) else: assert oldkey is not MISSING # just value duplication # {0: 1, 2: 3} + (4, 3) => {0: 1, 4: 3} # oldkey: 2, oldval: MISSING, newkey: 4, newval: 3 node = node_by_korv[oldkey if bykey else newval] write.append(partial(assoc, node, newkey, newval)) if save_unwrite: unwrite.append(partial(assoc, node, oldkey, newval)) return write, unwrite def __iter__(self) -> t.Iterator[KT]: """Iterator over the contained keys in insertion order.""" return self._iter(reverse=False) def __reversed__(self) -> t.Iterator[KT]: """Iterator over the contained keys in reverse insertion order.""" return self._iter(reverse=True) def _iter(self, *, reverse: bool = False) -> t.Iterator[KT]: nodes = self._sntl.iternodes(reverse=reverse) korv_by_node = self._node_by_korv.inverse if self._bykey: for node in nodes: yield korv_by_node[node] else: key_by_val = self._invm for node in nodes: val = korv_by_node[node] yield key_by_val[val] # * Code review nav * #============================================================================== # ← Prev: _bidict.py Current: _orderedbase.py Next: _frozenordered.py → #============================================================================== bidict-0.22.1/bidict/_orderedbidict.py000066400000000000000000000154341435416354600176260ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # * Code review nav * # (see comments in __init__.py) #============================================================================== # ← Prev: _frozenordered.py Current: _orderedbidict.py #============================================================================== """Provide :class:`OrderedBidict`.""" from __future__ import annotations from collections.abc import Set import typing as t from ._base import BidictKeysView from ._bidict import MutableBidict from ._orderedbase import OrderedBidictBase from ._typing import KT, VT class OrderedBidict(OrderedBidictBase[KT, VT], MutableBidict[KT, VT]): """Mutable bidict type that maintains items in insertion order.""" if t.TYPE_CHECKING: @property def inverse(self) -> OrderedBidict[VT, KT]: ... def clear(self) -> None: """Remove all items.""" super().clear() self._node_by_korv.clear() self._sntl.nxt = self._sntl.prv = self._sntl def _pop(self, key: KT) -> VT: val = super()._pop(key) node = self._node_by_korv[key if self._bykey else val] self._dissoc_node(node) return val def popitem(self, last: bool = True) -> tuple[KT, VT]: """*b.popitem() → (k, v)* If *last* is true, remove and return the most recently added item as a (key, value) pair. Otherwise, remove and return the least recently added item. :raises KeyError: if *b* is empty. """ if not self: raise KeyError('OrderedBidict is empty') node = getattr(self._sntl, 'prv' if last else 'nxt') korv = self._node_by_korv.inverse[node] if self._bykey: return korv, self._pop(korv) return self.inverse._pop(korv), korv def move_to_end(self, key: KT, last: bool = True) -> None: """Move the item with the given key to the end if *last* is true, else to the beginning. :raises KeyError: if *key* is missing """ korv = key if self._bykey else self._fwdm[key] node = self._node_by_korv[korv] node.prv.nxt = node.nxt node.nxt.prv = node.prv sntl = self._sntl if last: lastnode = sntl.prv node.prv = lastnode node.nxt = sntl sntl.prv = lastnode.nxt = node else: firstnode = sntl.nxt node.prv = sntl node.nxt = firstnode sntl.nxt = firstnode.prv = node # Override the keys() and items() implementations inherited from BidictBase, # which may delegate to the backing _fwdm dict, since this is a mutable ordered bidict, # and therefore the ordering of items can get out of sync with the backing mappings # after mutation. (Need not override values() because it delegates to .inverse.keys().) def keys(self) -> t.KeysView[KT]: """A set-like object providing a view on the contained keys.""" return _OrderedBidictKeysView(self) def items(self) -> t.ItemsView[KT, VT]: """A set-like object providing a view on the contained items.""" return _OrderedBidictItemsView(self) # The following MappingView implementations use the __iter__ implementations # inherited from their superclass counterparts in collections.abc, so they # continue to yield items in the correct order even after an OrderedBidict # is mutated. They also provide a __reversed__ implementation, which is not # provided by the collections.abc superclasses. class _OrderedBidictKeysView(BidictKeysView[KT]): _mapping: OrderedBidict[KT, t.Any] def __reversed__(self) -> t.Iterator[KT]: return reversed(self._mapping) class _OrderedBidictItemsView(t.ItemsView[KT, VT]): _mapping: OrderedBidict[KT, VT] def __reversed__(self) -> t.Iterator[tuple[KT, VT]]: ob = self._mapping for key in reversed(ob): yield key, ob[key] # For better performance, make _OrderedBidictKeysView and _OrderedBidictItemsView delegate # to backing dicts for the methods they inherit from collections.abc.Set. (Cannot delegate # for __iter__ and __reversed__ since they are order-sensitive.) See also: https://bugs.python.org/issue46713 def _override_set_methods_to_use_backing_dict( cls: t.Type[_OrderedBidictKeysView[KT]] | t.Type[_OrderedBidictItemsView[KT, t.Any]], viewname: str, _setmethodnames: t.Iterable[str] = ( '__lt__', '__le__', '__gt__', '__ge__', '__eq__', '__ne__', '__sub__', '__rsub__', '__or__', '__ror__', '__xor__', '__rxor__', '__and__', '__rand__', 'isdisjoint', ) ) -> None: def make_proxy_method(methodname: str) -> t.Any: def method(self: _OrderedBidictKeysView[KT] | _OrderedBidictItemsView[KT, t.Any], *args: t.Any) -> t.Any: fwdm = self._mapping._fwdm if not isinstance(fwdm, dict): # dict view speedup not available, fall back to Set's implementation. return getattr(Set, methodname)(self, *args) fwdm_dict_view = getattr(fwdm, viewname)() fwdm_dict_view_method = getattr(fwdm_dict_view, methodname) if len(args) != 1 or not isinstance(args[0], self.__class__) or not isinstance(args[0]._mapping._fwdm, dict): return fwdm_dict_view_method(*args) # self and arg are both _OrderedBidictKeysViews or _OrderedBidictItemsViews whose bidicts are backed by a dict. # Use arg's backing dict's corresponding view instead of arg. Otherwise, e.g. `ob1.keys() < ob2.keys()` would give # "TypeError: '<' not supported between instances of '_OrderedBidictKeysView' and '_OrderedBidictKeysView'", because # both `dict_keys(ob1).__lt__(ob2.keys()) is NotImplemented` and `dict_keys(ob2).__gt__(ob1.keys()) is NotImplemented`. arg_dict = args[0]._mapping._fwdm arg_dict_view = getattr(arg_dict, viewname)() return fwdm_dict_view_method(arg_dict_view) method.__name__ = methodname method.__qualname__ = f'{cls.__qualname__}.{methodname}' return method for name in _setmethodnames: setattr(cls, name, make_proxy_method(name)) _override_set_methods_to_use_backing_dict(_OrderedBidictKeysView, 'keys') _override_set_methods_to_use_backing_dict(_OrderedBidictItemsView, 'items') # * Code review nav * #============================================================================== # ← Prev: _frozenordered.py Current: _orderedbidict.py #============================================================================== bidict-0.22.1/bidict/_typing.py000066400000000000000000000022161435416354600163270ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """Provide typing-related objects.""" from __future__ import annotations from enum import Enum import typing as t if t.TYPE_CHECKING: from typing_extensions import TypeAlias as TypeAlias else: TypeAlias = 'TypeAlias' KT = t.TypeVar('KT') VT = t.TypeVar('VT') Items: TypeAlias = 't.Iterable[tuple[KT, VT]]' MapOrItems: TypeAlias = 't.Mapping[KT, VT] | Items[KT, VT]' ItemsIter: TypeAlias = 't.Iterator[tuple[KT, VT]]' class MissingT(Enum): """Sentinel used to represent none/missing when None itself can't be used.""" MISSING = 'MISSING' def __repr__(self) -> str: return '' MISSING: t.Final[MissingT] = MissingT.MISSING OKT: TypeAlias = 'KT | MissingT' #: optional key type OVT: TypeAlias = 'VT | MissingT' #: optional value type DT = t.TypeVar('DT') #: for default arguments ODT: TypeAlias = 'DT | MissingT' #: optional default arg type bidict-0.22.1/bidict/metadata.py000066400000000000000000000010761435416354600164410ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """Define bidict package metadata.""" __version__ = '0.22.1' __author__ = {'name': 'Joshua Bronson', 'email': 'jabronson@gmail.com'} __copyright__ = '© 2009-2022 Joshua Bronson' __description__ = 'The bidirectional mapping library for Python.' __license__ = 'MPL 2.0' __url__ = 'https://bidict.readthedocs.io' bidict-0.22.1/bidict/py.typed000066400000000000000000000000201435416354600157720ustar00rootroot00000000000000PEP-561 marker. bidict-0.22.1/codecov.yml000066400000000000000000000001321435416354600152060ustar00rootroot00000000000000coverage: status: project: default: target: 99% threshold: 1% bidict-0.22.1/dev-deps/000077500000000000000000000000001435416354600145545ustar00rootroot00000000000000bidict-0.22.1/dev-deps/dev.in000066400000000000000000000001001435416354600156510ustar00rootroot00000000000000-r lint.txt -r docs.txt -r test.txt pip-tools pytest-icdiff tox bidict-0.22.1/dev-deps/dev.txt000066400000000000000000000124411435416354600160750ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile --allow-unsafe --resolver=backtracking dev-deps/dev.in # alabaster==0.7.12 # via # -r dev-deps/docs.txt # -r dev-deps/test.txt # sphinx attrs==22.2.0 # via # -r dev-deps/test.txt # hypothesis # pytest babel==2.11.0 # via # -r dev-deps/docs.txt # -r dev-deps/test.txt # sphinx beautifulsoup4==4.11.1 # via # -r dev-deps/docs.txt # furo build==0.9.0 # via pip-tools cachetools==5.2.0 # via tox certifi==2022.12.7 # via # -r dev-deps/docs.txt # -r dev-deps/test.txt # requests cfgv==3.3.1 # via # -r dev-deps/lint.txt # pre-commit chardet==5.1.0 # via tox charset-normalizer==2.1.1 # via # -r dev-deps/docs.txt # -r dev-deps/test.txt # requests click==8.1.3 # via pip-tools colorama==0.4.6 # via tox coverage[toml]==7.0.1 # via # -r dev-deps/test.txt # pytest-cov distlib==0.3.6 # via # -r dev-deps/lint.txt # virtualenv docutils==0.19 # via # -r dev-deps/docs.txt # -r dev-deps/test.txt # sphinx execnet==1.9.0 # via # -r dev-deps/test.txt # pytest-xdist filelock==3.8.2 # via # -r dev-deps/lint.txt # tox # virtualenv furo==2022.12.7 # via -r dev-deps/docs.txt hypothesis==6.61.0 # via -r dev-deps/test.txt icdiff==2.0.6 # via pytest-icdiff identify==2.5.11 # via # -r dev-deps/lint.txt # pre-commit idna==3.4 # via # -r dev-deps/docs.txt # -r dev-deps/test.txt # requests imagesize==1.4.1 # via # -r dev-deps/docs.txt # -r dev-deps/test.txt # sphinx iniconfig==1.1.1 # via # -r dev-deps/test.txt # pytest jinja2==3.1.2 # via # -r dev-deps/docs.txt # -r dev-deps/test.txt # sphinx markupsafe==2.1.1 # via # -r dev-deps/docs.txt # -r dev-deps/test.txt # jinja2 nodeenv==1.7.0 # via # -r dev-deps/lint.txt # pre-commit packaging==22.0 # via # -r dev-deps/docs.txt # -r dev-deps/test.txt # build # pyproject-api # pytest # sphinx # tox pep517==0.13.0 # via build pip==22.3.1 # via pip-tools pip-tools==6.12.1 # via -r dev-deps/dev.in platformdirs==2.6.0 # via # -r dev-deps/lint.txt # tox # virtualenv pluggy==1.0.0 # via # -r dev-deps/test.txt # pytest # tox pprintpp==0.4.0 # via pytest-icdiff pre-commit==2.20.0 # via -r dev-deps/lint.txt py-cpuinfo==9.0.0 # via # -r dev-deps/test.txt # pytest-benchmark pygal==3.0.0 # via # -r dev-deps/test.txt # pytest-benchmark pygaljs==1.0.2 # via # -r dev-deps/test.txt # pytest-benchmark pygments==2.13.0 # via # -r dev-deps/docs.txt # -r dev-deps/test.txt # furo # sphinx pyproject-api==1.2.1 # via tox pytest==7.2.0 # via # -r dev-deps/test.txt # pytest-benchmark # pytest-cov # pytest-icdiff # pytest-xdist pytest-benchmark[histogram]==4.0.0 # via -r dev-deps/test.txt pytest-cov==4.0.0 # via -r dev-deps/test.txt pytest-icdiff==0.6 # via -r dev-deps/dev.in pytest-xdist==3.1.0 # via -r dev-deps/test.txt pytz==2022.7 # via # -r dev-deps/docs.txt # -r dev-deps/test.txt # babel pyyaml==6.0 # via # -r dev-deps/lint.txt # pre-commit requests==2.28.1 # via # -r dev-deps/docs.txt # -r dev-deps/test.txt # sphinx setuptools==65.6.3 # via # -r dev-deps/lint.txt # nodeenv # pip-tools snowballstemmer==2.2.0 # via # -r dev-deps/docs.txt # -r dev-deps/test.txt # sphinx sortedcollections==2.1.0 # via -r dev-deps/test.txt sortedcontainers==2.4.0 # via # -r dev-deps/test.txt # hypothesis # sortedcollections soupsieve==2.3.2.post1 # via # -r dev-deps/docs.txt # beautifulsoup4 sphinx==5.3.0 # via # -r dev-deps/docs.txt # -r dev-deps/test.txt # furo # sphinx-basic-ng # sphinx-copybutton sphinx-basic-ng==1.0.0b1 # via # -r dev-deps/docs.txt # furo sphinx-copybutton==0.5.1 # via -r dev-deps/docs.txt sphinxcontrib-applehelp==1.0.2 # via # -r dev-deps/docs.txt # -r dev-deps/test.txt # sphinx sphinxcontrib-devhelp==1.0.2 # via # -r dev-deps/docs.txt # -r dev-deps/test.txt # sphinx sphinxcontrib-htmlhelp==2.0.0 # via # -r dev-deps/docs.txt # -r dev-deps/test.txt # sphinx sphinxcontrib-jsmath==1.0.1 # via # -r dev-deps/docs.txt # -r dev-deps/test.txt # sphinx sphinxcontrib-qthelp==1.0.3 # via # -r dev-deps/docs.txt # -r dev-deps/test.txt # sphinx sphinxcontrib-serializinghtml==1.1.5 # via # -r dev-deps/docs.txt # -r dev-deps/test.txt # sphinx toml==0.10.2 # via # -r dev-deps/lint.txt # pre-commit tox==4.0.16 # via -r dev-deps/dev.in urllib3==1.26.13 # via # -r dev-deps/docs.txt # -r dev-deps/test.txt # requests virtualenv==20.17.1 # via # -r dev-deps/lint.txt # pre-commit # tox wheel==0.38.4 # via pip-tools bidict-0.22.1/dev-deps/docs.txt000066400000000000000000000025531435416354600162520ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile --allow-unsafe --extra=docs --output-file=dev-deps/docs.txt --resolver=backtracking pyproject.toml # alabaster==0.7.12 # via sphinx babel==2.11.0 # via sphinx beautifulsoup4==4.11.1 # via furo certifi==2022.12.7 # via requests charset-normalizer==2.1.1 # via requests docutils==0.19 # via sphinx furo==2022.12.7 # via bidict (pyproject.toml) idna==3.4 # via requests imagesize==1.4.1 # via sphinx jinja2==3.1.2 # via sphinx markupsafe==2.1.1 # via jinja2 packaging==22.0 # via sphinx pygments==2.13.0 # via # furo # sphinx pytz==2022.7 # via babel requests==2.28.1 # via sphinx snowballstemmer==2.2.0 # via sphinx soupsieve==2.3.2.post1 # via beautifulsoup4 sphinx==5.3.0 # via # bidict (pyproject.toml) # furo # sphinx-basic-ng # sphinx-copybutton sphinx-basic-ng==1.0.0b1 # via furo sphinx-copybutton==0.5.1 # via bidict (pyproject.toml) sphinxcontrib-applehelp==1.0.2 # via sphinx sphinxcontrib-devhelp==1.0.2 # via sphinx sphinxcontrib-htmlhelp==2.0.0 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.5 # via sphinx urllib3==1.26.13 # via requests bidict-0.22.1/dev-deps/lint.txt000066400000000000000000000011661435416354600162670ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile --allow-unsafe --extra=lint --output-file=dev-deps/lint.txt --resolver=backtracking pyproject.toml # cfgv==3.3.1 # via pre-commit distlib==0.3.6 # via virtualenv filelock==3.8.2 # via virtualenv identify==2.5.11 # via pre-commit nodeenv==1.7.0 # via pre-commit platformdirs==2.6.0 # via virtualenv pre-commit==2.20.0 # via bidict (pyproject.toml) pyyaml==6.0 # via pre-commit setuptools==65.6.3 # via nodeenv toml==0.10.2 # via pre-commit virtualenv==20.17.1 # via pre-commit bidict-0.22.1/dev-deps/test.txt000066400000000000000000000036151435416354600163010ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile --allow-unsafe --extra=test --output-file=dev-deps/test.txt --resolver=backtracking pyproject.toml # alabaster==0.7.12 # via sphinx attrs==22.2.0 # via # hypothesis # pytest babel==2.11.0 # via sphinx certifi==2022.12.7 # via requests charset-normalizer==2.1.1 # via requests coverage[toml]==7.0.1 # via pytest-cov docutils==0.19 # via sphinx execnet==1.9.0 # via pytest-xdist hypothesis==6.61.0 # via bidict (pyproject.toml) idna==3.4 # via requests imagesize==1.4.1 # via sphinx iniconfig==1.1.1 # via pytest jinja2==3.1.2 # via sphinx markupsafe==2.1.1 # via jinja2 packaging==22.0 # via # pytest # sphinx pluggy==1.0.0 # via pytest py-cpuinfo==9.0.0 # via pytest-benchmark pygal==3.0.0 # via pytest-benchmark pygaljs==1.0.2 # via pytest-benchmark pygments==2.13.0 # via sphinx pytest==7.2.0 # via # bidict (pyproject.toml) # pytest-benchmark # pytest-cov # pytest-xdist pytest-benchmark[histogram]==4.0.0 # via bidict (pyproject.toml) pytest-cov==4.0.0 # via bidict (pyproject.toml) pytest-xdist==3.1.0 # via bidict (pyproject.toml) pytz==2022.7 # via babel requests==2.28.1 # via sphinx snowballstemmer==2.2.0 # via sphinx sortedcollections==2.1.0 # via bidict (pyproject.toml) sortedcontainers==2.4.0 # via # bidict (pyproject.toml) # hypothesis # sortedcollections sphinx==5.3.0 # via bidict (pyproject.toml) sphinxcontrib-applehelp==1.0.2 # via sphinx sphinxcontrib-devhelp==1.0.2 # via sphinx sphinxcontrib-htmlhelp==2.0.0 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.5 # via sphinx urllib3==1.26.13 # via requests bidict-0.22.1/docs/000077500000000000000000000000001435416354600137755ustar00rootroot00000000000000bidict-0.22.1/docs/Makefile000066400000000000000000000151521435416354600154410ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/bidict.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/bidict.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/bidict" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/bidict" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." bidict-0.22.1/docs/README.rst000077700000000000000000000000001435416354600173612../README.rstustar00rootroot00000000000000bidict-0.22.1/docs/_static/000077500000000000000000000000001435416354600154235ustar00rootroot00000000000000bidict-0.22.1/docs/_static/bidict-types-diagram.dot000066400000000000000000000034621435416354600221420ustar00rootroot00000000000000// Copyright 2009-2022 Joshua Bronson. All rights reserved. // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // See build-bidict-types-diagram.sh for how to generate a PNG from this file. digraph G { rankdir=BT dpi=300 node [fontsize="12", shape="box"] subgraph ABCs { node [fillcolor="#EFEFEF", color="#666666", fontcolor="#333333", style="filled", fontname="OperatorMono Nerd Font Book Italic"] Mapping [label="collections.abc.Mapping"] MutableMapping [label="collections.abc.MutableMapping"] Hashable [label="collections.abc.Hashable"] MutableMapping -> Mapping { rank=same Mapping MutableMapping Hashable } BidirectionalMapping [label="bidict._abc.BidirectionalMapping", style="filled, bold", fontcolor="black", fontname="OperatorMono Nerd Font Book Italic"] MutableBidirectionalMapping [label="bidict._abc.MutableBidirectionalMapping", style="filled, bold", fontcolor="black", fontname="OperatorMono Nerd Font Book Italic"] BidirectionalMapping -> Mapping MutableBidirectionalMapping -> BidirectionalMapping MutableBidirectionalMapping -> MutableMapping } subgraph { node [style="bold", fontname="OperatorMono Nerd Font Book"] bidict [label="bidict.bidict"] frozenbidict [label="bidict.frozenbidict"] OrderedBidict [label="bidict.OrderedBidict"] FrozenOrderedBidict [label="bidict.FrozenOrderedBidict"] bidict -> { MutableBidirectionalMapping } OrderedBidict -> { MutableBidirectionalMapping } FrozenOrderedBidict -> { BidirectionalMapping, Hashable } frozenbidict -> { BidirectionalMapping, Hashable } { rank=same bidict frozenbidict OrderedBidict FrozenOrderedBidict } } } bidict-0.22.1/docs/_static/bidict-types-diagram.png000066400000000000000000002315221435416354600221400ustar00rootroot00000000000000PNG  IHDR ;ҟbKGD̿3 IDATxg$eշYE1笏>D`DrI(>" "HI**A(Iɒ"iaw ya)&tLW]קݞ>wuU52qˀ@@@@@@@@@@@@@`$IwRhV[Iñ=I錮ݧR`8[I v$I v$)$].I]`WK vHd]eHKQ;"؝f һ`l I<ݟ%va M`$I`$]`W+%IR+%I`$) vI$]`WK`WK].IR+$)쒤`WK v$I vI].IR+%IR+%I`A]`$I`$]`W+%IRK vI].I].I v] v$I v$)$].I]`WK vI vI].I v]$] vI]`$I.IR+%I v$I v$I`$].I].I v$I`WK`$)$]$]`WK]`$I`$]`W+%IRK vI].I].IR+%I`WK`WK].IR+$)%I v$I`$I vI].I v]$] vI]`$I.IR+%I v$IRK vIR+$)쒤`WK v$I v$)$].I]`WK vI vIR+%I v]$] vI]`$I.IR+%I v$I v$)$].I].I v$I`WK`$)$]$] v$) vI vIR+%I v]$$I`$)$)$] v$I v$)$].I]`WK vI vI].I v]$] vI]`$I.IR+%I v$I v$I`$].I].I v$I`WK`$)$x\Wc][).&{c:Hg^F?w?zέ_vO9+rD*ƖtTKā }/sX!*Qj3#ѻy{n^:~!O>'vvJT+-wnYr϶/YHS1n?D%*JHg^F?zέ_vOĉ1m9" v꘏E%*qU[,w4N?w~E-zb$z3o/#b[/]ɧXĞ*z#R`^+º1-AD%o㒝tf')]'s{~Q%XY18B;=3K{nzw9:ޭse v{5ܼ]TJCF]nagTWJTbU>۶dg>97!;fbX2*QW}}aE[hG"G{k/Ӛ`w8&Xέw:Foق v{?53oן?ݝsS\nҌ`w+B|,֋JTbո5s&ȿdg!JT:m5&ܹ"/ݣ*^\ّg(˴&].bX6 v[i,Kֻunln/ޟOӟ~5^_7~Ik:V}`W[9Oɝ6-YwHtCe/87ײv7|N~Rg+]vv$:/n$2 v\0/Yn>ezέcÖ-`SsvQiKNWI'*1-ǎnWv8#}rmY^hAޗF]^n-(Ht3_H!_EnӟƲtn[1Cnޟt'i~%U_2V|f+vB+gjLkITK9:;6Hz'@׭i~ջl߷ǽW(Ht3_H!1WXO[1^ v{?9ԟ&ZiMVՓҫ>5u)F `wO4C C섨D%w1ȴinɑ>Ɠu^jnn:<;{:]ԎIkqߙD#!6浿gT׶vr mn$:/j$Hbh٣Hv||+n`[{wY:wܽ׻1fa:`T#K\G135>۴\6 v ,vucX2֍]I,sC Qeb;NĮTTƝs۰IX"֌'}~h?NSX-kqd8֊7Q,zL,[{8x[UL/tV8;n9iZ;zVl+E%։oO!g1tsE%U3l6qX9S8"[vr$:?yrR߉cFLLu\+y˻i845cX*֊mĸ7΋sqJi!?. vww띻{w:FE':`S GS?YznZhYeP\i\s l19"Ϳ vj,=87ѿ%D%6'[G0j~/۰`\V]{[Z3Z/gĴXcO^=k|$K?srűeFއ{i9.٨D%V񚣖,kgq?FTb1-b1=Ỷj)mf -b$:;yrOzmxp֘lU;y˿ivoqU|JMH%ZN1IJqx<nzwwݽݹwc/:1v,~<Sգue7d}h4 m19"Ϳ vtNX^s25Y&fҸ†[6,좃nSxeD1 SG$yF`<8ک|/T;k0Ѝt&tbqA}Ϙ%oMբ-g>H#.Y.VKkf/bVH4S-ӫKF%<+re-^;+Mao5 vһչwj.C.G(_tcvOYO|d9WoQy+wJ}rZqդ6 vy[=^:tE8+=ֹ߰sӫgj?;׹YsKN{ ضEyftJQY%G7M^y￴_ۚゥT_9o\ū4`t˖|n&1m#v&sD%֬wB;?f҈{ko\&w|{{2߮{F}!1-nks[]7;w9zwY;FE;`}}GSY]#_6eF*ogbS#{ `aUwտpZu2𣪾*g K+?91ϒϞ7YӚʌcT>47f;#ǽp{ޭ~>-K mjoT{u]?.Xd->RX7*V=x{vI2U]鵯LR]v$:;rwv׳k\D7t'V-w/woh$IqO22u~ݽٹһ1/vSYOsd9كiL >S#{ `7L?wq3g;-U}utG6tϠ}mKIl\%Ǥ5%M\XqjZvIV3#qhz*K5:_jK4tϚ^RQ>v|ET` ƌ.Z7:ܢ^B;?u}ߩtԖ-o/o1} ӫpom@ v}- v7Z얩wsz]]Ύ~QL'՟R#=⮾2yGCHbrDo!*w 便~]'ϱlRu#zQg֊;'yzd'JgI0VFH7H 4n[GbtiޣubZwvG%Sb0dO1^n‡5mi\[z4moӝ~svKFhkս-B;?fg`ܜ^ݩe[\R牻q^zumC,ۺxc n8{\{w/wrvb:`=}GS?YƵVUO5S#n*S#{ `7ۦ -ɄX}vڗ1;ݯd oGqc Mv|yɇ_,DzM~+GwU*_ջbt3ew{Ub>xp7^~YWϩsR5&@׸ّoԭRcx/E%VMkz; AO2p Ht3o$}v-o/wo1|AޥU:h`wEc22u^ݽݹһ1/SYOtd2RU72FW|S#f]n$:÷~ϜXŎ+;Sgq[\?>JZc2#:XY_{ӘKϡwԳ+.|~_fx7չ e |JG3+h36s;1[4CkF&N[hGӟ#L]Hy9E[lV`.S;,֍3uջԹ{wv.O.gh(cvOxd?ϑ` urgzu1W&Tp[5[v9"mVn>[g*}7 mi.lv15`2mbF/H{^ewHk8#1H4SX.o/wo:{鮎iC}l3˵8ҝ'/bؾEO+W.S۝,!mG?uߑOG1;=֓V6S#O;Zc)i`Wn޵6k>-rqUim|Gp8 I^2#TTi\m F9ܶnO^'Vvꪇ*Cٲ-#|h.ԭ95w|{{iuΉV8(Ӛ{w:weEu n;],~:_G ;y5[v9+VM:_ͷi:Rg\v够$ﹶgsX[ ÷AՋ- sy5w7c4ߵ~ʶ5U$.jt Ht3o$eN:Uf^dmry2yYb܅`-'ݷŮqŘ`<\_{w;wYzw9;FEQCێ`W#K,㠺pw=;wfzZ#{ `7mo޼Vݻ] {*gQ>9.#4.9d_u28-nK֑Lw3ƻ$n0y7MD3{U= ^顖|cޓw6>Ac|<-Mȶ-#|ޑ_eVlp˖˿ɻXO%]v_[ąb`,l{w;wYzw;FgE1Cێ`W#K,c0sÖU&ֻ|-ˏ,uC|:o~yUh%iq[K|$=#0"| ys:c4׈GTgweEcI!-]ח7~&z)ى/M8_X&e\n:Z6Qe6|&򼿩)}=Ev*&f`|mnܒ-g>H`2UcX?[˥C.w/164r6P5;[얫wsz]]ѩ~QD'՟R*e#}0ۺiY7[̴6 vsQћ^qy.n>کq0cz6,yWlU:_W:v8z'#>=$b+5V6>gչ; =I*?x,_̏=8hŸ'>5ol=u \n\6DVpsݻO.~U^u;$}7QFsybF3fF"_]S \ޛw{fA]n^Vݽbq@l}[]۽;wzw:F'E;`]MGS,y;ֽ%ٝb[U#xvz26 vsyml=fwRg_1==e=_6}z*dlMךYrv.:5SYm\%Ź 6b8/; NTYUvM.IdpȆqވ 'aS3ctg 'L#?wi#S',;8-kxI5ƷbFF"O]ίbF<^}].^ٽE-qnv_svܻܽswc/1 vn:ԟ}d;\-\o8W*wrl1e:"mfo! 8+n-#^$u̯񫸬M[m86NΎe38-jhqRg^fFbu. x"Ύ38%N⸮-gFI;bI2s8>fUpG>8+sqgψoED?BSđqDOb.ךLEL/I". vջҹ{wK]QthW3՟Rrdʴ~o1e;"1`WK옻{:\9t{v_o㹦{w]:!`$i]dGLR6'bbjiQ1cv$) vI {vg,m>=~Ү vWe-%ctW vIR+$t-Wwe׸3fƣ1;fǬx0n?_`||Nw vI1] v$Ɇ=6CO>}m- vCKR!쒤`W+%I6^@-W=D_:W$u C+%I`WKlD%*|rE+qJߦ*%c].I v]d[n1=Nrxfľw|3qz\o_1t `$].I%SK vIR+$)쒤`WK v$I vI].IR+%I]`WK]`$I`$]`W+%IR+%I`$) vI$]`WK`WK].IR+$)쒤`WK v$I vI].IR+%IR+%I`$) vI vIR+%I v]$$I`$)$)쒤`W+%I`WK`WK].IR+$)%I v$I`$I`$)쒤`W+%IR+%I`$) vI$]`WK`WK vIR+$)쒤`WK v$I vI].IR+%IR+%I v$) vI vIR+%I v]$$I`$)$)$] v$I v$)$].I]`WK vI vI].I v]$] vI]`$I.IR+%I v$IRK vIR+$)쒤`WK v$I v$)$].I]`WK vI vIR+%I v]$] vI]`$I.IR+%I v$I v$)$].I].I v$I`WK`$)$]$] v$) vI vIR+%I v]$$I`$)$)$] v$I v$)$].I]}"]`W+%9U d<3.Q vdiF ( vIQU d\;T vd*W>].VX/f)%K=@|@(MX|7)/?mn v'Yj1UȖ5J,@ q*pg`P[;xS,~Q~ BqQ``l.rXR%۽.Yn_o쒝sxG @XH vIvķ@ @`z?Ղ].ٰ[bI,h^15 v$ib3O|F5H.~(uT5( r,uWvfkD(}=2bE=Ȗye KAЈb`8_wwgd 5XܨC #7е6gIxl݀a)qoA25[b.rШ)[~%t)X,Lqrn@ *#n1[(+ǎ|șc@W23ձNT@]. ub N-h}ʙ9KƓ:ӯ>$<W uU߂i1kra.*SP6@ۙ++,Ǜ v*poZ. 6P ]S# h+wƔ5Kgy2i*=u?A K~4FP_@37u_j`M?X#ހF ,kr$@oB4U9!uܲ{xYCG `@y{s h m'-R4q8j5>u E Pf>o'hh;,Qndvx)< 4ܑ6V6f us-2 Pfn"؝'( GGzEfĽ,]e氜Ը0* <}RK#b]aB,]e&ϔ 1-P:@9H||/%'c:y5|nx@D ̜D Vk"}HM̛=㚈[eūggjv?Nz@lAi6`vȞkz˳7t ]׋f+lv{4 ,:o(!(۳!c^"^?**tmlj!Pfb+M/Г7{drGT?ر`lCt.23XgBH6Z(P*@[۔zQ_!3c5MwEIt .2bSu282b V6kA{2@1-˿'Sǎ>Rт~va:`@Yt$bX5fdO>~gT68>NLicl[^ƛS?zE:^8^BiM1@5^xoTb?5@(71xM|5ꟳf(0vOh9D q]\qVvb \]A8\^y=ۧ 4vIPK>tR@Op=R=KmC t]Qi2oܧԇNS aջОG 6vW)q>J=8ugz=&wn;vISӊ0J]7J=YxGep݁`@pgLIaޜzЅJ=IYg{π݁`@Z?tפtR@OrE,E=>kA]`@lrb:SF)7^k}2@7 /NQMF_,Zקwz.{ixT10vX5˯// = vsb4 b:Ɠ!=>dE5&4-b:1/.~lub:YCC}O;.i"R:ĽY_xeڻO飘s]#+N=nz =#M >z߫|Mi*%tRO)GoՁb_һFt]4Y8f+\:+zq.{2l@7 o<|A.K}J=׳X=1oo3.c4yosA;oQ Nb}CMl@7 \MW @ۙ} Pc^}O)݀`@?RIӑ5vJ]g1so>1}YwtdJܡ6sj:W (5dW'l@7 Џ7^&${(͜zJ%X5u׌ t]^iBh<rV1u?*\jF v'ǼiJrba|Z)źo}\RI݀`@25MI>sF:J%k! }][`G _}v2lPJnWeq}^Q?m@w7+GYݾ`Vm@v/)|&%6{7*Yr 1%UVG w2MJU @r6;)I.(HD,qR#&%7ISC)d|#u#""ޘ*rR#=TRR@yvʸE9K59W)o>%K+MzJ%X |"UdG 9'P7dG (VLU9J)o^wVKJ%_,S9F1|J`@41Y S @X!u)SrF ĝA y<tFL}DR,xG9ư}J`LSw8hK}4pX)f*8LR)_yh9L]l7 @v b49YE)-g1Q rnW}8C9rhjJ`"~}gf7sR@WH#;.8P9jT(.D<oLӓb^zJ]ӱtn5yч(.DD쓦'*,z5J]4ug&MUzR#b4A9R1-e1<7*t-bw,N*P<]b4Ay|ܮХ<}+R*P<]hOf~O@SArw@v` JhG ٞG1!ezL10Ǥ O$N5L)KI h 7dt?b71iޑF #Y#MR>pU,P *~=Bh@v`$I1-W^EJ)qr42r?V h0?ibZE)kx$ޙ}; N;T)h4͌Wޣ%<KgF1WAvӕ(.f0^&*)iJ]e1`nlź3 0_LC)XO7dzhS=}Xr`T(.3yROYZ) 832nV\|#UpcF Y!4 8: X #'M5\K)x.ϺN1MqX(V ( K/4ώW XPteufMP ([UiO<%NW`*P,]O }P{%+Z#Z<oH]S/_)<d!F1WAZ‹1%E ~"/bj%)tUl< -婪X `j@̛,G+ N,}W<-M(P,]Ǻiʲ X!uc:ʙ ^7+GKy9J`qavb`GNT Bi;oF9ZL%$E H|,Ӕ:} nzVJR"O1 A@wom೩Q XPīҴhw.B` /,L< m`T߽(.L.ixZ1 E.T ;e*R"3{5\b7&J5R"Y-M\RdCQ h;^wrS?@v`b~J1 1o 7*[cѴϝ)G9,yUE b;M]6W @<]1<(G[9!y)E {ixT1,dpGh'O2wØ mTŔ(.L9=I3;Q hsclxFAoS X09Ns$eA1=۾+SsE(.LY$f㿊m1%kܐ=^"ɲlIr[쾠n6oZ9:=Yw{J1"d9=ܨI1l~rtA0Y˗0)JJm,brtgߩ@v` ?aᘭIpYU h9/,`L< eU H0y0L RxR@9u?TTK(.4FixQ12#(Xuqrtw{\P(]h+RXL)>K{q7UtD QIS50! X4_ĩQL?N)"@cM!P J-xovQ@v1M=0GR@x:@lsO#-D W,O*Z31dMv+ {gLU gnlźK JcR](Ǽ(؞ddT 'JźG0vMR](MӘ ]u~{Ԃ""qr>i6R @ .S vIqX1ecu hkc4 7~rP5@H ]$W4+ qx*$I v9GXw(SX|L).ob4W1 %IRBX=Wevy](;ŀ`$I..YT.h`J߳'hk;-I$ p`K8>u_w+GWpM@T,hk$IWnW/ ]mYb](dk]$hEӜ{+庈cيv|J/*$I vre~Usq_1.b@K`-X17 E󤑹Y1.c4b@K`-dnlźS e"]()[;yŀ`$I.ZYxT97R]('ͦ*$I v"Ί)iHܤ]`RiBR@K`-X8Ͳ_+GWx* ;.I]4lCRVN#tR](+fΝ]$hz6,N9R](/)+⟊.I]4,](MS @ 塘?MjT vI vbbP98}Y).Ӥb@K`99!u_w+GW4R[( 2sI6b@K`C̗f ƕ|;պJv ?am`$I.rp[,SzOR](7Ǥiɾ$)E e_rϤQxuO)Gp>`zDg޸O1 %IRg<Ϣ}O6v.TUiuŀ`$In_\Nj s̍yޤ`z]'>$)#bJ<="iT w߁Q'" EE.vl(g;[,Oԋ]Sc9O ޡXb9=vFWI_QDZgv7-ݙf[3,~3 ]Y*2'4v)(n`bۺݴ8|o@c3@KQEQ4vb=I$cxhlU0@c(hjafz8|lw% .QzEQEci+{h=k\N]t򠱻L34Po9=m~O_/ƶ/boKOlqnyUm[c͖(OԜ ;BY=du,9uRC9ܣ z ^϶k5Y>O/B1=zl 'ڛ['c99#ꇀr-Oc7=﫱qfK]q 4@5v2p|V2~eų4ϲGjm:[ŝm*Uq# ;,=Uz 'ژ )%:̼߳VC9r6Ȏ|Y}ճ{zP[}6;>׵RͣZ LSѱ;#fs2517;?d8aGmqn=޺/ז :ЎTE ;@cli݄zi6#м|lvڤ|T~m={hiBZWuv͂&:#}"_I1̚>^TdZ/.݂uqfw% .IfڳG1^S//*̻ӎWI`5&k'󸘟E7r7BA'ۂ5+gCGѺUO4v ԭvwB͖oE]Iv:a@B 2ҏwC]cmJ͟/|\f[ލ-8YZ HݵhU#miz 0z8g} >L€ݚSۛ{T[G]jf'yݯUKu?a1\4]'q:51g'"pBZ;*]'Īț+my֗Y%\%;B}5J{Y|t>=ofvKܮ ^L+=^TH uvHK!0G]Tkn Yr9r5Os7}vB>>~Wqll&Lߴ v .fVk6v'_M`4=1^CW< YO?z\H&1k}fz!ЩtmO(DuN ȯ>#.-IR=.6Q4v=PP?N& .2"] LbeZ1gfL?hwڥ# _`[:miN[j[sQ'03u.<m\q}L+u4O6ݷ\ݗB:ѴG;j$GMYmc/G "7ʑWt\MǾޣrLhFL?.-\l>7f4v=V}8@c/||X_USi61h~\n$n 5Z9Oqu_o8*̑ZgGʑcy]fݣ1 ]M46=5-͖| $l;jҥhf1,t@c ; H𰱻MFNx}=|E^ s,iIN q{8_̝8{FCNuӶLsTg8Ϳͻ{m]W'Be%),#4#GWǨcqáDjrO3>9Z5%vT^ҿ~?X—?C[km\&nۗm4XۚG?)=G>АBfM߇c.nٝľ{{Y¶p}&3Qc?&qmRHE3VŬCf'|GkCMPH!=i/~. S[#{L̶Iݝͭ\J@L#]k30Qcofݗ {MsoR3j:| RVs9r5li>t/ .-hgYaF{JpZ LFf;gDɼdo{h/{I|2a2}'5#G؟Umk/12E6Zt~Ӳ[f >\ Ej.9 ^syS -Q4v 7ӟZG[bWvNv:9w Jv[IO,B<'ל$ۺLupңy/rugh~:H זƺ5(!-.rJmnǬ,{9fqF i.5EwH=1ܣ[6, 7}us,Gڦ.WvlM!UF4;ۥ4\8ͭLL`IwhKc=gak%D`ki€']Mƽ٤6XΩF{~ֻ"NLvBϼ60/ x[o2>6k8#6smx?BCz׺Y'wd?vc.PDcMKty34S縺/G,NҴvNznH! #Gۘ^/ 9d]&oZs.}"-^ᕸzM4vcf?$I1(ٵ#7v+Wx'鳄d7OpyިYR'ȓxUn^[OvghXsuۭ!=%t 76j!R/ǶUz$h>j2?-Pgy:"6Ipl |b֨Ufa )z_]o7]bZ%nMBtl 4NSS͢C'rKMeI\ ;kD3Nc-Ie9e:^mV<Ϣ:' ]hT ]g ڌ$iJ҅ 5Ӓ45rCs[<:֥N#!oԓ 탒:8"K!bHo)Qa3;9r>ozyqo]L٧vf[DŽ+.G6ݜYdA @X"{@c#|a&ZJqc7|՛qo-—4'xni oLe*9ꖗ 139m}%ju~#Or2I؝L#(2;) ]&~281'9w~- `G@>z2{&X`YbGH׬/<}i{ xڋ##],o:4L&YhevjS;-nnQwۖ4vsJٌzkPi}@cL{F@+B\kqi\\/qyJ^o gfyL5y-3=%{KdM4Z"&h r.KWqK~',)"MOr]ݗ sus&oϴ2w~U^I/wktl .5#AUǙw9Bc7'&V1YCQ4v HS9l~E"@ݒsU^WiN^/WsvIziuy#yؽvֺe%]M4ӓ]s^:B7:$ljy"g}"d']5^2ΚݺF&9:|d.Kh/]c^kW=<+<"V:Q˒\El n^XWiBZ7O-{\TQ#D ̲L A}MYc3^uQ)9vpĜj,NzSzWoFtYK-TEuEC9.޷lR 3y.t9%᪷uyQC] k{{y }thoH/EzQܝmc]Ƭ*}i.ְk/a^{|oz=(xߣ]}\[>-[+^x#|ƮCB$@m?0$뵥]Ucw]m M7?IGU@iBZT:XFi^ud)Ztʳ~Bz6j{]wDu<*4\t4K{m=M-V[ݨw5G2^_Gc|ghַ4YsMԳZ Woɗ*g{qmDݫwj}"d+tVv ly]MЩof/xYdp3jZ1I׎)rY7=.mУ7ODlg#\ TUM'ijY_$ qc{h@\cA-3a 'UjUsGMtsJBs4ZZ_ks쪢k RH4NMuB m]$^2Kjmj ~ƫ#G:zn'ir&qDQO1F]5>Iwsּ+\ؗbWĭyjgX'Ź_͟Pֹ ?/R'xik&>.- iFD=z4v]!|!jI>0,FGIc$A5DW;g=å1(GJ;E|z$xu^O-umU3C.Ycw-={}6;I[/Fh:{9i;KN'̜lLyE}DZKĞc[E=K2ݗ6Yq]cOONֿ=FdOdAL| ԳH]FtAʋ M5LѐP3N> .͉)\4vgZ G~ ZQϰѸ'/ErQD\as4^WES{40lO_Ovuh}2}f^K}XĖ/xs}gh:RTG#4~XNk ^o6;:Oz$Pg?r:fk-k1{%wWOfR:EnL+^ܣܥٸR -K2}\[>-ֹ7[0AC[:Q4v h޳7 5ͺYzG 2x%zYwL$n^Sݫ_kߣ˯anݭIZNݺL7^r%zS鑘K,M'95my4UE=GoI*W*Z$ݯqulZ>I2ѕI|&A@wy}yľ =״8pV=Z^si=}-Xyguɫ\1=CRM])rإ(^G oUs,4v]1Ů\{st8s%Y_-4kQAu3+/{j,GFubs9GG u /otQ4v x~S{30@cwZKծ"HjE]sg=.jNr/t_::Rp$"@ًF4vinKۓ(ʠ^0!y8;~b -tUؽ湝3-CJ.4cOD`:L]EQT5$qWiҦn]y9xՠ5vL&eHUfԜF]W4 Kc(JzɑVZ㼯t`ݣ99z`5v&&Fz)Rv7G@cZۛ )Ac.EQV-վ:&\2}\}ԃ*y:C'DE9M;Wm|,2>3n .]Ac.EQEAd*cHKx D ]O4viREQ]l;\ULǐٞ(\̔]EQEc#mZd iz~@c"3)z0hإ((öujS1c3~Z@cl?3)ڇ(hإ((xM >c"LJfia4v %>' 4v)(nMQK3,֋LÐwՄڨ̤\ Kc(hbmm[rw2 CVQ0d7SV]EQEc7gjې;)2ю [&fZtaإKQEQ4vsZ'v~Z YhfF44v N3Ӣm]EQEc7GF۶Z YdD`N+ .](g{d,mgFh@n&F]EQEcu5;Lf<=I]EA4viREQ]WUG{CL}x;Q4v UJ .](UuG3+4#j Q4vWQ+& 4v)(ƮK6`=K%'1u Q4v?3إKQEQ4v]'Mkv%q!@cqfr ._)(Ʈ m.Ϥ ܌ ޶nTLc.EQEkf)\TfF DHR?3=N4vbMQEQ4v4U-X/0Ⴋf֡DHfz@?](hfat .k^DHүjc&HW](hfl۶YLqft# .fA](hfZ'۶Zd {Ōm 1KEfaإ((nFmmr}3@cv" EQE^3h!'1֐(D KQEQ4MR3lf!Gfo-,!UiH](h{u d!gqb]u"5 KQEQݔV_n)r7;@cJ$& KQEQmPl;Jb!%Q4v4@4v)(m[w7akkF l?G4v)(dgzڌ׈ 4L' EQEحj`)+x`'3E]@qiAc((ɿ嫵;fG10Q4v6(0hREQEc7I0G—껛(hef\+ .EQEQ4vXktt*xfu7@cm7pEQEMZ'۶ZǴ 9ӌ+ u*m* KQEQ8mmI19Q4va' .EQEQonjOF|k/b ]@\LF](Fml;Y&QG(?@c_05 KQEQT?m *d"RMftaإ((*ݵӶdzIEf4~G]@"w: KQTD2()Bf<3@cwOF@c5(N*'kl[1}BҌȷ H 3iړ(إ(ʟT[E\Gc_ '^fL@]@b]Jtu$ydYgɑ w?HVh֓qt$nfTV@c&0Ӧ.EFdC4DGA-Tl/e]Ǜc)߾9jC\'EcC߫4'̨|(dn1Ӧ&ZJ4v)ʅpCEpծQ5iQiR4v=ZgM#̸(d)'v5\i_ T?mjunj՟$]A&φ: հh4K͑:qGh&S Q4vɝi&Nݵ0hRT:S#tjoWouS;5OKʹWs\A4Tg). bILWFy1Q4v}a\w.EEVuPs{tk/v}}/Ej:X)]5)̟<( 6Sg֨Cru(u" =\R;_ǩN u50{T3}„ yz3>@cP' :VU=F.9&߿X.ir}*o{%䡻=(/´ޞ{!apeں 5`]59&vZ^8QZih3v8iFDx.k䩥~& "5vXeJO^nbl[Zː]{ڽ @֗(/BH>am&zH]wc5@KNpJbhqSkfv' K4vphOY@*v\%(ު.wNZkғ}yf% K4vpg'oǸfkw3'3~ljf"_悹kS9P,{8OpMg fhP誵DB$I4LE4v"@:CK-,wdؽVmPqā?](|wITnpC*{-ݴ=s ⴂ~_ycp{e k[GB(({Qޡ /fu=aʵMGJJ"F&{|&1Xw#j9IrH?eQ]0@Z2m&Re:%Ŗ>ZMM.Ϡ{>WS#> Yf(_|4Xcx >T'463" ;4v6j+3:0۠k}'iqzCʍ)pCTk"'ޏ(Lia>=-JZ'l걟:u VkxJ3r%ޚ0Apq`=iFnDx.2{ R}LKTvL*"gi^R/T "h%C)bY#"VH ͋JynzޔmOGcRjqk[rjhF{S}L(pOoVt zUo&_IF*q;֣=n:: Iд}鸨Y Էjcgn " +4vO֩NP~QiVe=[:duZ}%:v||Խk kÇk;;o"vD' +4v_2өVZM@Z2iuQYY*[BuҺZ'zgOxUu4_]{}KTd7x.ˏ (@U1]T .ACV]G"jeڨr{YGT l% ~+Q^ ~tTu䡯4JMcZvBBYuuȨ%s54*4aI=O|Щ\x.,{QV$ {hRD{em!*UQU3VJ%f;_'(?ĨtBAώΚG+?(z~y0?hoCWh 1:UDQKGA֪3Ffi=oM4܍V8{_?QgirQn'i% E̸y(2RjXBAlw@`#4v6\0A{U p;#"ctl~x :rS%wjk]Ogo3 k)*`k*hji1+b BAN,W/;n afI7h@\aXm0Mya`vM)᪍*WT*h tJ7h@SN2A5(X%h#=BaZ@(pQNkwF >pQޠ pd E\cTJ+9ukETB#6*W%ۑCKUjs]7엍iʢ?uVBD݌D#uBԞf \0^EO_D7h@0TkdTbU߉xی@9Sw32d57㩁^!'I7h@PiVCN$B%vEp Q%K3QK!I;r_ 26O[ڱq gC]_L# jUjhLKXc={(rlT]͟=Gs\>2abā{ 'h@p\`&Z0U1M*tA=8g~c{]BA6 ::L % 4v 8>MʵULSJhPOn EYHPى lzH4v H2S=Dej2L4Go؜ zydƨIħ@WH(Hvt(O `~|F*UӘ.A=[nG䷄ᙯgԧ-#M5M,Vd$ieFx.&0S6I#լ9RD݌ʧCU 4Pwjk`+B)0/` DK TJ ik>&ؼ(<t8B? X^v\O/,f*cLSJ4hgn4s?!3ƨP_#| Jf0/9Lk#a 0V\bmTD<-i)֋%*[ TGA@EmXw]/W:r!%v$z򊶎hZO(ncc-! JwIh@ 6ӭFGµ*8^y㈢R{B#G;SB$9QJӈ?' 4v x_SY ~6Ip Q%k! 3# O.8JJ}fD4+2!<뵥p]H*Ujnq FE=[25$VoJ-X~8!D LuPnhѠln[0_9*RVJ@]H881Dx."{}Yr6i:& ;Fبr{#GPmX9:ځN3{h@cD3*€/hiT~#f'<1GE} BB KQ!O. 4v ޳_N$ i*Zӑx*#H(FƪuħL[eynM@Bg@c*J1D6ITJAƌC",Q6i&l8$n4qDx.CfU JmL4 fLoAyg:F|4m"yOT[& 4v ~S{30Pp֩B4u[T? |b#Ҩ_ [^v[PG޲Q^ uvR((5F]bTD_`F DmuKu#XUx! 4v L! T1MmUD9Čk"OjYKTnA!HӍ(/ ;žD 俩* U%|*3ʏ ]HR,\C@`6h XiR Zͨ0^ƪe'U{%P8{br.[H-ԫ0WL[4uTV  > w5u~$v[\Ұ;3 =tԫ\`7_o\krGE*W*cH @ZN@K`/I,a LQ4uB3^ cJ>h:KQ!{Dxu.~fQMҰnA@Ͱ{OQP*UXSqNLAyn-480{<@c0~)ԗ A1M*ѷDV7Fg8r>#1ўFX.fz(ܣ h,:0P~U4u[T >F(@j U5bEz8 1{?=F3jO-U4u;5 IW<( ZVOm6e_f/(ܣ 0xnJ,[hIgt'5U#>T6@NT5a=I:L&€PIql5׎4[,#QcOkJxPRFqiK=I~ȭjUjhLKXTK$6;XPGi!䙛r WCcPcw3;(3U1M*t)EXkGWx4rٛ@@xܮG ʵULSJh$.+~-qQkžA2qYOG{4v5֩\Dej2L4@tۖ(|b:F|6ֶwnr?C@]eOB\MpliLSwUh)fϦXI>J>wBGjW-Xpv h~T3 ;a4Žp Q%J)ۨffy0|5u\lZG(Jp6>Փ(ܣ ,ߎ4ސWJ ik>& MtQom/Q_>ev Q8WkD]foۯ72^1D%A4@7{D;F}Vk~%O=jBpd;s4vDVyEhjJh =lDC4F#>/{MBD}cGkfϴ]@4~ 'mLSʵh,LO0|+9B 3[OlaFcW1 iJ4* jbSU[Prlzۼ#r. rR2ITJ.dٯ" QGi>h>QU^+r. ,ۢ{0P*UjnS\4]UB'/#ٔ7\ak4v1SD$֩B}bMTYD!HNTG u|i!q9cok4v^_x>! ĵZv*@N|f0R">[LEI-%q9sD]@*4 h{pT9'9N̾60!QQ+Ʉ/e2q9% hjLk a 4SBȩf(}mIPZC(Y[l3(\ mMcEAU f;(dFA'vzP=9б|BcOЕ*UjpLKFpj0̞׏(fD|D%ˎIf_(\ gEm*NL~$CYQO.@(f؇8c盽|r. !rQn'i%M >1a,IFc#hV*|į*6d״uԟFk$04PkfJ@$6~z0|kʴELSwKiy?" hJmߑ~P&U ̇09Fc@3-;(|iJ  gZO\rVEf%`"r. +̴- A1M*@[mO+BQԚU+ڈv9 =paEc<50Q~U4u[T (~Q \>,ПU:&?G'[4vu9L0 R}LS"`d T"S{Ѣ+l }ԋf/|(ܢ  D(HsTf1M*D;(b 7'A?fŠɶf?|(ܢ KvdAB%v1p Q%G_m7@yLc)P}56G hW'>E@nԭLkVƴt5L PV_ i)ysߎP>fO|(ܢ /jm&gFAX i6V Pzq@Rj,;:=Aԫ#̾x;QEc 䬣FE*ZZD4@;FG- lA|7^K@n;{ZӄǖLb]T[J@>:wHv}Գsx QEcq0{ER5i .fn b!S4FM"U}Nox\=r$QEc +gw&iD)5GUh_Yno )Z{F0B|8G)6ﮥɁ5!|@%EU]Un" &U߮25q}lOsP7Im͹5 @և` vC!BE{<؝mtugV4b`}s[>nNx;`vB!vt3Յ *8v3($sa."B9._MW2*۹l9꯷s|F\35@ijeX@ !Ba0ˇՅqE%*9ntswԯXPcjuV.B!n$癛BP[&*/ ^}}ƚy_L>+v`!B`7M8֪R-\n?Լ>8`vB!v#dON/)5b1Z5_}V.B!n$k6C:U+ n jS ] E!"؍fgs|;^,3tPĸ#<QӦv ] E!"؍&)tz!ԪF]\nGUiju˾ L@B!Dyk:pꄈq؟%m%Ppkjx ]B!h&e~ gzGg >c."BF󻚛BRJ"bfz[ l= 3v`!B`7m]t{!OxW&*ӯP,Vv`!B`gBR[GMưc30`vB!v6oG:(<%*J(0%V.B!n4ZEl;Xש+Xc,@B!DUvDt|!qH^C"FbL(=+v`!B`!̍}t|!༢jmOjUh1 @`."B64s|_:u#c:&ba]N?@B!Dې*;c)@pYMmp[c(usEmT焗J+v`!B`!/v䞣fFFS`@^sb."B651Ο@Rq=+Ŗ~(Oj6|)V.B!n,2@7n%vO '\h`vB!vcqu. ƆuR{op䨉J 6!WZ|2V.B!n,n7λZډHzTpw3+L @B!Dͭs bp]#Se Wcj ]B!XsN 2XIThj0 xݝ`vB!vc35=G,f_(h%Do-- gvkA[QNPaN47p cYjvTj ojmO ؅`#AL=>xg]/Y;bqˏG*1,nDiQ ?m[ ڊv&%O[ͦlv vbl9i b󉩱m`v#G}ҼkjyryGɣ::fF1m-h+י|Pڶ󟾿kv\a9@@ ٺʢ` HJ=7CSivl1Y!GN-[{?J>QL7]lGcB|%QQ+PgzejcdOQ"Evۊl++IN䲍Ʌ 3=ޔEt!;=#GXr;у#G~`pHOK=4_Zg2ڼ˅Y;brt:*Ïφ|0ֶ֗O$j+9r]H[X74>E6ۊl++IN䮍ɍ 3ؽєt!l뷏~q>ҶDJHSK? ]^[|Aݠ;>N]a.A>)Ïφluݺ[ޖ|%qY|M߾67sթi8GwmkͶ"J -dgRkcrB* v_3W\rm6Ӓi*՛ p05tXrhpfL,_L2]+sŢONRmS%ZUv\sOA]}ґBZQmm⎘!jf#hWNםSwلaR.vW/FR{T+T0ZAQΔ&y #]Kuw\2F8Gg$檫9C?ș5X]䨉=3c[xj-R;D#fGŴ ]Dk*Ӷ G-NRU€wnХ:'*?hKT;g}oռ mEW\$KuUf~{O/%G ~j/Q35צOG\un3{`:+5~5nh*BG`=_vq{t6q:AF-T2rĐhpt'ٹsu-Kދ޵9:@.[WK9:\ED[sz$Ƹrh |vKJ~90vb. &"h^3=ƶf~h-o^?g5.wq#jh&%>#5?oܪy^vwuT9q}Oo%n cx\M]vvS"-.mx-߷H##Zm ]v!Pnt;rЭbjjIv\7D}|?bH+t918>&0K4y~SɧW-4EڮA9r9)Q{^O2[^-ef ȏ$?Ըgoz;rU紵HxO^R~j7x}u4mS%qZ9V+=g+WHtF}%8zu[x=̶̘u8˞|:%Heq ˈDVاit!om2RWU5@N)5" ؅`ǧ/ч涷Օ+ nΚGw_^cFGN-0CZuC*2wxy:^x;h+o H{KtҽVhSɧWa_Z7Jh9bˍccf5>|Kbt1ɗȑ΂]׶V./1[uN[FM\#ttFqƉɚZuX m4[Ǧ7v-+mgz%jEquFi eG}d;m mRmꞚ!X@ vo#QUE1Y:hKT4YzX3 gF- 5`"PY_r7 4EnG^}O%89jef |IWWQFzk%oF֥7\ZjBvl-~1[e53t.[N8:{gG6Lx{/V& @Q+oB:׺;ǺNxmcgx$Guk>^{-v>jd vn=. YUTk}3zÕf*/@v`]:XGl;eF{f^7!hU Px'#tg87 5 >ؼ拀ƺP-]S۟Tuy9v4b4ai˹g]:lcg$昇㏌l3*.^l@ 㨝by#ۚќrZV<}lY_j@3zώfmJǙMhUc,U>%xfrCđ>>O.yژtsQ8C#۞锼 ώY7,v)ފ9ح0NH7v&=Z 5w6x 'nW`nip p8G#7z͌@<%#w&>13>r{@cif,1"𯪏Zv#Gҥ!ri#r/k4^_1ΰ!NmSͶvt` 宵HLO1/5򶯎mJޙ+=bn[Kõqw? /["|~\F> G(s>qE!X]=7Z/BL"jՁ!(mbW'Ha̎]¿?eI""zNmJ֙vDy~xNKcٶu[x?sb3>{zJքuw;uoʴ=F SZsгn ގҍ]жaEu'ocH/3UofScXW0p9x7?]7|*D]hX<ᅳпNp{e~e%~у$GUGLT_]l'_~6Z`M&ttOuן"XH,t:)Ygmq_Si8VG|ߖq^ m\d"Mq[𾧷:wNZHȑV`7)f۶d6`rݟG.阖s  ,5n1fBPwL|j#s8~=2)lX7`}7z1}?S+/W9:Y]72g:C&m'Fo/G{]3=G72J [ɻU}]߻Ue նM ]JvKn=MJVKpp%kД^ʮތdQ݃u#3U՘#>mY5jAGh =Sg$VQ ݖęmYSmf55fؑru1? Lkk+91H3lzmJޙ}`g_ϥqgM.yژtn1.&qn{zV?Ɲq= I_W Ξ`ͳ u꯷)V N`7<[3[ĘزF^2咄cOHznY2ahʑށcR}%*<[-ljezE|ގǚm_T/:bod6[I6&:RBzS .4q~"+Gb.H5ڼ>;۶)ygإb 5kQhnB>%ou[y?`=|z+|ΝMrV?|;*-@ljڻX@ A vwI@ mm›·=敕 -^$1Wz$ʍ£.Mcɧ;E;08{[fYƯ~ImɼAIS_oN'6Z3Z$o0㳣mwʭ8,2Slstx]|Kꄷ6&ݴML3.AeO/ߟJ0ƚ|C4% ߩu~OVY: /@F>`w%0&5I;Wֱ)%4^`7]?SW_ /k_={}EvNJ>%fƖJU{?\ljK}w/ܶM;L*6Li`'W:᭍~+1&:ک .ϴAj& g?."}4XF@{U{'q]栜msCcoPil56#ShEȭ-}-tLՙj^蚟8&o`9vʽ|KTqpix}S.ʸAVH!W3i-Rk+BqJTz8˶)5gsLgݧc32ǺNxmcgx _yVJ~N&B2f^3'MlRmn۪ ʹ P>ac.# /BG&9cytSg9<=sN!կ6B:+^9CCs%p&"QpG>H}'~PJhqt%:󽸯8̼8k5uNQ9U$ j%{B_>}n 1pFJQG g  Pkj[KVywGs6vLד Fqw7۪]clhfUV'13<iã㜉= iofGi]EԸvaߴ'>(c~z '75 ؅`{YT=` nVH4A5*PUSle;Y=|wj#GZhi.VG )7mLg L;t6R=kAj)G&bWrEFd:'t?wҏzNgD^3G'5z5r]Fo.%GGk#zW9-EIiKL0UOhOmƻi=~|GwLX[Ax}o5Q]p4<n-Rm+bi=|ۣGmJՙm{b.u?Sufs۶;PTg>|.J=g]8訉N|mqȵQ=Q5,fW[""gE9Gs6Ǯfu:{c|`>o]lݧ4Nxocg7K]k{4 ^J{ 4?W-_ Mﬓ ՞/.9]>Wh 6:uVB44X}:T7Ǧ">LVMې4zL+OFUI\Fkd=kt3֣嚥/czD+z_S>ץtD[pP|֝RW/XZ/׉K'6 MKt0}EW<g“`!Bv,,J7+ ikK#f)\1hG ]

ךZeZٚtT)vHi5]Iα \#U,bjUu'U >55V. !1%݂g! ==}1!ؽȸwakjfVݘc#ڤ!K iugf.B!T<nn>VYP.lߏY c C|vJC4JZ6,Y1֜f.B!Tf*ӯEF@B!DgC j A^>Zj wЛEC6@B!DC~Ӟ6|j1G&i'W[R(bkg] E!"MtSS T<0g֪R-])PbA ]B!XeN1DPa#~Z{ qEF4):+v`!B`7YQ*1WF{MQjwEU)P|ia] E!"MYtu,/aGC#٘E {3-."B&IL Æ,bGVnh*SXbE@ !Bٰ9c$gmo#252bSh^?`."B&/jbn?[ ZlraHV>hʵSHhaϱ`vB!ve+s; bE6VF?aGY2S#G @Q\`."B&)vnq i:v-5Q~(x+v`!B`7in7ӻ-.r^S3s-GؑC~S}0ݑ?Lgڟ ] E!"Mt ,jU's%ؑs&k׈Fh@ҫ`vB!veJ $:ER֩Jm]n` ,6 ]B! Gz1.Rhkdh-1nwb $'k+v`!B`7yN471.Jju w* XԨ+jw(@0W`vB!vfsC;" 4@B!D/N,USo݃ϗha z7V.B!nj̵1 :QSZݞzS m`vB!vSE`Gj : r (B踈qi@\#` vG!PuB(GH1涺+jfJe6rEU:s}nBW5 %؝et dJDObGQBvwNsm ] E(:Mu*>9rN vJ7sc]`;@+mr)1WnX@ OknM-䨏—Mu nV9X_M9Ym7@Jj wmL0.@PuNґW;kkV5x!mT=OB`7yNF>.Z!`P۸z%*ϘUsM ] E(:U Aꫮj ƍ֦ĺLTQb!,_\o}; T W^9V.B:IG [zjbF4zj!l2ݶ$1B5; S̴Wu,)͵؄cv`!Ǐ!KksuWc\b"lZ߱t󖉶ҔrlPڹz\U)CfkW ؅X!KtӁYFj3]Ǎ{B(UD=L[A'9O;.F#Gh@XjÅ@ u(׭Y$ ^r,(K~l$^WUi@Xci'P|}%jѴ0#Ϩ⨫~HTWk{M*X @1k?V`j\ÿ kg"x>܂n4j2, ?!XwG-Ф9b @?VSٔ[j{A](N~S/bĺ)0?!@+e`1&_h'WD#BV8\s`@r/Z)@j%dolۻ3: HuTKW?Fc dz;V .+jc#S∸^>9Yaښu Nߕ;kvOa)Q4%X'PܘfabJV >@v˴7aEpMRjw:A.6WYX'PT4b- S~AY"X?jbJ&c|12*V .3פ.93`@VxԴcEhh2ĨڨR12;+`S;P1/e6gBW۱@Vl-nj3_wdv@Y҈>Sw‹(~B bĺQg$,NMf_~dFJ<㴩O\SW`*ScݾuQodL{ V :~;uY`Ύm1|Esem~B tѺ}5 <2( 'b@8ϴ'aE@fJ!쀬v˚T` [~B WT3#rOsCy~/vY7UDаmFƞiSYY`gށboīr G;Rs0{- nWUhs{[lL0q1mX mx6`ZըQU\o f.6ذ*b(xN49%`<86wk ; , }x <\Eb@2^7h+(VX&5`= d m_0# h {iv@ޮZkUh-zk5](Dj56j5_G=T c̐q#}iv #Ga@f1!v; `T S e4ϳXPhlXU~kdմA؍|/)a@waE\mɻ5S~HG?vؠQ:*$LU=Oڳ&b/ L1u4 2̙=+r|mb,U1b )O?vPXxOXZ~oCu!\)Ǻ6 siqÊ5}YgRēq%*JL$9\7c@!F5 nO}F0Z7!}#6(d 6p4Dk0ZU׶` $ũ暹+`5VXp{$}mvԧxv=qec!|[iv@DFh1@g?v UU(z~c䨃>ZXfԴdAŽ+r#{~#jU[gUc 4j9+`_T46PW&\:rdӘ5Ǽ -쀼c٘ \)`@>#*[c}y+'xv/<Reu\:'Q꽵QР8\'b@Rp&5鱀@xߎ 3r xWa152ɣ+`򉥪P } 0yu9z2/fPGaFYڀ!猋XʸʵSϚ+dK]B"b  4e+>Y~)PTȑA,F A "MDD9i`!@9ٴXUns4;`xXT[//.>4F{]:?LBciƺ,Yn3.XEq%v@A*ՓLLwsL@dfL-#0&O!`Va#@x۴-3k|6x;]#zs# S@4^ ]*3Tf1&=`1VĴ0#+&eP ~A l4RMBRbajTkuUQD}9(M[0Vdծgb4547Ls1Yk ]SmT"yW*?ux}/4C 2[U9Dg}!L~OjգjbKd3 ]_j}(4H`LA͔X3n6mXq.-;(X=}I_~A AC u۫L0`keM[Jk1#Q37L)B1WXKkPw#Uh%2+r5.Ê \m?oE*G,ܟEo P-*JšH:vFa=ڀ!P|m]}gL)*`j ]>4V;G95eAPj:;W쀢g*uKcJq)/v lXS~ǚ㏦9d>3|_IW=^``Jp)3/v {l(mnjÚs 9i yNMm4;\wj#jL) a| ]kUQ"rs ri2ImMvD1KD1ՔaXy֨F[D=ڥd \g!X# ܎1n6dA],ZB4JXSfC lD*Z~sIgFF'lSv.d_U^Q^K 5VE[zf;Ck|*,c~QzF{k,ƀ s]9&33X پϖZRu9?ŔmS]/v~V4u`6b@fZ˰.;=mwh2)DS-/v_BB݃1@^3Go10-X6海}-HժPs=)tn'v?B#" A**9"3~zP7 RDK2L) ڲ]@ Xj60M$; r\=3`Z'OSZ)zYL)~:3`GuPw@Be-fEYm6`g뀈{a)A2@ 1Ke;jcQk&90ƙ͟N;ҢVj)¥V.xgJ,"m I c0rUE>+2\[b@?SI0C>R5vdvs0%/۔XsOU kg‹'\R*\[^R9vQ_*mfg]pAUTk k  s2vP-Ico/MޠXܮ162Y[5dX_(hNc@)7.4Ν1? aaYYI358kuźyJ@tfymhV6OSʡ]h;b~ףuH\ai )243)ƙ7;(.l<,i (`㶘[D3bVBP_Mb}\9lWj,<.ȷ4A pC̍-Z6ŬR-jJe93~962Xsf`_P. bnIF> \_{4@.+GaL4rWi({^Oi (`}+2{aGbfB2qg1.oYxxO?\gY o+".00`(\h+wh5 ؅qnjY1wZXĪkWnt;\RdF|Ű`,15@ZPMep8gPwwi24^xZOnp.TfH&i9̇ ТP.9q1oOc4MMs+YŪ- 862g62vXWBPqPSd47_/Uϋ RkZQύiޏP.wq渏GXiWM ܼQ,oc=n7`"eZ{{MeB3B¸OP3G{136O/V zwǜ1fWFV) ]5$ QZ:2c4@Ff7ω͊4=6qF6W-eIP.8>.Ń:@Fޖf77+VX1 AVƼظeeZv.VxPI8Vkj4dܨ^O,X1q.n1'iihk4O u*ţDžXi Mqh9?XP|H.M2 6I{ v!w–j7ZMX #+m9磊u~ƺ2jyimMBΖe\{@aq|!{ҘWݠƢz롱Hj?.X urg4S4?M#{ŗ&m| ^iZ+'؅- B]v߻k ,,>;ئ$10jI8f7e,%yVj4BfݸPa(84@V~qߐ-kZ1n-go4J FdMBNnkFF_92+.0 HJSdeylf_ y,^96oS|"?v͋UOJ!MB.nyEKQ̊_jh_1i8Xw=/O]#1On>~5O 9>BwM1Wj !7x|,[ܮˁ)صn6'k/00]ڵ176k ugn- +'C{m:}1?6g-~Qz ߠ)]51'6i u7q1c:d8?O3ǺeqF fi `*bvl<.ԝ7heMHw eY1C_2gxx;Ugz0Cvk;HS@P7us& C~e 3O3̖œ5/suܲ7jZ'؅A4/^qvLRMץ!;E*X.űܶ(j}#'؅AYnsV C=&] 4C?duT=9^ݲ{؏R)]sbVSHl. C흮j ̜f!:.˙1-bt} 3sFbǘwkgk 3w3|h{ŷl?@9-7.(SMjp`YPa1?iM}}b"NJFb$6yqF EkYc@ߋ.Lqfj)JMkc_9zP-k";AYwH<>~Q&qwR7j `ǚ836.5-t}G}1zJv^)> IJxRB;NwtkKFI^䣚%؅-P8S@}5@n-8Wѯ.ƒxQc3c^("k `z,C7u}'tCSdiI_c?z.ӕ@sca@Ĝ/.tۍ u5 zҬ4Ovc:mg_\T觱瘝&177EKm%؅cHwF̊ 5 t%m)4O\8=-ʘ3!wՆK -Ŗ-F1+.0Ї GJCkհm? \Ooyy츳mqRj/.t[ uз+ky<[bQDx0cv;M-AԮ-OBk(MuuNiKq__GznlV?q@9- 9s^i@꼘5.*-#ӵMLͬbi1-2 ~[q>2$؅ΉY-HlO M?eb.]Ɗ!{ҡ?`9'n nt}ASdj^k6s*{NtF̉{GB9qb~Y@<7]wj L}5;eX=bAl:fGPCyTC4YqcA000/Nމ S,sqОۡ?]kmO }/' ث!MiUgf_w8_g5&V-/:Zi>(GvtlVJO u]XWؽeGkQ^Ǖ~};\襔uJ!voӿJ'k85ReWJ vJ)%UJ)`W)`W)%UJ)`W)]R]] vRJRJ v vRJR]RJ)RJR]RJ vJ)J)RJ vJ)%UJ)#4RJV$]_)6mt#@)ym<#RJjݽ@`wdn! v[(C TM 3.M%؅ v&ș`BUL @S v!s] vr&9.@9T]Ȝ`j] vh*.dN P5.@4`2'` g]J TM 3.M%؅ v&ș`BUL @S v!s] vr&9.@9T]Ȝ`j] vh*.dN P5.@4`2'` g]J TM 3.M%؅ v&ș`BUL @S v!s] vr&9.@9T]Ȝ`j] vh*.dN P5.@4`2'` g]J TM 3.M%؅ v&ș`BUL @S v!s] vr&9.@9T]Ȝ`j] vh*.dN P5.@4`2'` g]J TM 3.M%؅ v&ș`BUL @S v!s] vr&9.@9T]Ȝ`j] vh*.dN P5.@4`2'` g]J TM 3.M%؅ v&ș`BUL @S v!s] vr&9.@9T]Ȝ`j] vh*.dN P5.@4Uʸ..[[Vǭqy3EOTInNW~>:gh>7ܸ*V~Mvs u4W1srK_ψGxS|^JVc[k܋݊݊g ?{=hxq=Q}OvmyA/Wqp:qxCK0k1#y81.%V5ۂ]sosmt5ǤjbYp:? #8^^Ӈ'U<` y~mTtcbX[au+b!+r.c[k{ڭحxw8. #8^^Ӈ'U< 8C)~"KqX[au0O:`\-5= vVV< fQ`wx/Ժzw]misӿ-Y.}RiKƵqK9VŊ8'NFEO ,-YL^-R{~LR4q}ͤjbYp:?]=Qͣo~LқsGorql_|nmJ[*=a[~8f {X[*nS}i´$VʵT]soޏi4=ȴibYUc`WOTMvWUpvA=j3fWǯ:~j}BMuJ7HX[j7#פcTts\+>O?h\-5=N_̹λ*v+{x2ؽ*>Ob$}qK;淩E]D̊cc۶QO_g}8 ?'8@D=`wУ-6~y|턷?;q K&xTcN~c1(C|$[H8-isri '[*E4!&"MXO ~,ţLyrf{s*uxc`{{۾[8)>'Kx\l8g56s{{{ vͽzViyѐqޤ iw<-[`<;6xQ`*ozhb;іxuOI˿tOEc>g酥C]?фvwpӭ943pׄ`wiE{ˏ]!cmn-y\c ~߶1/zBI .yaAqrc[k{Kkm7omحȋ*}~<~4sVݵ*w.x|;.>{l͜:h#D۠o<~&xTߊqRac6?5#<}3|tb.h~ĠrgYGzRv"#156?DŽEܤr`ܛ:`nnv[ Uno1;;n`G8&#.xFq=u WET""nGſ!DA:^k!(7!cAoG5aèDxzxQqD~/ޛnbg༯ nnw'=c߬6^n6 +سmƽ>8b'MU{yms899޼މ޺Vziyѐc1~ՆlZL'~r]|ҮٱMqRO\mqPxѓOeϋ7Ⓒ~XI9X˿/)|=vH{=^og8Ry1#񈸷3pׄ``}x1(/Zu]=PڬUWƼ ~恸%nS➸7V}*fqz˧2.)z|T3Vu޼~y~*v+!vඓ#}z]q\Y|vgGz _ A\K17}N-z)1ĬX2wO[o?^NT]..u;zor^ݿ Yxx +~x۫%ӣO~85r\-5={[wb"/` o}Z-/6:<V0u݄U< {_6V6_1>(z6|q׶raYz&Nri -y2 5`7}x v5#ޑ n{xC+;>3ǖ:On9kZۥ[r]ƶ`ܛokn݊珜ݯǺZOOo*d[w豅v`wpn٣m/>?-m1]oñ믙ֳni GGcŏ7U\u&1z>nwSwW]loז0yߛL~+庌m7ﹷ`[J{yQ9kI)u^$cuQ$z"Nhxu䤭^=nkGVǶ1#_|OٍcMۣwH{=^gGooƹ&1z>n7f`o͵?/~̷'Ϥ|OVۂ]sosԦ>mpn݊珜(t'֮.>17빅v`wpn٣mkO-j΢o{SGz?å|ZGomw5`7}x v5zm17Z櫂y^8,'ymG𰯔3޼'YMKmSJ{yQyǤ[Jw;i0k ]9`wzF[ Wc1}n(z68?{ n{0ǝ)Gg7~#kFl&1cEm3p=uyûOͬ*x$ni{W^etGt9}\-5={[)cb"/*#`w[</6K1 Kc~wf>kjBsxa<'b%-[fm#x_G&%xVqN}5`7}x v5oJ||8S\2 tīӿ<+m7w4}ls\J{yK: S'uwnmnkϖ%m}' vmo!cIt~ԏ=^=7LڇG9v3񉘙n}I)#+ׄ`w_94̚:}Pg2_M?y:"/.9حJ>c[kG{P?on݊珼Ov0tU,̇ȡ'19qݏ~,l{UkmИǛ8:ǿSm#xdK{VMgz\v߇w=Q`Ys6Uc_-g{zvR{{ vͽuܭW[Wtk8fk- G]9`w:F[= *ǴR)tvaS~t۩}o珴s/և j:hmkn'v3kC?_=N.4OGi]a_)gl vͽyϽ:VثحȋJ}+}E%tt־8>~gd>oIH{)O??tj?^NT}+k:h] v߇w=Q`Yb }Fmn)1g鈈7L#xWۂ]soso2*v+R? vL/_[JwuU҆[f={j[[YV? _.G𰯔3޼~]sow+eUVE>8zTI5+ o8JgmWBi zNhxu⯊/wO7E :8^pW[vH{=^g8Rimt \k"B>^^g=qHyuI>)qU~qoGy᭓Ɓ_)gl vͽyϽ:VثحȋJ}']OOM{}I5BƟ')mxU|bEO-xT?֧ =Q}Ovgq LqLx_OCm=m7~S-p \k"B>^gSx9zʄ#cf.6C>/-'þR{{ vͽܭW[O.>74f.O%.N(~qkOm/z]O+h=QaOT;9q[ nA'p@~F]7~/F_ic)k;ir^1^ym~Top2O vͽyϽzV߫حȋJ}#]c4 uNm0[%bnƵ]Q勺[DujjУm0? ~ޖ8OƙqY+'io GoiY,ΎK, \5!ح"n~ozbP^}[uy,`g}HKt5'i$1/]qL̍?Qے~a^)c{s{{ vͽ:VziyP[pդe'͸=5M}19 g4E+>fPNsD=!حzq ⺏['cLO'5G}>xvqM~KMb H_qogમ nnw{߷[yauuψ?7g.,i>2zrp0O vͽy{ vͽ}Hv+ݷ݊h(Q%{ixf۟9ov[N`D=!ح~qMuqv=?ny$OwX+حQWH?^gt \5!ح2l~ozb΂SJ5/j6<\yo-'J9yZk{m=]nS޺VmyPۊ7 Ώ4<ꦻ?_rGg#m%o8,v 䣙bN=" cy'9:׹3fǦcSm0dIm?L4*?/]ǵ?nNT{DZq_Qxׄ`wno{=w~Yx۰g6…%ӽ Z)>YK8E8;ė\O[uՍ#"bnU4&iU|;Ɖ6o*S7 JWqV\9' 5!5>xc^)^~nzr=Ƕ`km[ʠ*v+ÝuQE 4/ȏ` g]J TM 3.M%؅ v&ș`BUL @S v!s] vr&9.@9T]Ȝ`j] vh*.dN P5.@4`2'` g]J TM 3.M%؅ v&ș`BUL @S v!s] vr&9.@9T]Ȝ`j] vh*.dN P5.@4`2'` g]J TM 3.M%؅ v&ș`BUL @S v!s] vr&9.@9T]Ȝ`j] vh*.dN P5.@4`2'` g]J TM 3.M%؅ v&ș`BUL @S v!s] vr&9.@9T]Ȝ`j] vh*.dN P5.@4`2'` g]J TM 3.M%؅ v&ș`BUL @S v!s] vr&9.@9T]Ȝ`j] vh*.dN P5.@4`2'` g]J TM 3.M%؅ v&ș`BUL @S v!s] vr&9.@9T]Ȝ`j] vh*.dN P5.@4`2'` g]J TM 3.M%؅ v&ș`BUL @S v!s] vr&9.@9T]Ȝ`j] vh*.dN P5.@44;|TiutMݣRjhkFo6RזCF*ꠦ!UJMo*TsYRJխ!UJ)z)R]RJ vRJ+UJ)%UJ vRJ+UJ)`W)`W)`W)]*R]] vR]RJ)RJR]RJ vJ)J)RJ vJ)%UJ)J v?{)XWgZ?(\`OW)U:^SJ)UF`, vjF P3]Ԍ`f5#.@vjF P3]2lIENDB`bidict-0.22.1/docs/_static/build-bidict-types-diagram.sh000077500000000000000000000024101435416354600230560ustar00rootroot00000000000000#!/bin/bash # # Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. set -euo pipefail log() { echo >&2 " *" "$@" } # Generate a new graph image from its source file if it's been modified. main() { local -r graph_src="bidict-types-diagram.dot" local -r graph_dst="${graph_src%.*}.png" if [[ ! "$(git diff --name-only -- "$graph_src")" ]] && [[ ! "$(git diff --name-only --cached -- "$graph_src")" ]]; then log "$graph_src not modified -> skipping graph update." return 0 fi if ! command -v dot &>/dev/null; then log "'dot' not found -> skipping graph update. Hint: brew install graphviz" return 1 fi if ! dot -v -Tpng -o "$graph_dst" <"$graph_src"; then log "dot exited nonzero." return 1 fi # return 0 if any of the below fail because running dot succeeded, which is the main thing. if ! command -v optipng &>/dev/null; then log "'optipng' not found -> skipping png optimization. Hint: brew install optipng" return 0 fi if ! optipng "$graph_dst"; then log "optipng exited nonzero." return 0 fi } main bidict-0.22.1/docs/_static/custom.js000066400000000000000000000015651435416354600173020ustar00rootroot00000000000000'use strict'; document.addEventListener('DOMContentLoaded', function() { function addScript(src) { var el = document.createElement('script'); el.setAttribute('async', ''); el.setAttribute('src', src); document.head.appendChild(el); } // Google Analytics addScript('https://www.googletagmanager.com/gtag/js?id=UA-10116163-3'); window.dataLayer = window.dataLayer || []; function gtag() { dataLayer.push(arguments); } gtag('js', new Date()); gtag('config', 'UA-10116163-3'); var sidebarUl = document.querySelector('.sidebar-tree > ul'); sidebarUl.insertAdjacentHTML('beforeend', '

  • GitHub Repository
  • '); sidebarUl.insertAdjacentHTML('afterend', '
    ') }); bidict-0.22.1/docs/_static/favicon.ico000066400000000000000000000246361435416354600175570ustar00rootroot00000000000000 )PNG  IHDR{`)OIDATxw|Ew\r$DTTPT lWEPE>*v `ÂX@A(Mz~redww\.߼gg>mfA"H$D"H$D"H$D"H$D"H$D"H$D"H$D"H$D"H$D"H$D"H$D"H$D"H$D"H$D"H$D"H$D"H$D"H$Mt+& qjw¨qn`v R=!~b6'R)Mhzs WQ /we/ùvnQB'9lV]#_,Fzf3dGg?mr48I2> E;MH7+)R&N}pHZRNK Gr&c^1c/!Is3?Qv34bP !pߘqԪ]W7\CrbBwL #lw.+eSîZ`JJ+]ߚٻ ݞ[NJI)986+38^NJa< #'4q]WCT \+>J,!m F @d4W⿜ҢJ 9-c:'tAv$1ӏi8m8=ZMmkoct+Cvy'_3o!j b>6u2>~ 홗#C"fk>kBc6~cR"WKf$ K_a ߍ|&p>c sZI G2i$D,IP )lTSԋd0[9[uXYLQB;\eӃ[Fp]wl. I!vt+dA:X0aˆQ nܸpJP!r8!ʩ82"1e~9_q5I-v=37+(\h9A3Зo+jA˒Dw.A>7QqY7QMYlXxķvl>u}!_sCyO [X)\b;nq<Z4Vq7SuOXݢC9<@[c(=d+t)dZ<ȧr*bÆ .\#&̘`!DH& hK: ē Ua`bcƳ[gߟa_#CA572FŤtI2gXN[^~&2D20Mt4.e4[PPV&z,;59]h-Gw>T:x.be\1$j `O)f1KFRDۨЃlJ"Dsx=;x^-mٯ&%d3C#hD 2\^3iB[a.q-r f3]bq) FfI/[Nbxb+DЙ1,$) { w^_-y1 !kj$g-FL/95s0U(my7ptmFR" f%{[) h/FSIR Lf.5([4~5+HFFx:M; uи*v(_dfenDLHg̷ё{9+#2 /b.׆#}lg#kY~0k_KH7P#g,b8H@ \ȊF:w"2 ub8A$r K4SR.387D2l:-e~9f& 5 {CI!5}ewDl!HA=y yLoK$U)7F{FBcbWdj|.c С"yΙ(kZe&d9D=8fmi*\([3E- ilb rFD (&糀Ku~T7T?qy0'x~ 2.(@7&Ac>'OM/ [}WDŽXD3T}νa-?経MA0ǯAw٨kKFVf9E~]l|3/렊"R3.R9^a;_| ìݸQpyMN{ 'TKhߊ"=oX43k@- ۜ$W1\|r U;w=^Q=&.Y̗((x=572qM^LO#l]-exb:*Mک#RDGm8tOל8gpjQx~ƅYz]"_bN*UXA \)Oj2YBWANf2-1rYr^rN:nHmb^!8zd~c/E^3N@wvўSSo}F99D &l䳖Oxq<{'7$M!41tF!3y}kzj]tfHjJNo1UMXQX>&,'j?F ϣW Y'Y5Ȕ-75;FܽМ0ۏ66Z, 9f_ۘ@:Ё~7Y/(CYN?|:AB0ج X֬5?jFTZ=>nr6:A]H6~Q9jFH0} ЖPPX^sBs59';y<" ;Xýa: 2oexP^vCt N?{a~/md_謰=Q/4:IRYd2kmoPOcC7z"J[~_d?a_3Xw_Qa|c'^;s[يk?UiDҦb`=)mmRO*QPXgձwPǰ৊qW( x{ySXe!]6& f GiN={7W'¾Ub_O5bnPKAydoiFr>qad& %j]݆2'|ߌ ~O@Oi&`GP/@"sb0s+1/I>K0TJ4:\ @gf1FVKҀAǼ3}%v5t>S]|. jZx1ўK .!7y,{S6>1媟wٙ+rHǚOe8+tWy-৊VswolFғ`gfrJ/@Wm7 <IG>fUoqJli$~~R ϑ8z2%JoÉ|߸3je:?vZpOoco4j(=5^ldjd27'|^g_rCÐL} uR!y)"ϝJWTf1t#>7i7*Z0뜿ii:N>D:w+5VN c><xTd+#H&i4PoJ5N MߢI ډ~k:ikA3F8Qp1^TcW% ˆl(6~JCofUq?(vf8+3FitJJf.$K jQ퉍x#㏀L#oOT\dâyǝ-(Lܡyd 4.F0fb((l+PP!v(ѼNYrҁ׹YGקW y>C6* on͔T͸%rϲݥvxb/™TZu y&ྼu,gNӘ~`(Gsɧ#wD0 Y2`f>-=Yu a U()-4hfײ&xc7o\Y.ӑh -g6h{6{*nfAB7"ŃG&#p\C52gG,I |UpS5nV1gU7M%o]|DK $Ha> \'O\yTm5'?q]/@U',zd r 2C^`1K IsU8GRy j!O<҆tRI"8,1 #W;p`N V(/xF ԅq`3PLp^u Zj Ou0[u zOXuc;Ct5䫀@mk|&,I&t$t,Da"*$K 'N)B)J! K1Gf'1$ŕaVB/S1ГWS|K-?sNS5a!ol@m*|qt\F߳jٴiŅrvLzNmPͨfN/u&v`΅5yΑ61ɕ:Ac~ { zďL`Q@wHnoF33 x8EةjQxY(Ċ?Fc&G'0K,b` BoFs-m'F_*Ь_H;[/2A:y"C8߸4,f=#Cȿu_17ZSf2hMtdЃY:wR+r( *Rn̨2` O"BmȢ-I'xMNS,Q66R~!s09TMdOrͣ+-E7T8 ~Vg0O5,cݐaXl܏tȵZ*-1߬G(x(q8Bb> !n0U=/4g12B, F` Io!Ƅ߉:gϫ+2J)jjXL )1~J7"RN' oFZMLL㐷Rg۹IgYxB*O7r1@gmty-Lܗx'g:rpW̡,g(Ҩ1ēD:ґv-h_pR@y"䲅Ogp&}*"m/9} `TBY@Z}y/`bY5P2O٧V!K F?%okxM^J y4Sϭ<,V07iJfr'8\Q &'[, EP>nS']i^U?:).ְ"[4+9x5F:cYM׊xLn2Tf~G6Q ɤ+'r"=N*q70aF`,qU@ ј9 tL |K sBcIjU͢Ǝ*zbV38Gɑʭ\g&a/;7q.`R)(bIB4QDItQ8|jVoDqIr*rjNXIӝK][= դ/hYtEp qưA^'kx(?) 0xCylMqb(TEvՅn\-:رc"(" )J؁8RɜĉGz wbSL8>}-.Ss\ gޥ[w в|Hyd9bjmf=&o>3r3/p8u7f's!BbDo/Q}̘5ذQC "pK2s:ғL+wa0RqEPKdVYFOr6q '+>A$Vʨ*Tclذ }ߩntQB7K,,ĉxH$^LJ5䰗 eDvJYEjxs*9\i! oPRF뾆.ėz.z11ۓSx4)QGȭX}Av'_8f (+b7hjT"tOL'tq؀h/4'UTNvR#>ۂY3摤݀n=ԓʕOb'S9k7F*1Mtⅲ(37.v+߭QόY[${9\[pɱD"_Pb:GNّ6x"@G5?3e!>Uky' cR3I$")R( l8#"n& c $HI$J2)$}#v(e[nvQF+Puw& p ͠K(KY&]Gd5p*7o)tPرb*J\bŊ] 0c! q$ImiG&dAQ"MlgZ}dfÍ*b[9r ˹~plALgN^5j7f TñZuEOTR-&_I$dҙ.tiE5\q:M" O p @&CSt3> ^ب<2y~3\ލ2Fu7ifhkC1hÆ)"DdQ)A7;]Dof wk`[D~5|-]b yuV3Y<()2)2ʨ*ljO&a[S={`O`LJNjj*EL"a!B&]EOӕtQa^^ENnJt^\HQ9u7aLCPRH!@"1BBM") |(Jh dҝxٺxFshڑff3Q\v*tRH#Fj5tS+}baBiۆa $HEIC.rC.c,m]d{ WN`?z\fsͅcսX`qPCPɼÎrϣPidA&,EG(bsTS];sMiğYX$(WeRvs"<'i{iDNR]1aE[hKP"%^vXIĀ+bMÕlA4<Í`-( cр9\B@D>~A* AP;# Y%`.lDNBBR$TM|~_R%Q2ܘX+`. ``bidict`` Avoids Reference Cycles ---------------------------------- A careful reader might notice the following... .. testsetup:: from bidict import bidict .. doctest:: >>> fwd = bidict(one=1) >>> inv = fwd.inverse >>> inv.inverse is fwd True ...and worry that a :class:`~bidict.bidict` and its inverse create a reference cycle. If this were true, in CPython this would mean that the memory for a :class:`~bidict.bidict` could not be immediately reclaimed when you retained no more references to it, but rather would have to wait for the next garbage collection to kick in before it could be reclaimed. However, :class:`~bidict.bidict`\s use a :class:`weakref.ref` to store the inverse reference in one direction, avoiding the strong reference cycle. As a result, when you no longer retain any references to a :class:`~bidict.bidict` you create, you can be sure that its refcount in CPython drops to zero, and that its memory will therefore be reclaimed immediately. .. note:: In PyPy this is not an issue, as PyPy doesn't use reference counts. The memory for unreferenced objects in PyPy is only reclaimed when GC kicks in, which is unpredictable. Terminology ----------- - It's intentional that the term "inverse" is used rather than "reverse". Consider a collection of *(k, v)* pairs. Taking the reverse of the collection can only be done if it is ordered, and (as you'd expect) reverses the order of the pairs in the collection. But each original *(k, v)* pair remains in the resulting collection. By contrast, taking the inverse of such a collection neither requires the collection to be ordered nor guarantees any ordering in the result, but rather just replaces every *(k, v)* pair with the inverse pair *(v, k)*. - "keys" and "values" could perhaps more properly be called "primary keys" and "secondary keys" (as in a database), or even "forward keys" and "inverse keys", respectively. :mod:`bidict` sticks with the terms "keys" and "values" for the sake of familiarity and to avoid potential confusion, but technically values are also keys themselves. Concretely, this allows :class:`~bidict.bidict`\s to return a set-like (*dict_keys*) object for :meth:`~bidict.bidict.values`, rather than a non-set-like *dict_values* object. Missing ``bidict``\s in the Standard Library -------------------------------------------- The Python standard library actually contains some examples where :class:`~bidict.bidict`\s could be used for fun and profit (depending on your ideas of fun and profit): - The :mod:`logging` module contains a private ``_levelToName`` dict which maps integer levels like *10* to their string names like *DEBUG*. If I had a nickel for every time I wanted that exposed in a bidirectional map (and as a public attribute, no less), I bet I could afford some better turns of phrase. - The :mod:`dis` module maintains a mapping from opnames to opcodes ``dis.opmap`` and a separate list of opnames indexed by opcode ``dis.opnames``. These could be combined into a single bidict. - Python 3's :mod:`html.entities` module maintains separate ``html.entities.name2codepoint`` and ``html.entities.codepoint2name`` dicts. These could be combined into a single bidict. Caveats ------- Non-Atomic Mutation ^^^^^^^^^^^^^^^^^^^ As with built-in dicts, mutating operations on a :class:`~bidict.bidict` are not atomic. If you need to mutate the same :class:`~bidict.bidict` from different threads, use a `synchronization primitive `__ to coordinate access. [#]_ .. [#] *See also:* [`2 `__], [`3 `__] Equivalent but distinct :class:`~collections.abc.Hashable`\s ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Consider the following: .. doctest:: >>> d = {1: int, 1.0: float} How many items do you expect *d* to contain? The actual result might surprise you: .. doctest:: >>> len(d) 1 And similarly, .. doctest:: >>> dict([(1, int), (1.0, float), (1+0j, complex), (True, bool)]) {1: <... 'bool'>} >>> 1.0 in {True} True (Note that ``1 == 1.0 == 1+0j == True``.) This illustrates that a mapping cannot contain two items with equivalent but distinct keys (and likewise a set cannot contain two equivalent but distinct elements). If an object that is being looked up in a set or mapping is equal to a contained object, the contained object will be found, even if it is distinct. With a :class:`~bidict.bidict`, since values function as keys in the inverse mapping, this behavior occurs in the inverse direction too, and means that a :class:`~bidict.bidict` can end up with a different but equivalent key from the corresponding value in its own inverse: .. doctest:: >>> b = bidict({'false': 0}) >>> b.forceput('FALSE', False) >>> b bidict({'FALSE': False}) >>> b.inverse bidict({0: 'FALSE'}) *nan* as a Key ^^^^^^^^^^^^^^ In CPython, *nan* is especially tricky when used as a dictionary key: .. doctest:: >>> d = {float('nan'): 'nan'} >>> d {nan: 'nan'} >>> d[float('nan')] # doctest: +SKIP Traceback (most recent call last): ... KeyError: nan >>> d[float('nan')] = 'not overwritten' >>> d # doctest: +SKIP {nan: 'nan', nan: 'not overwritten'} In other Python implementations such as PyPy, *nan* behaves just like any other dictionary key. But in CPython, beware of this unexpected behavior, which applies to :class:`~bidict.bidict`\s too. :mod:`bidict` contains no special-case logic for dealing with *nan* as a key, so bidict's behavior will match :class:`dict`'s on whatever runtime you're using. See e.g. `these docs `__ for more info (search the page for "nan"). Simultaneous Assignment ^^^^^^^^^^^^^^^^^^^^^^^ :class:`~bidict.bidict`\s may behave differently from dicts with respect to so-called "simultaneous assignment". Consider the following: .. doctest:: >>> m = {'a': 'a', 'b': 'b'} >>> m['a'], m['b'] = m['b'], m['a'] # swap two values >>> m {'a': 'b', 'b': 'a'} With a :class:`~bidict.bidict`, simultaneous assignment cannot be used to swap two values in this way: .. doctest:: >>> m = bidict({'a': 'a', 'b': 'b'}) >>> m['a'], m['b'] = m['b'], m['a'] Traceback (most recent call last): ... KeyAndValueDuplicationError: ('a', 'b') This is because "simultaneous" assignments like the above are `by definition `__ just syntax sugar for: .. code-block:: python >>> # desugaring: m['a'], m['b'] = m['b'], m['a'] >>> tmp = (m['b'], m['a']) >>> m['a'] = tmp[0] >>> m['b'] = tmp[1] and so the intermediate ``m['a'] = tmp[0]`` assignment raises :class:`~bidict.KeyAndValueDuplicationError` before the second half of the swap assignment has a chance to run. For a working alternative, you can write: .. doctest:: >>> m.forceupdate({m['a']: m['b'], m['b']: m['a']}) >>> m bidict({'a': 'b', 'b': 'a'}) ---- For more in this vein, check out :doc:`learning-from-bidict`. bidict-0.22.1/docs/api.rst000066400000000000000000000010421435416354600152750ustar00rootroot00000000000000API === This page contains auto-generated documentation from the bidict source code. bidict ------ .. automodule:: bidict :members: :imported-members: :member-order: bysource :special-members: :show-inheritance: :undoc-members: :inherited-members: .. autodata:: bidict.RAISE .. autodata:: bidict.DROP_OLD .. autodata:: bidict.DROP_NEW .. autodata:: bidict.ON_DUP_DEFAULT .. autodata:: bidict.ON_DUP_RAISE .. autodata:: bidict.ON_DUP_DROP_OLD .. attribute:: __version__ The version of bidict represented as a string. bidict-0.22.1/docs/basic-usage.rst000066400000000000000000000236511435416354600167210ustar00rootroot00000000000000Basic Usage ----------- Let's return to the example from the :doc:`intro`: .. testsetup:: from bidict import bidict .. doctest:: >>> element_by_symbol = bidict(H='hydrogen') As we saw, this behaves just like a dict, but maintains a special :attr:`~bidict.BidictBase.inverse` attribute giving access to inverse items: .. doctest:: >>> element_by_symbol.inverse['helium'] = 'He' >>> del element_by_symbol.inverse['hydrogen'] >>> element_by_symbol bidict({'He': 'helium'}) :class:`bidict.bidict` supports the rest of the :class:`collections.abc.MutableMapping` interface as well: .. doctest:: >>> 'C' in element_by_symbol False >>> element_by_symbol.get('C', 'carbon') 'carbon' >>> element_by_symbol.pop('He') 'helium' >>> element_by_symbol bidict() >>> element_by_symbol.update(Hg='mercury') >>> element_by_symbol bidict({'Hg': 'mercury'}) >>> 'mercury' in element_by_symbol.inverse True >>> element_by_symbol.inverse.pop('mercury') 'Hg' Because inverse items are maintained alongside forward items, referencing a :class:`~bidict.bidict`'s inverse is always a constant-time operation. Values Must Be Hashable +++++++++++++++++++++++ Because you must be able to look up keys by value as well as values by key, values must also be hashable. Attempting to insert an unhashable value will result in an error: .. doctest:: >>> anagrams_by_alphagram = dict(opt=['opt', 'pot', 'top']) >>> bidict(anagrams_by_alphagram) Traceback (most recent call last): ... TypeError: ... So in this example, using a tuple or a frozenset instead of a list would do the trick: .. doctest:: >>> bidict(opt=('opt', 'pot', 'top')) bidict({'opt': ('opt', 'pot', 'top')}) Values Must Be Unique +++++++++++++++++++++ As we know, in a bidirectional map, not only must keys be unique, but values must be unique as well. This has immediate implications for :mod:`bidict`'s API. Consider the following: .. doctest:: >>> b = bidict({'one': 1}) >>> b['two'] = 1 # doctest: +SKIP What should happen next? If the bidict allowed this to succeed, because of the uniqueness-of-values constraint, it would silently clobber the existing item, resulting in: .. doctest:: >>> b # doctest: +SKIP bidict({'two': 1}) This could result in surprises or problems down the line. Instead, bidict raises a :class:`~bidict.ValueDuplicationError` so you have an opportunity to catch this early and resolve the conflict before it causes problems later on: .. doctest:: >>> b['two'] = 1 Traceback (most recent call last): ... ValueDuplicationError: 1 The purpose of this is to be more in line with the `Zen of Python `__, which advises, | *Errors should never pass silently.* | *Unless explicitly silenced.* So if you really just want to clobber any existing items, all you have to do is say so explicitly: .. doctest:: >>> b.forceput('two', 1) >>> b bidict({'two': 1}) Similarly, initializations and :meth:`~bidict.bidict.update` calls that would overwrite the key of an existing value raise an exception too: .. doctest:: >>> bidict({'one': 1, 'uno': 1}) Traceback (most recent call last): ... ValueDuplicationError: 1 >>> b = bidict({'one': 1}) >>> b.update([('uno', 1)]) Traceback (most recent call last): ... ValueDuplicationError: 1 >>> b bidict({'one': 1}) Setting an existing key to a new value does *not* cause an error, and is considered an intentional overwrite of the value associated with the existing key, in keeping with dict's behavior: .. doctest:: >>> b = bidict({'one': 1}) >>> b['one'] = 2 # succeeds >>> b bidict({'one': 2}) >>> b.update([('one', 3), ('one', 4), ('one', 5)]) >>> b bidict({'one': 5}) >>> bidict([('one', 1), ('one', 2)]) bidict({'one': 2}) In summary, when attempting to insert an item whose key duplicates an existing item's, :class:`~bidict.bidict`'s default behavior is to allow the insertion, overwriting the existing item with the new one. When attempting to insert an item whose value duplicates an existing item's, :class:`~bidict.bidict`'s default behavior is to raise. This design naturally falls out of the behavior of Python's built-in dict, and protects against unexpected data loss. One set of alternatives to this behavior is provided by :meth:`~bidict.bidict.forceput` (mentioned above) and :meth:`~bidict.bidict.forceupdate`, which allow you to explicitly overwrite existing keys and values: .. doctest:: >>> b = bidict({'one': 1}) >>> b.forceput('two', 1) >>> b bidict({'two': 1}) >>> b.forceupdate([('three', 1), ('four', 1)]) >>> b bidict({'four': 1}) For even more control, you can use :meth:`~bidict.bidict.put` and :meth:`~bidict.bidict.putall`. These variants allow you to pass an :class:`~bidict.OnDup` instance to specify custom :class:`~bidict.OnDupAction`\s for each type of duplication that can occur. .. doctest:: >>> from bidict import OnDup, RAISE >>> b = bidict({1: 'one'}) >>> b.put(1, 'uno', OnDup(key=RAISE)) Traceback (most recent call last): ... KeyDuplicationError: 2 >>> b bidict({1: 'one'}) :mod:`bidict` provides the :attr:`~bidict.ON_DUP_DEFAULT`, :attr:`~bidict.ON_DUP_RAISE`, and :attr:`~bidict.ON_DUP_DROP_OLD` :class:`~bidict.OnDup` instances for convenience. If no *on_dup* argument is passed, :meth:`~bidict.bidict.put` and :meth:`~bidict.bidict.putall` will use :attr:`~bidict.ON_DUP_RAISE`, providing stricter-by-default alternatives to :meth:`~bidict.bidict.__setitem__` and :meth:`~bidict.bidict.update`. (These defaults complement the looser alternatives provided by :meth:`~bidict.bidict.forceput` and :meth:`~bidict.bidict.forceupdate`.) Key and Value Duplication ~~~~~~~~~~~~~~~~~~~~~~~~~ Note that it's possible for a given item to duplicate the key of one existing item, and the value of another existing item. In the following example, the key of the third item duplicates the first item's key, and the value of the third item dulicates the second item's value: .. code-block:: python >>> b.putall([(1, 2), (3, 4), (1, 4)], OnDup(key=...)) What should happen next? Keep in mind, the active :class:`~bidict.OnDup` may specify one :class:`~bidict.OnDupAction` for :attr:`key duplication ` and a different :class:`~bidict.OnDupAction` for :attr:`value duplication `. To account for this, :class:`~bidict.OnDup` allows you to use its :attr:`~bidict.OnDup.kv` field to indicate how you want to handle this case without ambiguity: .. doctest:: >>> from bidict import DROP_OLD >>> on_dup = OnDup(key=DROP_OLD, val=RAISE, kv=RAISE) >>> b.putall([(1, 2), (3, 4), (1, 4)], on_dup) Traceback (most recent call last): ... KeyAndValueDuplicationError: (1, 4) If not specified, *kv* defaults to whatever was provided for *val*. Note that repeated insertions of the same item are construed as a no-op and will not raise, no matter what the active :class:`~bidict.OnDup` is: .. doctest:: >>> b = bidict({1: 'one'}) >>> b.put(1, 'one') # no-op, not a DuplicationError >>> b.putall([(2, 'two'), (2, 'two')]) # The repeat (2, 'two') is also a no-op. >>> sorted(b.items()) [(1, 'one'), (2, 'two')] See the :ref:`extending:\`\`YoloBidict\`\` Recipe` for another way to customize this behavior. Updates Fail Clean ++++++++++++++++++ If an update to a :class:`~bidict.bidict` fails, you can be sure that it fails clean. In other words, a :class:`~bidict.bidict` will never apply only part of an update that ultimately fails, without restoring itself to the state it was in before processing the update: .. doctest:: >>> b = bidict({1: 'one', 2: 'two'}) >>> b.putall([(3, 'three'), (1, 'uno')]) Traceback (most recent call last): ... KeyDuplicationError: 1 >>> # (1, 'uno') was the problem... >>> b # ...but (3, 'three') was not added either: bidict({1: 'one', 2: 'two'}) Order Matters +++++++++++++ Performing a bulk insert operation – i.e. passing multiple items to :meth:`~bidict.BidictBase.__init__`, :meth:`~bidict.bidict.update`, :meth:`~bidict.bidict.forceupdate`, or :meth:`~bidict.bidict.putall` – is like inserting each of those items individually in sequence. [#fn-fail-clean]_ Therefore, the order of the items provided to the bulk insert operation is significant to the result: .. doctest:: >>> b = bidict({0: 0, 1: 2}) >>> b.forceupdate([(2, 0), (0, 1), (0, 0)]) >>> # 1. (2, 0) overwrites (0, 0) -> bidict({2: 0, 1: 2}) >>> # 2. (0, 1) is added -> bidict({2: 0, 1: 2, 0: 1}) >>> # 3. (0, 0) overwrites (0, 1) and (2, 0) -> bidict({0: 0, 1: 2}) >>> sorted(b.items()) [(0, 0), (1, 2)] >>> b = bidict({0: 0, 1: 2}) # as before >>> # Give the same items to forceupdate() but in a different order: >>> b.forceupdate([(0, 1), (0, 0), (2, 0)]) >>> # 1. (0, 1) overwrites (0, 0) -> bidict({0: 1, 1: 2}) >>> # 2. (0, 0) overwrites (0, 1) -> bidict({0: 0, 1: 2}) >>> # 3. (2, 0) overwrites (0, 0) -> bidict({1: 2, 2: 0}) >>> sorted(b.items()) # different items! [(1, 2), (2, 0)] .. [#fn-fail-clean] Albeit with the extremely important advantage of :ref:`failing clean `. Interop +++++++ :class:`~bidict.bidict`\s interoperate well with other types of mappings. For example, they support (efficient) polymorphic equality testing: .. doctest:: >>> bidict(a=1) == dict(a=1) True And converting back and forth works as expected (assuming no :ref:`value duplication `): .. doctest:: >>> dict(bidict(a=1)) {'a': 1} >>> bidict(dict(a=1)) bidict({'a': 1}) See the :ref:`other-bidict-types:Polymorphism` section for more interoperability documentation. ---- Hopefully :mod:`bidict` feels right at home among the Python built-ins you already know. Proceed to :doc:`other-bidict-types` for documentation on the remaining bidict variants. bidict-0.22.1/docs/changelog.rst000077700000000000000000000000001435416354600207052../CHANGELOG.rstustar00rootroot00000000000000bidict-0.22.1/docs/code-of-conduct.rst000077700000000000000000000000001435416354600227152../CODE_OF_CONDUCT.rstustar00rootroot00000000000000bidict-0.22.1/docs/conf.py000066400000000000000000000206131435416354600152760ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # bidict documentation build configuration file, created by # sphinx-quickstart on Fri Aug 29 11:38:22 2014. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. """Sphinx configuration.""" import sys import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('..')) import bidict # -- General configuration ------------------------------------------------ # suppress_warnings = [ # ] # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or custom ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.autosectionlabel', # 'sphinx.ext.coverage', 'sphinx.ext.doctest', 'sphinx.ext.extlinks', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode', # 'sphinx.ext.todo', ] try: import sphinx_copybutton # noqa: F401 except ImportError: pass else: extensions.append('sphinx_copybutton') intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} #todo_include_todos = True # Add any paths that contain templates here, relative to this directory. #templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'bidict' author = bidict.metadata.__author__['name'] copyright = bidict.metadata.__copyright__.lstrip('© ') # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # The full version, including alpha/beta/rc tags. release = bidict.__version__ # The short X.Y version. version = '.'.join(release.split('.')[:2]) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. # pygments_style = 'colorful' # A list of ignored prefixes for module index sorting. modindex_common_prefix = ['bidict.'] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'furo' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # https://pradyunsg.me/furo/customisation/#theme-options # html_theme_options = dict( # ) # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". html_title = 'bidict' # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = 'bidict' # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = '_static/logo-256.png' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. html_favicon = '_static/favicon.ico' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # https://docs.readthedocs.io/en/stable/guides/adding-custom-css.html#adding-custom-css-or-javascript-to-sphinx-documentation # html_css_files = ['custom.css'] html_js_files = ['custom.js'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # https://pradyunsg.me/furo/customisation/sidebar/#using-html-sidebars html_sidebars = { '**': [ 'sidebar/brand.html', 'sidebar/search.html', 'sidebar/scroll-start.html', 'sidebar/navigation.html', 'sidebar/scroll-end.html', ], } # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. html_split_index = True # If true, links to the reST sources are added to the pages. html_show_sourcelink = False # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. html_show_sphinx = False # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'bidictdoc' # https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html extlinks = { 'issue': ('https://github.com/jab/bidict/issues/%s', '#%s') } # Ignore urls matching these regex strings when doing "make linkcheck" linkcheck_ignore = [ r'https://codecov\.io/.*', # gives 405 for HEAD requests r'https://pypistats\.org/.*', # unreliable r'https://gitpod\.io/#.*', # linkcheck complains about anchor links on this site # alternative links for readers on GitHub (which don't work on readthedocs.io): r'docs/learning-from-bidict\.rst', r'CHANGELOG\.rst', r'docs/intro\.rst', r'CONTRIBUTING\.rst', r'CODE_OF_CONDUCT\.rst', ] linkcheck_timeout = 10 # 5s default too low # http://www.sphinx-doc.org/en/stable/ext/autosectionlabel.html#configuration autosectionlabel_prefix_document = True # https://www.sphinx-doc.org/en/3.x/usage/extensions/autodoc.html#confval-autodoc_typehints autodoc_typehints = 'description' # http://www.sphinx-doc.org/en/master/usage/extensions/doctest.html doctest_global_setup = """ import sys not_cpython = sys.implementation.name != 'cpython' """ # https://sphinx-copybutton.readthedocs.io/en/latest/#strip-and-configure-input-prompts-for-code-cells copybutton_prompt_text = '>>> ' bidict-0.22.1/docs/contributors-guide.rst000077700000000000000000000000001435416354600232262../CONTRIBUTING.rstustar00rootroot00000000000000bidict-0.22.1/docs/extending.rst000066400000000000000000000222301435416354600165130ustar00rootroot00000000000000Extending ``bidict`` -------------------- Although :mod:`bidict` provides the various bidirectional mapping types covered already, it's possible that some use case might require something more than what's provided. For this reason, :mod:`bidict` was written with extensibility in mind. Let's look at some examples. ``YoloBidict`` Recipe ##################### If you'd like :attr:`~bidict.ON_DUP_DROP_OLD` to be the default :class:`~bidict.bidict.on_dup` behavior (for :meth:`~bidict.bidict.__init__`, :meth:`~bidict.bidict.__setitem__`, and :meth:`~bidict.bidict.update`), you can use the following recipe: .. doctest:: >>> from bidict import bidict, ON_DUP_DROP_OLD >>> class YoloBidict(bidict): ... on_dup = ON_DUP_DROP_OLD >>> b = YoloBidict({'one': 1}) >>> b['two'] = 1 # succeeds, no ValueDuplicationError >>> b YoloBidict({'two': 1}) >>> b.update({'three': 1}) # ditto >>> b YoloBidict({'three': 1}) Of course, ``YoloBidict``'s inherited :meth:`~bidict.bidict.put` and :meth:`~bidict.bidict.putall` methods still allow specifying a custom :class:`~bidict.OnDup` per call via the *on_dup* argument, and will both still default to raising for all duplication types. Further demonstrating :mod:`bidict`'s extensibility, to make an ``OrderedYoloBidict``, simply have the subclass above inherit from :class:`bidict.OrderedBidict` rather than :class:`bidict.bidict`. Beware of ``ON_DUP_DROP_OLD`` ::::::::::::::::::::::::::::: There's a good reason that :mod:`bidict` does not provide a ``YoloBidict`` out of the box. Before you decide to use a ``YoloBidict`` in your own code, beware of the following potentially unexpected, dangerous behavior: .. doctest:: >>> b = YoloBidict({'one': 1, 'two': 2}) # contains two items >>> b['one'] = 2 # update one of the items >>> b # now only has one item! YoloBidict({'one': 2}) As covered in :ref:`basic-usage:Key and Value Duplication`, setting an existing key to the value of a different existing item causes both existing items to quietly collapse into a single new item. A safer example of this type of customization would be something like: .. doctest:: >>> from bidict import ON_DUP_RAISE >>> class YodoBidict(bidict): # Note, "Yodo" with a "d" ... on_dup = ON_DUP_RAISE >>> b = YodoBidict({'one': 1}) >>> b['one'] = 2 # Works with a regular bidict, but Yodo plays it safe. Traceback (most recent call last): ... bidict.KeyDuplicationError: one >>> b YodoBidict({'one': 1}) >>> b.forceput('one', 2) # Any destructive change requires more force. >>> b YodoBidict({'one': 2}) ``WeakrefBidict`` Recipe ######################## Suppose you need a custom bidict type that only retains weakrefs to some objects whose refcounts you're trying not increment. With :class:`~bidict.BidictBase`\'s :attr:`~bidict.BidictBase._fwdm_cls` (forward mapping class) and :attr:`~bidict.BidictBase._invm_cls` (inverse mapping class) attributes, accomplishing this is as simple as: .. doctest:: >>> from bidict import MutableBidict >>> from weakref import WeakKeyDictionary, WeakValueDictionary >>> class WeakrefBidict(MutableBidict): ... _fwdm_cls = WeakKeyDictionary ... _invm_cls = WeakValueDictionary Now you can insert items into *WeakrefBidict* without incrementing any refcounts: .. doctest:: >>> id_by_obj = WeakrefBidict() >>> class MyObj: ... def __init__(self, id): ... self.id = id ... def __repr__(self): ... return f'' >>> o1, o2 = MyObj(1), MyObj(2) >>> id_by_obj[o1] = o1.id >>> id_by_obj[o2] = o2.id >>> id_by_obj WeakrefBidict({: 1, : 2}) >>> id_by_obj.inverse WeakrefBidictInv({1: , 2: }) If you drop your references to your objects, you can see that they get deallocated on CPython right away, since your *WeakrefBidict* isn't holding on to them: .. doctest:: :skipif: not_cpython >>> del o1, o2 >>> id_by_obj WeakrefBidict() ``SortedBidict`` Recipes ######################## Suppose you need a bidict that maintains its items in sorted order. The Python standard library does not include any sorted dict types, but the excellent `sortedcontainers `__ and `sortedcollections `__ libraries do. Armed with these, along with :class:`~bidict.BidictBase`'s :attr:`~bidict.BidictBase._fwdm_cls` (forward mapping class) and :attr:`~bidict.BidictBase._invm_cls` (inverse mapping class) attributes, creating a sorted bidict is simple: .. doctest:: >>> from sortedcontainers import SortedDict >>> class SortedBidict(MutableBidict): ... """A sorted bidict whose forward items stay sorted by their keys, ... and whose inverse items stay sorted by *their* keys. ... Note: As a result, an instance and its inverse yield their items ... in different orders. ... """ ... _fwdm_cls = SortedDict ... _invm_cls = SortedDict ... _repr_delegate = list # only used for list-style repr >>> b = SortedBidict({'Tokyo': 'Japan', 'Cairo': 'Egypt'}) >>> b SortedBidict([('Cairo', 'Egypt'), ('Tokyo', 'Japan')]) >>> b['Lima'] = 'Peru' >>> list(b.items()) # stays sorted by key [('Cairo', 'Egypt'), ('Lima', 'Peru'), ('Tokyo', 'Japan')] >>> list(b.inverse.items()) # .inverse stays sorted by *its* keys (b's values) [('Egypt', 'Cairo'), ('Japan', 'Tokyo'), ('Peru', 'Lima')] Here's a recipe for a sorted bidict whose forward items stay sorted by their keys, and whose inverse items stay sorted by their values. i.e. An instance and its inverse will yield their items in *the same* order: .. doctest:: >>> from sortedcollections import ValueSortedDict >>> class KeySortedBidict(MutableBidict): ... _fwdm_cls = SortedDict ... _invm_cls = ValueSortedDict ... _repr_delegate = list >>> elem_by_atomicnum = KeySortedBidict({ ... 6: 'carbon', 1: 'hydrogen', 2: 'helium'}) >>> list(elem_by_atomicnum.items()) # stays sorted by key [(1, 'hydrogen'), (2, 'helium'), (6, 'carbon')] >>> list(elem_by_atomicnum.inverse.items()) # .inverse stays sorted by value [('hydrogen', 1), ('helium', 2), ('carbon', 6)] >>> elem_by_atomicnum[4] = 'beryllium' >>> list(elem_by_atomicnum.inverse.items()) [('hydrogen', 1), ('helium', 2), ('beryllium', 4), ('carbon', 6)] Automatic "Get Attribute" Pass-Through ###################################### Python makes it easy to customize a class's "get attribute" behavior. You can take advantage of this to pass attribute access through to the backing ``_fwdm`` mapping, for example, when an attribute is not provided by the bidict class itself: >>> def __getattribute__(self, name): ... try: ... return object.__getattribute__(self, name) ... except AttributeError: ... return getattr(self._fwdm, name) >>> KeySortedBidict.__getattribute__ = __getattribute__ Now, even though this ``KeySortedBidict`` itself provides no ``peekitem`` attribute, the following call still succeeds because it's passed through to the backing ``SortedDict``: >>> elem_by_atomicnum.peekitem() (6, 'carbon') Dynamic Inverse Class Generation ################################ When a bidict class's :attr:`~bidict.BidictBase._fwdm_cls` and :attr:`~bidict.BidictBase._invm_cls` are the same, the bidict class is its own inverse class. (This is the case for all the :ref:`bidict classes ` that come with :mod:`bidict`.) However, when a bidict's :attr:`~bidict.BidictBase._fwdm_cls` and :attr:`~bidict.BidictBase._invm_cls` differ, as in the ``KeySortedBidict`` and ``WeakrefBidict`` recipes above, the inverse class of the bidict needs to have its :attr:`~bidict.BidictBase._fwdm_cls` and :attr:`~bidict.BidictBase._invm_cls` swapped. :class:`~bidict.BidictBase` detects this and dynamically computes the correct inverse class for you automatically. You can see this if you inspect ``KeySortedBidict``'s inverse bidict: >>> elem_by_atomicnum.inverse.__class__.__name__ 'KeySortedBidictInv' Notice that :class:`~bidict.BidictBase` automatically created a ``KeySortedBidictInv`` class and used it for the inverse bidict. As expected, ``KeySortedBidictInv``'s :attr:`~bidict.BidictBase._fwdm_cls` and :attr:`~bidict.BidictBase._invm_cls` are the opposite of ``KeySortedBidict``'s: >>> elem_by_atomicnum.inverse._fwdm_cls.__name__ 'ValueSortedDict' >>> elem_by_atomicnum.inverse._invm_cls.__name__ 'SortedDict' :class:`~bidict.BidictBase` also ensures that round trips work as expected: >>> KeySortedBidictInv = elem_by_atomicnum.inverse.__class__ # i.e. a value-sorted bidict >>> atomicnum_by_elem = KeySortedBidictInv(elem_by_atomicnum.inverse) >>> atomicnum_by_elem KeySortedBidictInv([('hydrogen', 1), ('helium', 2), ('beryllium', 4), ('carbon', 6)]) >>> KeySortedBidict(atomicnum_by_elem.inverse) == elem_by_atomicnum True ----- This all goes to show how simple it can be to compose your own bidirectional mapping types out of the building blocks that :mod:`bidict` provides. bidict-0.22.1/docs/index.rst000066400000000000000000000003601435416354600156350ustar00rootroot00000000000000.. toctree:: :hidden: intro basic-usage other-bidict-types extending other-functionality addendum changelog api learning-from-bidict contributors-guide code-of-conduct thanks .. include:: README.rst bidict-0.22.1/docs/intro.rst000066400000000000000000000125141435416354600156650ustar00rootroot00000000000000Introduction ============ The :mod:`bidict` library provides several friendly, efficient data structures for working with `bidirectional mappings `__ in Python. bidict.bidict ------------- :class:`bidict.bidict` is the main bidirectional mapping data structure provided. It allows looking up the value associated with a key, just like a :class:`dict`: .. testsetup:: from bidict import bidict .. doctest:: >>> element_by_symbol = bidict({'H': 'hydrogen'}) >>> element_by_symbol['H'] 'hydrogen' But it also allows looking up the key associated with a value, via the special :attr:`~bidict.BidictBase.inverse` attribute: .. doctest:: >>> element_by_symbol.inverse['hydrogen'] 'H' The :attr:`~bidict.BidictBase.inverse` attribute actually references the entire inverse bidirectional mapping: .. doctest:: >>> element_by_symbol bidict({'H': 'hydrogen'}) >>> element_by_symbol.inverse bidict({'hydrogen': 'H'}) ...which is automatically kept in sync as the original mapping is updated: .. doctest:: >>> element_by_symbol['H'] = 'hydrogène' >>> element_by_symbol.inverse bidict({'hydrogène': 'H'}) If you're used to working with :class:`dict`\s, you'll feel right at home using :mod:`bidict`: >>> dir(element_by_symbol) [..., '__getitem__', ..., '__setitem__', ..., 'items', 'keys', ...] Familiar, concise, Pythonic. Why can't I just use a dict? ---------------------------- A skeptic writes: If I want a mapping associating *a* → *b* and *b* → *a*, I can just create the dict ``{a: b, b: a}``. Why bother using :mod:`bidict`? One answer is better ergonomics for maintaining a correct representation. For example, to get the correct length, you'd have to take the number reported by :func:`len` and cut it in half. But now consider what happens when we need to store a new association, and we try to do so naively: .. code-block:: python >>> el_by_sym = {'H': 'hydrogen', 'hydrogen': 'H'} >>> # Later we need to associate 'H' with a different value >>> el_by_sym.update({'H': 'hydrogène', 'hydrogène': 'H'} # Too naive Here is what we're left with: .. code-block:: python >>> el_by_sym {'H': 'hydrogène', 'hydrogène': 'H', 'hydrogen': 'H'} Oops. We forgot to look up whether the key and value we wanted to set already had any previous associations and remove them as necessary. In general, if we want to store the association *k* ⟷ *v*, but we may have already stored the associations *k* ⟷ *v′* or *k′* ⟷ *v*, a correct implementation using the single-dict approach would require code like this: .. doctest:: >>> d = {'H': 'hydrogen', 'hydrogen': 'H'} >>> def update(d, key, val): ... oldval = d.pop(key, object()) ... d.pop(oldval, None) ... oldkey = d.pop(val, object()) ... d.pop(oldkey, None) ... d.update({key: val, val: key}) >>> update(d, 'H', 'hydrogène') >>> d == {'H': 'hydrogène', 'hydrogène': 'H'} True With :mod:`bidict`, we can instead just write: .. doctest:: >>> b = bidict({'H': 'hydrogen'}) >>> b['H'] = 'hydrogène' And :mod:`bidict` takes care of all the fussy details, leaving us with just what we wanted: .. doctest:: >>> b bidict({'H': 'hydrogène'}) >>> b.inverse bidict({'hydrogène': 'H'}) Even more important... ++++++++++++++++++++++ Beyond this, consider what would happen if we needed to work with just the keys, values, or items that we have associated. Since the single-dict approach inserts values as keys into the same dict that it inserts keys into, we'd never be able to tell our keys and values apart. So iterating over the keys would also yield the values (and vice versa), with no way to tell which was which. Iterating over the items would yield twice as many as we wanted, with a *(v, k)* item that we'd have to ignore for each *(k, v)* item that we expect, and no way to tell which was which. .. doctest:: >>> # Compare the single-dict approach: >>> set(d.keys()) == {'H', 'hydrogène'} # .keys() also gives values True >>> set(d.values()) == {'H', 'hydrogène'} # .values() also gives keys True >>> # ...to using a bidict: >>> b.keys() == {'H'} # just the keys True >>> b.values() == {'hydrogène'} # just the values True In short, to model a bidirectional mapping correctly and unambiguously, we need two separate one-directional mappings, one for the forward associations and one for the inverse, that are kept in sync as the associations change. This is exactly what :mod:`bidict` does under the hood, abstracting it into a clean and ergonomic interface. :mod:`bidict`'s APIs also provide power, flexibility, and safety, making sure the one-to-one invariant is maintained and inverse mappings are kept consistent, while also helping make sure you don't accidentally :ref:`shoot yourself in the foot `. Additional Functionality ------------------------ Besides the standard :class:`bidict.bidict` type, the :mod:`bidict` module provides other bidirectional mapping variants: :class:`~bidict.frozenbidict`, :class:`~bidict.OrderedBidict`, :class:`~bidict.FrozenOrderedBidict`, and :func:`~bidict.namedbidict`. These, and :mod:`bidict`'s other functionality, will be covered in later sections. But first, let's look at a few more details of :doc:`basic-usage`. bidict-0.22.1/docs/learning-from-bidict.rst000066400000000000000000000462441435416354600205350ustar00rootroot00000000000000Learning from ``bidict`` ------------------------ Working on :mod:`bidict` has taken me to some of the most interesting and unexpected places I've ever gotten to visit in many years of programming. (When I started this project ~15 years ago, I'd never heard of things like higher-kinded types. Thanks to :mod:`bidict`, I not only came across them, I even got to `share a practical, real-world example with Guido `__ where they would be beneficial for Python.) The problem space that :mod:`bidict` inhabits is abundant with beautiful symmetries, delightful surprises, and rich opportunities to come up with elegant solutions. You can check out :mod:`bidict`'s source to see for yourself. I've sought to optimize the code not just for correctness and performance, but also for clarity, maintainability, and to make for an enjoyable read. See below for more, and feel free to let me know what you think. I hope reading :mod:`bidict`'s code brings you some of the `joy `__ that :mod:`bidict` has brought me. Code structure ============== :class:`~bidict.bidict`\s come in every combination of mutable, immutable, ordered, and unordered types, implementing Python's various :class:`relevant ` :class:`collections ` :class:`interfaces ` as appropriate. Factoring the code to maximize reuse, modularity, and adherence to `SOLID `__ design principles (while not missing any chances for specialized optimizations) has been one of the most fun parts of working on bidict. To see how this is done, check out the code starting with `__init__.py `__, and then follow the path suggested in the "code review nav" comments at the top of the file: - `_base.py `__ - `_frozenbidict.py `__ - `_bidict.py `__ - `_orderedbase.py `__ - `_frozenordered.py `__ - `_orderedbidict.py `__ Data structures are amazing =========================== Data structures are one of the most fascinating and important building blocks of programming and computer science. It's all too easy to lose sight of the magic when having to implement them for computer science courses or job interview questions. Part of this is because many of the most interesting real-world details get left out, and you miss all the value that comes from ongoing, direct practical application. Bidict shows how fundamental data structures can be implemented in Python for important real-world usage, with practical concerns at top of mind. .. admonition:: To give you a taste... A regular :class:`~bidict.bidict` encapsulates two regular dicts, keeping them in sync to preserve the bidirectional mapping invariants. Since dicts are unordered, regular bidicts are unordered too. How should we extend this to implement an ordered bidict? :class:`~bidict.OrderedBidictBase` inherits from :class:`~bidict.BidictBase` the use of two regular dicts to store the forward and inverse associations. And to store the ordering of the associations, we use a doubly-linked list. This allows us to e.g. move any item to the front of the ordering in O(1) time. Interestingly, the nodes of the linked list encode only the ordering of the items; the nodes themselves contain no key or value data. An additional backing mapping associates the key/value data with the nodes, providing the final piece of the puzzle. And since :class:`~bidict.OrderedBidictBase` needs to not only look up nodes by key/value, but also key/value by node, it uses an (unordered) :class:`~bidict.bidict` for this internally. Bidicts all the way down! Python syntax hacks =================== :mod:`bidict` used to support (ab)using a specialized form of Python's :ref:`slice ` syntax for getting and setting keys by value: .. use `code-block` rather than `doctest` for this since slice syntax is no longer supported: .. code-block:: python >>> element_by_symbol = bidict(H='hydrogen') >>> element_by_symbol['H'] # [normal] syntax for the forward mapping 'hydrogen' >>> element_by_symbol[:'hydrogen'] # [:slice] syntax for the inverse (no longer supported) 'H' See `this code `__ for how this was implemented, and :issue:`19` for why this was dropped. Property-based testing is indispensable ======================================= When your automated tests run, are they only checking the test cases you happened to hard-code into your test suite? How do you know these test cases aren't missing some important edge cases? With property-based testing, you describe the types of test case inputs your functions accept, along with the properties that should hold for all inputs. Rather than having to think up your test case inputs manually and hard-code them into your test suite, they get generated for you dynamically, in much greater quantity and edge case-exercising diversity than you could come up with by hand. This dramatically increases test coverage and confidence that your code is correct. Bidict never would have survived so many refactorings with so few bugs if it weren't for property-based testing, enabled by the amazing `Hypothesis `__ library. Check out `bidict's property-based tests `__ to see this in action. Python surprises ================ - What should happen when checking equality of several ordered mappings that contain the same items but in a different order? What about when comparing an ordered mapping with an unordered mapping? First let's see how :class:`collections.OrderedDict` works. The results may surprise you: .. doctest:: >>> from collections import OrderedDict >>> x = OrderedDict({1: 1, 2: 2}) >>> y = {1: 1, 2: 2} >>> z = OrderedDict({2: 2, 1: 1}) >>> x == y True >>> y == z True >>> x == z False So :class:`collections.OrderedDict` violates the `transitive property of equality `__. This can lead to some even more unusual behavior than the above. As an example, let's see what would happen if :class:`bidict.FrozenOrderedBidict.__eq__` behaved this way: .. testsetup:: from bidict import FrozenOrderedBidict, frozenbidict .. doctest:: >>> class BadFrozenOrderedBidict(FrozenOrderedBidict): ... __hash__ = FrozenOrderedBidict.__hash__ ... ... def __eq__(self, other): # (deliberately simplified) ... # Override to be order-sensitive, like collections.OrderedDict: ... return all(i == j for (i, j) in zip(self.items(), other.items())) >>> x = BadFrozenOrderedBidict({1: 1, 2: 2}) >>> y = frozenbidict({1: 1, 2: 2}) >>> z = BadFrozenOrderedBidict({2: 2, 1: 1}) >>> assert x == y and y == z and x != z >>> set1 = {x, y, z} >>> len(set1) 2 >>> set2 = {y, x, z} >>> len(set2) 1 Gotcha alert! According to Raymond Hettinger, the Python core developer who built Python's collections foundation, if we had it to do over again, we would make :meth:`collections.OrderedDict.__eq__` order-insensitive. Making ``__eq__`` order-sensitive not only violates the transitive property of equality, but also the `Liskov substitution principle `__. Unfortunately, it's too late now to fix this for :class:`collections.OrderedDict`. Fortunately though, it's not too late for bidict to learn from this. Hence :ref:`eq-order-insensitive`, even for ordered bidicts. For an order-sensitive equality check, bidict provides the separate :meth:`~bidict.BidictBase.equals_order_sensitive` method, thanks in no small part to `Raymond's good advice `__. - See :ref:`addendum:\*nan\* as a Key`. - See :ref:`addendum:Equivalent but distinct \:class\:\`~collections.abc.Hashable\`\\s`. Better memory usage through ``__slots__`` ========================================= Using :ref:`slots` speeds up attribute access, and can dramatically reduce memory usage in CPython when creating many instances of the same class. As an example, the ``Node`` class used internally by :class:`~bidict.OrderedBidictBase` to store the ordering of inserted items uses slots for better performance at scale, since as many node instances are kept in memory as there are items in every ordered bidict in memory. *See:* `_orderedbase.py `__ (Note that extra care must be taken when using slots with pickling and weakrefs.) Better memory usage through :mod:`weakref` ========================================== A :class:`~bidict.bidict` and its inverse use :mod:`weakref` to :ref:`avoid creating a reference cycle `. As a result, when you drop your last reference to a bidict, its memory is reclaimed immediately in CPython rather than having to wait for the next garbage collection. *See:* `_base.py `__ As another example, the ``Node`` class used internally by :class:`~bidict.OrderedBidictBase` uses weakrefs to avoid creating reference cycles in the doubly-linked lists used to encode the ordering of inserted items. *See:* `_orderedbase.py `__ Using descriptors for managed attributes ======================================== To abstract the details of creating and dereferencing the weakrefs that :class:`~bidict.OrderedBidictBase`\'s aforementioned doubly-linked list nodes use to refer to their neighbor nodes, a ``WeakAttr`` descriptor is used to `manage access to these attributes automatically `__. *See:* `_orderedbase.py `__ The implicit ``__class__`` reference ==================================== Anytime you have to reference the exact class of an instance (and not a potential subclass) from within a method body, you can use the implicit, lexically-scoped ``__class__`` reference rather than hard-coding the current class's name. *See:* https://docs.python.org/3/reference/datamodel.html#executing-the-class-body Subclassing :func:`~collections.namedtuple` classes =================================================== To get the performance benefits, intrinsic sortability, etc. of :func:`~collections.namedtuple` while customizing behavior, state, API, etc., you can subclass a :func:`~collections.namedtuple` class. (Make sure to include ``__slots__ = ()``, if you want to keep the associated performance benefits – see the section about slots above.) See the *OnDup* class in `_dup.py `__ for an example. Here's another example: .. doctest:: >>> from collections import namedtuple >>> from itertools import count >>> class Node(namedtuple('_Node', 'cost tiebreaker data parent depth')): ... """Represent nodes in a graph traversal. Suitable for use with e.g. heapq.""" ... ... __slots__ = () ... _counter = count() # break ties between equal-cost nodes, avoid comparing data ... ... # Give call sites a cleaner API for creating new Nodes ... def __new__(cls, cost, data, parent=None): ... tiebreaker = next(cls._counter) ... depth = parent.depth + 1 if parent else 0 ... return super().__new__(cls, cost, tiebreaker, data, parent, depth) ... ... def __repr__(self): ... return 'Node(cost={cost}, data={data!r})'.format(**self._asdict()) >>> start = Node(cost=0, data='foo') >>> child = Node(cost=5, data='bar', parent=start) >>> child Node(cost=5, data='bar') >>> child.parent Node(cost=0, data='foo') >>> child.depth 1 :func:`~collections.namedtuple`-style dynamic class generation ============================================================== See the `implementation `__ of :func:`~bidict.namedbidict`. API Design ========== How to deeply integrate with Python's :mod:`collections` and other built-in APIs? - Beyond implementing :class:`collections.abc.Mapping`, bidicts implement additional APIs that :class:`dict` and :class:`~collections.OrderedDict` implement (e.g. :meth:`setdefault`, :meth:`popitem`, etc.). - When creating a new API, making it familiar, memorable, and intuitive is hugely important to a good user experience. - Thanks to :class:`~collections.abc.Hashable`'s implementing :meth:`abc.ABCMeta.__subclasshook__`, any class that implements the required methods of the :class:`~collections.abc.Hashable` interface (namely, :meth:`~collections.abc.Hashable.__hash__`) makes it a virtual subclass already, no need to explicitly extend. I.e. As long as ``Foo`` implements a ``__hash__()`` method, ``issubclass(Foo, Hashable)`` will always be True, no need to explicitly subclass via ``class Foo(Hashable): ...`` - How to make your own open ABC like :class:`~collections.abc.Hashable`? - Override :meth:`~abc.ABCMeta.__subclasshook__` to check for the interface you require. - Interesting consequence of the ``__subclasshook__()`` design: the "subclass" relation becomes intransitive. e.g. :class:`object` is a subclass of :class:`~collections.abc.Hashable`, :class:`list` is a subclass of :class:`object`, but :class:`list` is not a subclass of :class:`~collections.abc.Hashable`. - What if you needed to derive from a second metaclass? Be careful to avoid "TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases". See the great write-up in https://blog.ionelmc.ro/2015/02/09/understanding-python-metaclasses/. - :class:`collections.abc.Mapping` and :class:`collections.abc.MutableMapping` don't implement :meth:`~abc.ABCMeta.__subclasshook__`, so you must either explicitly subclass them (in which case you inherit their concrete method implementations) or use :meth:`abc.ABCMeta.register` (to register as a virtual subclass without inheriting any of the implementation). - Notice that Python provides :class:`collections.abc.Reversible` but no ``collections.abc.Ordered`` or ``collections.abc.OrderedMapping``. *See:* ``__ - See the `Zen of Python `__ for how to make APIs Pythonic. The following Zen of Python guidelines have been particularly influential for bidict: - "Errors should never pass silently. Unless explicitly silenced. - "In the face of ambiguity, refuse the temptation to guess." - "Readability counts." - "There should be one – and preferably only one – obvious way to do it." Python's data model =================== - What happens when you implement a custom :meth:`~object.__eq__`? e.g. What's the difference between ``a == b`` and ``b == a`` when only ``a`` is an instance of your class? See the great write-up in https://eev.ee/blog/2012/03/24/python-faq-equality/ for the answer. - Making an immutable type hashable (so it can be inserted into :class:`dict`\s and :class:`set`\s): Must implement :meth:`~object.__hash__` such that ``a == b ⇒ hash(a) == hash(b)``. See the :meth:`object.__hash__` and :meth:`object.__eq__` docs, and the `implementation `__ of :class:`~bidict.frozenbidict`. - Consider :class:`~bidict.FrozenOrderedBidict`: its :meth:`~bidict.FrozenOrderedBidict.__eq__` is :ref:`order-insensitive `. So all contained items must participate in the hash order-insensitively. - Can use `collections.abc.Set._hash `__ which provides a pure Python implementation of the same hash algorithm used to hash :class:`frozenset`\s. (Since :class:`~collections.abc.ItemsView` extends :class:`~collections.abc.Set`, :meth:`bidict.frozenbidict.__hash__` just calls ``ItemsView(self)._hash()``.) - See also ``__ - Unlike other attributes, if a class implements ``__hash__()``, any subclasses of that class will not inherit it. It's like Python implicitly adds ``__hash__ = None`` to the body of every class that doesn't explicitly define ``__hash__``. So if you do want a subclass to inherit a base class's ``__hash__()`` implementation, you have to set that manually, e.g. by adding ``__hash__ = BaseClass.__hash__`` in the class body. This is consistent with the fact that :class:`object` implements ``__hash__()``, but subclasses of :class:`object` that override :meth:`~object.__eq__` are not hashable by default. - Overriding :meth:`object.__getattribute__` for custom attribute lookup. See :ref:`extending:\`\`SortedBidict\`\` Recipes`. - Using :meth:`object.__getstate__`, :meth:`object.__setstate__`, and :meth:`object.__reduce__` to make an object pickleable that otherwise wouldn't be, due to e.g. using weakrefs, as bidicts do (covered further below). Portability =========== - CPython vs. PyPy (and other Python implementations) - See https://doc.pypy.org/en/latest/cpython_differences.html - gc / weakref - Hence ``test_bidicts_freed_on_zero_refcount()`` in `test_properties.py `__ is skipped outside CPython. - primitives' identities, nan, etc. - Python 2 vs. Python 3 - As affects bidict, mostly :class:`dict` API changes, but also functions like :func:`zip`, :func:`map`, :func:`filter`, etc. - :meth:`~object.__ne__` fixed in Python 3 - Borrowing methods from other classes: In Python 2, must grab the ``.im_func`` / ``__func__`` attribute off the borrowed method to avoid getting ``TypeError: unbound method ...() must be called with ... instance as first argument`` Other interesting stuff in the standard library =============================================== - :mod:`reprlib` and :func:`reprlib.recursive_repr` (but not needed for bidict because there's no way to insert a bidict into itself) - :func:`operator.methodcaller` - See :ref:`addendum:Missing \`\`bidict\`\`\\s in the Standard Library` Tools ===== See the :ref:`Thanks ` page for some of the fantastic tools for software verification, performance, code quality, etc. that bidict has provided a great opportunity to learn and use. bidict-0.22.1/docs/other-bidict-types.rst000066400000000000000000000333141435416354600202520ustar00rootroot00000000000000Other ``bidict`` Types ====================== Now that we've covered :doc:`basic-usage` with the :class:`bidict.bidict` type, let's look at some other bidirectional mapping types. .. testsetup:: from bidict import bidict, BidirectionalMapping, FrozenOrderedBidict from collections.abc import Mapping, MutableMapping Bidict Types Diagram -------------------- .. image:: _static/bidict-types-diagram.png :target: _static/bidict-types-diagram.png :alt: bidict types diagram All bidirectional mapping types that :mod:`bidict` provides are subclasses of :class:`bidict.BidirectionalMapping`. This abstract base class extends :class:`collections.abc.Mapping` by adding the ":attr:`~bidict.BidirectionalMapping.inverse`" :obj:`~abc.abstractproperty`. As you may have noticed, :class:`bidict.bidict` is also a :class:`collections.abc.MutableMapping`. But :mod:`bidict` provides immutable bidirectional mapping types as well. :class:`~bidict.frozenbidict` ----------------------------- :class:`~bidict.frozenbidict` is an immutable, hashable bidirectional mapping type. As you would expect, attempting to mutate a :class:`~bidict.frozenbidict` causes an error: .. doctest:: >>> from bidict import frozenbidict >>> f = frozenbidict({'H': 'hydrogen'}) >>> f['C'] = 'carbon' Traceback (most recent call last): ... TypeError: 'frozenbidict' object does not support item assignment :class:`~bidict.frozenbidict` also implements :class:`collections.abc.Hashable`, so it's suitable for insertion into sets or other mappings: .. doctest:: >>> my_set = {f} # not an error >>> my_dict = {f: 1} # also not an error See the :class:`~bidict.frozenbidict` API documentation for more information. :class:`~bidict.OrderedBidict` ------------------------------ :class:`bidict.OrderedBidict` is a :class:`~bidict.MutableBidirectionalMapping` that preserves the ordering of its items, and offers some additional ordering-related APIs that unordered bidicts can't offer. It's like a bidirectional version of :class:`collections.OrderedDict`. .. doctest:: >>> from bidict import OrderedBidict >>> element_by_symbol = OrderedBidict([ ... ('H', 'hydrogen'), ('He', 'helium'), ('Li', 'lithium')]) >>> element_by_symbol.inverse OrderedBidict([('hydrogen', 'H'), ('helium', 'He'), ('lithium', 'Li')]) >>> first, second, third = element_by_symbol.values() >>> first, second, third ('hydrogen', 'helium', 'lithium') >>> # Insert an additional item and verify it now comes last: >>> element_by_symbol['Be'] = 'beryllium' >>> last_item = list(element_by_symbol.items())[-1] >>> last_item ('Be', 'beryllium') Additional, efficiently-implemented, order-mutating APIs modeled after :class:`~collections.OrderedDict`, e.g. :meth:`popitem(last: bool) `, which makes ordered bidicts suitable for use as FIFO queues, and :meth:`move_to_end(last: bool) `, are provided as well: .. doctest:: >>> element_by_symbol.popitem(last=True) # Remove the last item ('Be', 'beryllium') >>> element_by_symbol.popitem(last=False) # Remove the first item ('H', 'hydrogen') >>> # Re-adding hydrogen after it's been removed moves it to the end: >>> element_by_symbol['H'] = 'hydrogen' >>> element_by_symbol OrderedBidict([('He', 'helium'), ('Li', 'lithium'), ('H', 'hydrogen')]) >>> # But there's also a `move_to_end` method just for this purpose: >>> element_by_symbol.move_to_end('Li') >>> element_by_symbol OrderedBidict([('He', 'helium'), ('H', 'hydrogen'), ('Li', 'lithium')]) >>> element_by_symbol.move_to_end('H', last=False) # move to front >>> element_by_symbol OrderedBidict([('H', 'hydrogen'), ('He', 'helium'), ('Li', 'lithium')]) As with :class:`~collections.OrderedDict`, updating an existing item preserves its position in the order: .. doctest:: >>> element_by_symbol['He'] = 'updated in place!' >>> element_by_symbol OrderedBidict([('H', 'hydrogen'), ('He', 'updated in place!'), ('Li', 'lithium')]) Collapsing overwrites ##################### When setting an item in an ordered bidict whose key duplicates that of an existing item, and whose value duplicates that of a *different* existing item, the existing item whose *value* is duplicated will be dropped, and the existing item whose *key* is duplicated will have its value overwritten in place: .. doctest:: >>> o = OrderedBidict([(1, 2), (3, 4), (5, 6), (7, 8)]) >>> o.forceput(3, 8) # item with duplicated value (7, 8) is dropped... >>> o # and the item with duplicated key (3, 4) is updated in place: OrderedBidict([(1, 2), (3, 8), (5, 6)]) >>> # (3, 8) took the place of (3, 4), not (7, 8) >>> o = OrderedBidict([(1, 2), (3, 4), (5, 6), (7, 8)]) # as before >>> o.forceput(5, 2) # another example >>> o OrderedBidict([(3, 4), (5, 2), (7, 8)]) >>> # (5, 2) took the place of (5, 6), not (1, 2) .. _eq-order-insensitive: :meth:`~bidict.OrderedBidict.__eq__` is order-insensitive ######################################################### To ensure that equals comparison for any bidict always upholds the `transitive property of equality `__ and the `Liskov substitution principle `__, equality tests between a bidict and another mapping are always order-insensitive, even for ordered bidicts: .. doctest:: >>> o1 = OrderedBidict({1: 1, 2: 2}) >>> o2 = OrderedBidict({2: 2, 1: 1}) >>> o1 == o2 True For order-sensitive equality tests, use :meth:`~bidict.BidictBase.equals_order_sensitive`: .. doctest:: >>> o1.equals_order_sensitive(o2) False (Note that this differs from the behavior of :meth:`collections.OrderedDict.__eq__`, and for good reason, by recommendation of the Python core developer who designed and implemented :class:`~collections.OrderedDict`. For more about this, see :ref:`learning-from-bidict:Python surprises`.) What about order-preserving dicts? ################################## In CPython 3.6+ and all versions of PyPy, :class:`dict` (which bidicts are built on by default) preserves insertion order. Given that, can you get away with using an unordered bidict in places where you need an order-preserving bidirectional mapping? Of course, this assumes you don't need the additional APIs offered only by :class:`~bidict.OrderedBidict`, such as :meth:`popitem(last=False) `, which makes it suitable for use as a FIFO queue. Consider this example: .. doctest:: >>> b = bidict({1: -1, 2: -2, 3: -3}) >>> b[2] = 'UPDATED' >>> b bidict({1: -1, 2: 'UPDATED', 3: -3}) So far so good, but look what happens here: .. doctest:: >>> b.inverse bidict({-1: 1, -3: 3, 'UPDATED': 2}) The ordering of items between the bidict and its inverse instance is no longer consistent. To ensure that ordering is kept consistent between a bidict and its inverse, no matter how it's mutated, you have to use an ordered bidict: >>> ob = OrderedBidict({1: -1, 2: -2, 3: -3}) >>> ob[2] = 'UPDATED' >>> ob OrderedBidict([(1, -1), (2, 'UPDATED'), (3, -3)]) >>> ob.inverse OrderedBidict([(-1, 1), ('UPDATED', 2), (-3, 3)]) The ordered bidict and its inverse always give you a consistent ordering. That said, if you depend on preserving insertion order, an unordered bidict may be sufficient if: * you'll never mutate it (in which case, use a :class:`~bidict.frozenbidict`), or: * you only mutate by removing and/or adding whole new items, never changing just the key or value of an existing item, or: * you only depend on the order in the forward bidict, and are only changing existing items in the forward direction (i.e. changing values by key, rather than changing keys by value). On the other hand, if your code is actually depending on the order, using an explicitly-ordered bidict type makes for clearer code. :class:`~bidict.OrderedBidict` also gives you additional, constant-time, order-mutating APIs, such as :meth:`move_to_end(last: bool) ` and :meth:`popitem(last: bool) `. These additional APIs expand the range of use cases where an :class:`~bidict.OrderedBidict` can be used. For example, ``popitem(last=False)`` allows using an :class:`~bidict.OrderedBidict` as a FIFO queue. If you're on Python <= 3.7, :class:`~bidict.OrderedBidict` also gives you :meth:`~bidict.OrderedBidict.__reversed__`, which you don't get with unordered bidicts unless you upgrade to Python 3.8+. :class:`~bidict.FrozenOrderedBidict` ------------------------------------ :class:`~bidict.FrozenOrderedBidict` is an immutable ordered bidict type. It's like a :class:`hashable ` :class:`~bidict.OrderedBidict` without the mutating APIs, or like a :class:`reversible ` :class:`~bidict.frozenbidict` even on Python < 3.8. (All :class:`~bidict.bidict`\s are `order-preserving when never mutated <#what-about-order-preserving-dicts>`__, so :class:`~bidict.frozenbidict` is already order-preserving, but only on Python 3.8+, where :class:`dict`\s are :class:`reversible `, are all :class:`~bidict.bidict`\s (including :class:`~bidict.frozenbidict`) also :class:`reversible `.) If you are using Python 3.8+, :class:`~bidict.frozenbidict` gives you everything that :class:`~bidict.FrozenOrderedBidict` gives you, but with less space overhead. :func:`~bidict.namedbidict` --------------------------- :func:`bidict.namedbidict`, inspired by :func:`collections.namedtuple`, allows you to easily generate a new bidirectional mapping type with custom attribute-based access to forward and inverse mappings: .. doctest:: >>> from bidict import namedbidict >>> ElementBySymbolBidict = namedbidict('ElementBySymbolBidict', 'symbol', 'name') >>> el_by_sym = ElementBySymbolBidict(H='hydrogen', He='helium') >>> el_by_sym.name_for['He'] 'helium' >>> el_by_sym.symbol_for['helium'] 'He' >>> el_by_sym.name_for['Ne'] = 'neon' >>> el_by_sym ElementBySymbolBidict({'H': 'hydrogen', 'He': 'helium', 'Ne': 'neon'}) >>> el_by_sym['H'] # regular lookup still works the same 'hydrogen' >>> el_by_sym.inverse['hydrogen'] # and for the inverse as well 'H' >>> el_by_sym.inverse ElementBySymbolBidictInv({'hydrogen': 'H', 'helium': 'He', 'neon': 'Ne'}) >>> el_by_sym.inverse.name_for['H'] # custom attribute lookup works on the inverse too 'hydrogen' .. note:: Notice how, unlike the other bidict types, namedbidict classes aren't their own inverse classes, because the roles of the custom attribute-based accessors are inverted when accessing the inverse. :class:`~bidict.BidictBase` realizes when a subclass is not its own inverse, and dynamically generates the inverse class for you automatically. You can see this in action above if you look at the dynamically-generated inverse class name, ``ElementBySymbolBidictInv``. For more about this, see :ref:`extending:Dynamic Inverse Class Generation`. Using the *base_type* keyword arg – whose default value is :class:`bidict.bidict` – you can customize the bidict type used as the base class. For example, the following creates a named frozenbidict type: .. doctest:: >>> FrozenElBySymBidict = namedbidict('FrozenElBySymBidict', 'sym', 'name', base_type=frozenbidict) >>> noble = FrozenElBySymBidict(He='helium', Ne='neon', Ar='argon', Kr='krypton') >>> noble.sym_for['helium'] 'He' >>> hash(noble) is not TypeError # does not raise TypeError: unhashable type True >>> noble['C'] = 'carbon' # mutation fails - it's frozen! Traceback (most recent call last): ... TypeError: 'FrozenElBySymBidict' object does not support item assignment Polymorphism ------------ Code that needs to check only whether an object is *dict-like* should not use ``isinstance(obj, dict)``. This check is too specific, because dict-like objects need not actually be instances of dict or a dict subclass. You can see this fails for many dict-like in the standard library: .. doctest:: >>> from collections import ChainMap >>> chainmap = ChainMap() >>> isinstance(chainmap, dict) False The same is true for all the bidict types: .. doctest:: >>> bi = bidict() >>> isinstance(bi, dict) False A better way to check whether an object is dict-like is to use the :class:`~collections.abc.Mapping` abstract base class (ABC) from the :mod:`collections.abc` module, which provides a number of ABCs intended for this purpose: .. doctest:: >>> isinstance(chainmap, Mapping) True >>> isinstance(bi, Mapping) True Also note that the proper way to check whether an object is an (im)mutable mapping is to use the :class:`~collections.abc.MutableMapping` ABC: .. doctest:: >>> isinstance(chainmap, MutableMapping) True >>> isinstance(bi, MutableMapping) True You can combine this with bidict's own :class:`~bidict.BidirectionalMapping` ABC to implement your own check for whether an object is an immutable, bidirectional mapping: >>> def is_immutable_bimap(obj): ... return (isinstance(obj, BidirectionalMapping) ... and not isinstance(obj, MutableMapping)) >>> is_immutable_bimap(bidict()) False >>> is_immutable_bimap(frozenbidict()) True Using this in the next example, we can see the concept above in action again: .. doctest:: >>> fb = FrozenOrderedBidict() >>> isinstance(fb, frozenbidict) False >>> is_immutable_bimap(fb) True Checking for ``isinstance(obj, frozenbidict)`` is too specific for this purpose and can fail in some cases. But using the collections ABCs as intended does the trick. For more you can do with :mod:`bidict`, check out :doc:`extending` next. bidict-0.22.1/docs/other-functionality.rst000066400000000000000000000017101435416354600205350ustar00rootroot00000000000000Other Functionality =================== :func:`bidict.inverted` ----------------------- bidict provides the :class:`~bidict.inverted` iterator to help you get inverse items from various types of objects. Pass in a mapping to get the inverse mapping: .. doctest:: >>> from bidict import inverted >>> it = inverted({1: 'one'}) >>> {k: v for (k, v) in it} {'one': 1} ...an iterable of pairs to get the pairs' inverses: .. doctest:: >>> list(inverted([(1, 'one'), (2, 'two')])) [('one', 1), ('two', 2)] >>> list(inverted((i*i, i) for i in range(2, 5))) [(2, 4), (3, 9), (4, 16)] ...or any object implementing an ``__inverted__`` method, which objects that already know their own inverses (such as bidicts) can implement themselves: .. testsetup:: from bidict import bidict, OrderedBidict .. doctest:: >>> dict(inverted(bidict({1: 'one'}))) {'one': 1} >>> list(inverted(OrderedBidict([(2, 4), (3, 9)]))) [(4, 2), (9, 3)] bidict-0.22.1/docs/thanks.rst000066400000000000000000000033071435416354600160220ustar00rootroot00000000000000Thanks ------ Bidict has benefited from the assistance of many people and projects. People ====== - Gregory Ewing for the name. - Terry Reedy for suggesting the slice syntax (it was fun while it lasted). - Raymond Hettinger for suggesting :func:`~bidict.namedbidict`, providing feedback on the design and implementation, and (most of all) for the amazing work on Python's built-in collections that made bidict possible. - Francis Carr for the idea of storing the inverse bidict. - Adopt Pytest Month 2015 for choosing bidict, Tom Viner for being bidict's Adopt Pytest helper for the month, and Brianna Laugher for coordinating. - Daniel Pope, Leif Walsh, David Turner, and Michael Arntzenius for suggestions, code reviews, and design discussion. - Leif Walsh for contributing the initial `devcontainer `__ setup. - Jozef Knaperek for the bugfix. - Igor Nehoroshev for contributing the py.typed marker. - Bernát Gábor for pyproject.toml support. - Richard Sanger, Zeyi Wang, and Amol Sahsrabudhe for reporting bugs. Projects ======== - `Python `__ - `GitHub `__ - `Tidelift `__ - `Pytest `__ - `Hypothesis `__ - `Pytest-Benchmark `__ - `Coverage `__ - `Codecov `__ - `Sphinx `__ - `Readthedocs `__ - `mypy `__ - `ruff `__ - `Flake8 `__ - `pre-commit `__ bidict-0.22.1/mypy.ini000066400000000000000000000003201435416354600145370ustar00rootroot00000000000000[mypy] # Do not use `ignore_errors`; it doesn't apply # downstream and users have to deal with them. strict = True show_error_codes = True show_error_context = True show_column_numbers = True pretty = True bidict-0.22.1/pyproject.toml000066400000000000000000000050061435416354600157620ustar00rootroot00000000000000[project] name = "bidict" dynamic = ["version"] description = "The bidirectional mapping library for Python." authors = [{name="Joshua Bronson", email="jabronson@gmail.com"}] license = {text="MPL 2.0"} dependencies = [] requires-python = ">=3.7" readme = "README.rst" keywords = [ "bidict", "bimap", "bidirectional", "dict", "dictionary", "mapping", "collections", ] classifiers = [ "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed", ] [project.urls] Homepage = "https://bidict.readthedocs.io" Repository = "https://github.com/jab/bidict" Documentation = "https://bidict.readthedocs.io" Donate = "https://github.com/sponsors/jab" Changelog = "https://bidict.readthedocs.io/changelog.html" Chat = "https://gitter.im/jab/bidict" "Source Code" = "https://github.com/jab/bidict" "Issue Tracker" = "https://github.com/jab/bidict/issues" "Enterprise Support" = "https://bidict.readthedocs.io/#enterprise-support" [build-system] requires = ["setuptools >= 40.9.0"] build-backend = "setuptools.build_meta" [tool.setuptools] packages = ["bidict"] [project.optional-dependencies] # See dev-deps/*.txt for pip-compiled sets of these dependencies. lint = ["pre-commit"] docs = [ "sphinx", "sphinx-copybutton", "furo", ] test = [ "hypothesis", "pytest", "pytest-benchmark[histogram]", "pytest-cov", "pytest-xdist", "sortedcollections", "sortedcontainers", # run_tests.py depends on Sphinx to run doctests (see note in run_tests.py) "sphinx", ] [tool.ruff] # https://github.com/charliermarsh/ruff#configuration ignore = ["E501"] # line too long fix = true [tool.ruff.per-file-ignores] "docs/conf.py" = ["E402"] # import not at top of file # __init__.py collects functionality implemented in the rest of the package # for the sole purpose of exporting it under the `bidict` module namespace, # and does not understand mypy-style "import foo as foo" explicit re-exports, # so ignore F401 in this file ("imported but unused"). "bidict/__init__.py" = ["F401"] bidict-0.22.1/pytest.ini000066400000000000000000000015171435416354600151020ustar00rootroot00000000000000[pytest] # See comment about pytest-doctest below. #testpaths = bidict tests docs testpaths = bidict tests doctest_optionflags = ELLIPSIS addopts = --verbose --numprocesses=auto --benchmark-disable --doctest-modules --doctest-glob=tests/*.txt # pytest-doctest is missing too many features to use for the doctests in the Sphinx docs # (e.g. still no skipif support: https://github.com/thisch/pytest-sphinx/issues/9), # so we have our own ./run_tests.py that uses Sphinx's own support for running doctests. # Once we can move back to pytest for running the doctests in the Sphinx docs (which would # allow them to contribute to the pytest-cov(erage) report, besides letting us get rid of # run_tests.py), add "docs" back to "testpaths" above, and uncomment out the following two lines: # --doctest-glob=docs/*.rst # --ignore=docs/conf.py bidict-0.22.1/run_tests.py000077500000000000000000000015771435416354600154620ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # First run all tests that pytest discovers. """Run all tests.""" from functools import partial from pytest import main as pytest_main from sphinx.cmd.build import main as sphinx_main TEST_FUNCS = ( pytest_main, # pytest's doctest support doesn't support Sphinx extensions # (see https://www.sphinx-doc.org/en/latest/usage/extensions/doctest.html) # so test the code in the Sphinx docs using Sphinx's own doctest support. partial(sphinx_main, '-b doctest -d docs/_build/doctrees docs docs/_build/doctest'.split()), ) raise SystemExit(sum(bool(f()) for f in TEST_FUNCS)) # type: ignore bidict-0.22.1/setup.cfg000066400000000000000000000007131435416354600146670ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # https://packaging.python.org/guides/single-sourcing-package-version/#:~:text=%5Bmetadata%5D%0Aversion%20%3D%20attr%3A%20package.__version__ [metadata] version = attr: bidict.metadata.__version__ bidict-0.22.1/tests/000077500000000000000000000000001435416354600142075ustar00rootroot00000000000000bidict-0.22.1/tests/__init__.py000066400000000000000000000005001435416354600163130ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """See https://doc.pytest.org/en/latest/pythonpath.html""" bidict-0.22.1/tests/conftest.py000066400000000000000000000006201435416354600164040ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """Set up Hypothesis.""" from hypothesis import settings settings.register_profile('more-examples', max_examples=10_000, deadline=None) bidict-0.22.1/tests/property_tests/000077500000000000000000000000001435416354600173155ustar00rootroot00000000000000bidict-0.22.1/tests/property_tests/__init__.py000066400000000000000000000005001435416354600214210ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """See https://doc.pytest.org/en/latest/pythonpath.html""" bidict-0.22.1/tests/property_tests/_strategies.py000066400000000000000000000144121435416354600222020ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """Strategies for Hypothesis tests.""" from __future__ import annotations from collections import OrderedDict from operator import attrgetter, itemgetter, methodcaller import typing as t import hypothesis.strategies as st from bidict import DROP_NEW, DROP_OLD, RAISE, OnDup, OrderedBidictBase, namedbidict from . import _types as _t def one_of(items: t.Any) -> t.Any: """Create a one_of strategy using the given items.""" return st.one_of((st.just(i) for i in items)) # type: ignore [call-overload] DATA = st.data() BIDICT_TYPES = one_of(_t.BIDICT_TYPES) MUTABLE_BIDICT_TYPES = one_of(_t.MUTABLE_BIDICT_TYPES) FROZEN_BIDICT_TYPES = one_of(_t.FROZEN_BIDICT_TYPES) ORDERED_BIDICT_TYPES = one_of(_t.ORDERED_BIDICT_TYPES) REVERSIBLE_BIDICT_TYPES = one_of(_t.REVERSIBLE_BIDICT_TYPES) MAPPING_TYPES = one_of(_t.MAPPING_TYPES) NON_BI_MAPPING_TYPES = one_of(_t.NON_BI_MAPPING_TYPES) NON_NAMED_BIDICT_TYPES = one_of(_t.NON_NAMED_BIDICT_TYPES) ORDERED_MAPPING_TYPES = one_of(_t.ORDERED_MAPPING_TYPES) HASHABLE_MAPPING_TYPES = one_of(_t.HASHABLE_MAPPING_TYPES) ON_DUP_ACTIONS = one_of((DROP_NEW, DROP_OLD, RAISE)) ON_DUP = st.tuples(ON_DUP_ACTIONS, ON_DUP_ACTIONS, ON_DUP_ACTIONS).map(OnDup._make) BOOLEANS = st.booleans() # Combine a few different strategies together that generate atomic values # that can be used to initialize test bidicts with. Including only None, bools, and ints # provides enough coverage; including more just slows down example generation. ATOMS = st.none() | BOOLEANS | st.integers() PAIRS = st.tuples(ATOMS, ATOMS) NON_MAPPINGS = ATOMS | st.iterables(ATOMS) ALPHABET = tuple(chr(i) for i in range(0x10ffff) if chr(i).isidentifier()) VALID_NAMES = st.text(ALPHABET, min_size=1, max_size=16) DICTS_KW_PAIRS = st.dictionaries(VALID_NAMES, ATOMS) L_PAIRS = st.lists(PAIRS) I_PAIRS = st.iterables(PAIRS) FST_SND = (itemgetter(0), itemgetter(1)) L_PAIRS_NODUP = st.lists(PAIRS, unique_by=FST_SND) I_PAIRS_NODUP = st.iterables(PAIRS, unique_by=FST_SND) # Reserve a disjoint set of atoms as a source of values guaranteed not to have been # inserted into a test bidict already. DIFF_ATOMS = st.characters() DIFF_PAIRS = st.tuples(DIFF_ATOMS, DIFF_ATOMS) L_DIFF_PAIRS_NODUP = st.lists(DIFF_PAIRS, unique_by=FST_SND, min_size=1) DIFF_ITEMS = st.tuples(L_PAIRS_NODUP, L_DIFF_PAIRS_NODUP) RANDOMS = st.randoms(use_true_random=False) SAME_ITEMS_DIFF_ORDER = st.tuples( st.lists(PAIRS, unique_by=FST_SND, min_size=2), RANDOMS ).map( lambda i: (i[0], i[1].sample(i[0], len(i[0]))) # (seq, shuffled seq) ).filter(lambda i: i[0] != i[1]) def _bidict_strat(bi_types: t.Any, init_items: t.Any = I_PAIRS_NODUP, _inv: t.Any = attrgetter('inverse')) -> t.Any: fwd_bidicts = st.tuples(bi_types, init_items).map(lambda i: i[0](i[1])) # type: ignore inv_bidicts = fwd_bidicts.map(_inv) return fwd_bidicts | inv_bidicts BIDICTS = _bidict_strat(BIDICT_TYPES) FROZEN_BIDICTS = _bidict_strat(FROZEN_BIDICT_TYPES) MUTABLE_BIDICTS = _bidict_strat(MUTABLE_BIDICT_TYPES) ORDERED_BIDICTS = _bidict_strat(ORDERED_BIDICT_TYPES) callkeys, callitems = methodcaller('keys'), methodcaller('items') KEYSVIEW_SET_OP_ARGS = st.sets(ATOMS) | st.dictionaries(ATOMS, ATOMS).map(callkeys) | BIDICTS.map(callkeys) ITEMSVIEW_SET_OP_ARGS = st.sets(PAIRS) | st.dictionaries(ATOMS, ATOMS).map(callitems) | BIDICTS.map(callitems) NON_BI_MAPPINGS = st.tuples(NON_BI_MAPPING_TYPES, L_PAIRS).map(lambda i: i[0](i[1])) # type: ignore NAMEDBIDICT_NAMES_ALL_VALID = st.lists(VALID_NAMES, min_size=3, max_size=3, unique=True) NAMEDBIDICT_NAMES_SOME_INVALID = st.lists(st.text(min_size=1), min_size=3, max_size=3).filter( lambda i: not all(str.isidentifier(name) for name in i) ) NAMEDBIDICT_TYPES = st.tuples(NAMEDBIDICT_NAMES_ALL_VALID, NON_NAMED_BIDICT_TYPES).map( lambda i: namedbidict(*i[0], base_type=i[1]) ) NAMEDBIDICTS = _bidict_strat(NAMEDBIDICT_TYPES) def _bi_and_map(bi_types: t.Any, map_types: t.Any = MAPPING_TYPES, init_items: t.Any = L_PAIRS_NODUP) -> t.Any: """Given bidict types and mapping types, return a pair of each type created from init_items.""" return st.tuples(bi_types, map_types, init_items).map( lambda i: (i[0](i[2]), i[1](i[2])) ) BI_AND_MAP_FROM_SAME_ND_ITEMS = _bi_and_map(BIDICT_TYPES) # Update the following when we drop support for Python < 3.8. On 3.8+, all mappings are reversible. RBI_AND_RMAP_FROM_SAME_ND_ITEMS = _bi_and_map(REVERSIBLE_BIDICT_TYPES, st.just(OrderedDict)) HBI_AND_HMAP_FROM_SAME_ND_ITEMS = _bi_and_map(FROZEN_BIDICT_TYPES, HASHABLE_MAPPING_TYPES) _unpack = lambda i: (i[0](i[2][0]), i[1](i[2][1])) # noqa: E731 BI_AND_MAP_FROM_DIFF_ITEMS = st.tuples(BIDICT_TYPES, MAPPING_TYPES, DIFF_ITEMS).map(_unpack) OBI_AND_OMAP_FROM_SAME_ITEMS_DIFF_ORDER = st.tuples( ORDERED_BIDICT_TYPES, ORDERED_MAPPING_TYPES, SAME_ITEMS_DIFF_ORDER ).map(_unpack) _cmpdict: t.Any = lambda i: (OrderedDict if isinstance(i, OrderedBidictBase) else dict) # noqa: E731 BI_AND_CMPDICT_FROM_SAME_ITEMS = L_PAIRS_NODUP.map( lambda items: (lambda b: (b, _cmpdict(b)(items)))(_bidict_strat(BIDICT_TYPES, items)) ) ARGS_ATOM = st.tuples(ATOMS) ARGS_ITERPAIRS = st.tuples(I_PAIRS) ARGS_ATOM_ATOM = st.tuples(ATOMS, ATOMS) METHOD_ARGS_PAIRS = ( # 0-arity methods -> no need to generate any args: ('clear', None), ('popitem', None), ('__copy__', None), ('__iter__', None), ('__len__', None), ('copy', None), ('keys', None), ('items', None), ('values', None), # 1-arity methods that take an atom: ('__contains__', ARGS_ATOM), ('__getitem__', ARGS_ATOM), ('__delitem__', ARGS_ATOM), ('get', ARGS_ATOM), ('pop', ARGS_ATOM), ('setdefault', ARGS_ATOM), ('move_to_end', ARGS_ATOM), # 1-arity methods that take an iterable of pairs: ('update', ARGS_ITERPAIRS), ('forceupdate', ARGS_ITERPAIRS), # 2-arity methods that take two atoms: ('__setitem__', ARGS_ATOM_ATOM), ('setdefault', ARGS_ATOM_ATOM), ('pop', ARGS_ATOM_ATOM), ('put', ARGS_ATOM_ATOM), ('forceput', ARGS_ATOM_ATOM), # Other ('popitem', st.tuples(BOOLEANS)), ('move_to_end', st.tuples(ATOMS, BOOLEANS)), ) bidict-0.22.1/tests/property_tests/_types.py000066400000000000000000000065611435416354600212020ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """Types for Hypothoses tests.""" from __future__ import annotations from collections import OrderedDict, UserDict import typing as t from bidict import BidictBase, FrozenOrderedBidict, OrderedBidict, bidict, frozenbidict, namedbidict from bidict._typing import TypeAlias BiTypesT: TypeAlias = t.Tuple[t.Type[BidictBase[t.Any, t.Any]], ...] class UserBidict(bidict[t.Any, t.Any]): """Custom bidict subclass.""" _fwdm_cls = UserDict _invm_cls = UserDict class UserOrderedBidict(OrderedBidict[t.Any, t.Any]): """Custom OrderedBidict subclass.""" _fwdm_cls = UserDict _invm_cls = UserDict class UserBidictNotOwnInverse(bidict[t.Any, t.Any]): """Custom bidict subclass that is not its own inverse.""" _fwdm_cls = dict _invm_cls = UserDict UserBidictNotOwnInverseInv = UserBidictNotOwnInverse._inv_cls assert UserBidictNotOwnInverseInv is not UserBidictNotOwnInverse class UserBidictNotOwnInverse2(UserBidictNotOwnInverse): """Another custom bidict subclass that is not its own inverse.""" NamedBidict = namedbidict('NamedBidict', 'key', 'val', base_type=bidict) NamedFrozenBidict = namedbidict('NamedFrozenBidict', 'key', 'val', base_type=frozenbidict) NamedOrderedBidict = namedbidict('NamedOrderedBidict', 'key', 'val', base_type=OrderedBidict) NamedUserBidict = namedbidict('NamedUserBidict', 'key', 'val', base_type=UserBidict) NAMED_BIDICT_TYPES: BiTypesT = (NamedBidict, NamedFrozenBidict, NamedOrderedBidict, NamedUserBidict) MUTABLE_BIDICT_TYPES: BiTypesT = (bidict, OrderedBidict, NamedBidict, UserBidict, UserOrderedBidict, UserBidictNotOwnInverse) FROZEN_BIDICT_TYPES: BiTypesT = (frozenbidict, FrozenOrderedBidict, NamedFrozenBidict) ORDERED_BIDICT_TYPES: BiTypesT = (OrderedBidict, FrozenOrderedBidict, NamedOrderedBidict, UserOrderedBidict) ORDER_PRESERVING_BIDICT_TYPES: BiTypesT = tuple(set(FROZEN_BIDICT_TYPES + ORDERED_BIDICT_TYPES)) BIDICT_TYPES: BiTypesT = tuple(set(MUTABLE_BIDICT_TYPES + FROZEN_BIDICT_TYPES + ORDERED_BIDICT_TYPES)) NON_NAMED_BIDICT_TYPES: BiTypesT = tuple(set(BIDICT_TYPES) - set(NAMED_BIDICT_TYPES)) # When support is dropped for Python < 3.8, all bidict types will be reversible, # and we can remove the following and just use BIDICT_TYPES instead: REVERSIBLE_BIDICT_TYPES: BiTypesT = tuple(b for b in BIDICT_TYPES if issubclass(b, t.Reversible)) BIDICT_TYPE_WHOSE_MODULE_HAS_REF_TO_INV_CLS = UserBidictNotOwnInverse BIDICT_TYPE_WHOSE_MODULE_HAS_NO_REF_TO_INV_CLS = UserBidictNotOwnInverse2 class _FrozenMap(t.Mapping[t.Any, t.Any]): def __init__(self, *args: t.Any, **kw: t.Any) -> None: self._mapping = dict(*args, **kw) def __iter__(self) -> t.Iterator[t.Any]: return iter(self._mapping) def __len__(self) -> int: return len(self._mapping) def __getitem__(self, key: t.Any) -> t.Any: return self._mapping[key] def __hash__(self) -> int: return t.ItemsView(self._mapping)._hash() NON_BI_MAPPING_TYPES = (dict, OrderedDict, _FrozenMap) MAPPING_TYPES = BIDICT_TYPES + NON_BI_MAPPING_TYPES ORDERED_MAPPING_TYPES = ORDERED_BIDICT_TYPES + (OrderedDict,) HASHABLE_MAPPING_TYPES = FROZEN_BIDICT_TYPES + (_FrozenMap,) bidict-0.22.1/tests/property_tests/test_properties.py000066400000000000000000000633331435416354600231320ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """Property-based tests using https://hypothesis.readthedocs.io.""" from __future__ import annotations from copy import deepcopy from collections import OrderedDict, UserDict from collections.abc import Iterable, KeysView, ValuesView, ItemsView from itertools import tee from unittest.mock import ANY import gc import operator as op import pickle import sys import typing as t import weakref import pytest from hypothesis import assume, example, given from bidict import ( DROP_NEW, DROP_OLD, RAISE, OnDup, BidirectionalMapping, MutableBidirectionalMapping, BidictBase, MutableBidict, OrderedBidictBase, OrderedBidict, bidict, namedbidict, inverted, DuplicationError, KeyDuplicationError, ValueDuplicationError, KeyAndValueDuplicationError, ) from bidict._iter import iteritems from bidict._typing import Items, TypeAlias from . import _strategies as st from ._types import ( ORDER_PRESERVING_BIDICT_TYPES, BIDICT_TYPE_WHOSE_MODULE_HAS_REF_TO_INV_CLS, BIDICT_TYPE_WHOSE_MODULE_HAS_NO_REF_TO_INV_CLS, ) require_cpython_refcounting = pytest.mark.skipif( sys.implementation.name != 'cpython', reason='Requires CPython refcounting behavior', ) Bi: TypeAlias = BidictBase[t.Any, t.Any] MBi: TypeAlias = MutableBidict[t.Any, t.Any] OBi: TypeAlias = OrderedBidictBase[t.Any, t.Any] @given(st.BIDICTS, st.NON_MAPPINGS) def test_unequal_to_non_mapping(bi: Bi, not_a_mapping: t.Any) -> None: """Bidicts and their inverses should compare unequal to bools, ints, and other typical non-mappings.""" assert bi != not_a_mapping assert bi.inv != not_a_mapping assert not bi == not_a_mapping assert not bi.inv == not_a_mapping @given(st.BIDICTS) def test_eq_correctly_defers_to_eq_of_non_mapping(bi: Bi) -> None: """Bidicts' __eq__ does not defeat non-mapping objects' __eq__, when implemented.""" assert bi == ANY assert ANY == bi @given(st.BI_AND_MAP_FROM_DIFF_ITEMS) def test_unequal_to_mapping_with_different_items(bi_and_map_from_diff_items: t.Any) -> None: """Bidicts should be unequal to mappings containing different items.""" bi, mapping = bi_and_map_from_diff_items assert bi != mapping assert not bi == mapping @given(st.BI_AND_MAP_FROM_SAME_ND_ITEMS) def test_equal_to_mapping_with_same_items(bi_and_map_from_same_items: t.Any) -> None: """Bidicts should be equal to mappings created from the same non-duplicating items. The bidict's inverse and the mapping's inverse should also be equal. """ bi, mapping = bi_and_map_from_same_items assert bi == mapping assert mapping == bi assert not bi != mapping assert not mapping != bi mapping_inv = OrderedDict((v, k) for (k, v) in mapping.items()) assert bi.inv == mapping_inv assert mapping_inv == bi.inv assert not bi.inv != mapping_inv assert not mapping_inv != bi.inv @given(st.HBI_AND_HMAP_FROM_SAME_ND_ITEMS) def test_equal_hashables_have_same_hash(hashable_bidict_and_mapping: t.Any) -> None: """Hashable bidicts and hashable mappings that are equal should hash to the same value.""" bi, mapping = hashable_bidict_and_mapping assert bi == mapping assert mapping == bi assert hash(bi) == hash(mapping) @given(st.BIDICTS, st.NON_BI_MAPPINGS) @example(OrderedBidict([(1, 1), (2, 2)]), OrderedDict([(1, 1), (2, 2)])) @example(OrderedBidict([(1, 1), (2, 2)]), OrderedDict([(2, 2), (1, 1)])) @example(OrderedBidict({None: None}), {False: None, None: None}) def test_equals_matches_equals_order_sensitive(bi: Bi, mapping: t.Mapping[t.Any, t.Any]) -> None: """Bidict equals_order_sensitive should agree with __eq__.""" mapping_inv = OrderedDict((v, k) for (k, v) in mapping.items()) if bi.equals_order_sensitive(mapping): assert bi == mapping assert mapping == bi assert list(bi.inv.items()) == list(mapping_inv.items()) else: assert list(bi.items()) != list(mapping.items()) if bi == mapping: assert mapping == bi assert bi.items() == mapping.items() # should use (unordered) set comparison assert bi.inv.items() == mapping_inv.items() # ditto assert mapping_inv.items() == bi.inv.items() # ditto else: assert mapping != bi assert bi.items() != mapping.items() assert bi.inv.items() != mapping_inv.items() or len(mapping_inv) != len(mapping) assert mapping_inv.items() != bi.inv.items() or len(mapping_inv) != len(mapping) @given(st.BI_AND_MAP_FROM_SAME_ND_ITEMS) def test_equals_order_sensitive_same_items(bi_and_map_from_same_items: t.Any) -> None: """Bidicts should be order-sensitive-equal to mappings with the same items in the same order. The bidict's inverse and the ordered mapping's inverse should also be order-sensitive-equal. """ bi, mapping = bi_and_map_from_same_items assert bi.equals_order_sensitive(mapping) mapping_inv = OrderedDict((v, k) for (k, v) in mapping.items()) assert bi.inv.equals_order_sensitive(mapping_inv) @given(st.OBI_AND_OMAP_FROM_SAME_ITEMS_DIFF_ORDER) def test_unequal_order_sensitive_same_items_different_order(ob_and_om: t.Any) -> None: """Ordered bidicts should be order-sensitive-unequal to ordered mappings of diff-ordered items. Where both were created from the same items where no key or value was duplicated, but the items were ordered differently. The bidict's inverse and the ordered mapping's inverse should also be order-sensitive-unequal. """ ob, om = ob_and_om assert not ob.equals_order_sensitive(om) om_inv = OrderedDict((v, k) for (k, v) in om.items()) assert not ob.inv.equals_order_sensitive(om_inv) @given(st.ORDERED_BIDICTS, st.NON_MAPPINGS) def test_unequal_order_sensitive_non_mapping(ob: OBi, not_a_mapping: t.Any) -> None: """Ordered bidicts should be order-sensitive-unequal to ordered mappings of diff-ordered items. Where both were created from the same items where no key or value was duplicated, but the items were ordered differently. The bidict's inverse and the ordered mapping's inverse should also be order-sensitive-unequal. """ assert not ob.equals_order_sensitive(not_a_mapping) assert not ob.inv.equals_order_sensitive(not_a_mapping) @given(st.BIDICTS, st.NON_BI_MAPPINGS) def test_merge_operators(bi: Bi, mapping: t.Mapping[t.Any, t.Any]) -> None: """PEP 584-style dict merge operators should work as expected.""" try: merged = bi | mapping except DuplicationError as exc: with pytest.raises(exc.__class__): bidict(bi).update(mapping) with pytest.raises(exc.__class__): bi |= mapping else: assert merged == bidict({**bi, **mapping}) tmp = bidict(bi) tmp |= mapping # type: ignore assert merged == tmp try: merged = mapping | bi except DuplicationError as exc: with pytest.raises(exc.__class__): bidict(mapping).update(bi) else: assert merged == bidict({**mapping, **bi}) mapping |= bi assert merged == mapping @given(st.MUTABLE_BIDICTS, st.DIFF_ATOMS, st.RANDOMS) def test_setitem_with_dup_val_raises(bi: MBi, new_key: t.Any, rand: t.Any) -> None: """Setting an item whose value duplicates that of an existing item should raise ValueDuplicationError.""" ln = len(bi) assume(ln > 2) for b in (bi, bi.inv): existing_val = rand.choice(list(b.inv)) with pytest.raises(ValueDuplicationError): b[new_key] = existing_val # type: ignore assert len(b) == len(b.inv) == ln @given(st.MUTABLE_BIDICTS, st.RANDOMS) def test_setitem_with_dup_key_val_raises(bi: MBi, rand: t.Any) -> None: """Setting an item whose key and val duplicate two different existing items raises KeyAndValueDuplicationError.""" ln = len(bi) assume(ln > 2) for b in (bi, bi.inv): existing_items = rand.sample(list(b.items()), 2) existing_key = existing_items[0][0] existing_val = existing_items[1][1] with pytest.raises(KeyAndValueDuplicationError): b[existing_key] = existing_val # type: ignore assert len(b) == len(b.inv) == ln @given(st.MUTABLE_BIDICTS, st.DIFF_ATOMS, st.RANDOMS) def test_put_with_dup_key_raises(bi: MBi, new_val: t.Any, rand: t.Any) -> None: """Putting an item whose key duplicates that of an existing item should raise KeyDuplicationError.""" ln = len(bi) assume(ln > 2) for b in (bi, bi.inv): existing_key = rand.choice(list(b)) with pytest.raises(KeyDuplicationError): b.put(existing_key, new_val) # type: ignore assert len(b) == len(b.inv) == ln @given(st.BIDICTS) def test_bijectivity(bi: Bi) -> None: """b[k] == v <==> b.inv[v] == k""" for b in (bi, bi.inv): assert all(b.inv[v] == k for (k, v) in b.items()) @given(st.MUTABLE_BIDICTS) def test_cleared_bidicts_have_no_items(bi: MBi) -> None: """A cleared bidict should contain no items.""" bi.clear() assert not bi assert len(bi) == 0 sntl = object() assert next(iter(bi), sntl) is sntl @given(st.BI_AND_CMPDICT_FROM_SAME_ITEMS, st.DATA) def test_consistency_after_method_call(bi_and_cmp_dict: t.Any, data: t.Any) -> None: """A bidict should be left in a consistent state after calling any method, even if it raises.""" bi_orig, cmp_dict_orig = bi_and_cmp_dict for methodname, args_strat in st.METHOD_ARGS_PAIRS: if not hasattr(bi_orig, methodname): continue bi = bi_orig.copy() collect = list if isinstance(bi, ORDER_PRESERVING_BIDICT_TYPES) else set method = getattr(bi, methodname) args = data.draw(args_strat) if args_strat is not None else () try: result = method(*args) except (KeyError, TypeError, DuplicationError) as exc: if isinstance(exc, TypeError): assert methodname == 'popitem', 'popitem should be the only method that can raise TypeError here (we sometimes pass in the wrong number of args)' # Call should fail clean, i.e. bi should be in the same state it was before the call. assertmsg = f'{method!r} did not fail clean: {exc!r}' assert bi == bi_orig, assertmsg assert bi.inv == bi_orig.inv, assertmsg assert collect(bi.keys()) == collect(bi_orig.keys()), assertmsg assert collect(bi.values()) == collect(bi_orig.values()), assertmsg assert collect(bi.items()) == collect(bi_orig.items()), assertmsg assert collect(reversed(bi.keys())) == collect(reversed(bi_orig.keys())), assertmsg assert collect(reversed(bi.values())) == collect(reversed(bi_orig.values())), assertmsg assert collect(reversed(bi.items())) == collect(reversed(bi_orig.items())), assertmsg else: # Should get the same result as calling the same method on the compare-to dict. cmp_dict = cmp_dict_orig.copy() cmp_dict_meth = getattr(cmp_dict, methodname, None) if cmp_dict_meth: cmp_result = cmp_dict_meth(*args) if isinstance(cmp_result, Iterable): result = collect(result) cmp_result = collect(cmp_result) assert result == cmp_result, f'methodname={methodname} args={args!r}' # Whether the call failed or succeeded, bi should pass consistency checks. keys = collect(bi.keys()) assert keys == collect(bi) vals = collect(bi.values()) assert vals == collect(bi[k] for k in bi) items = collect(bi.items()) assert items == collect((k, bi[k]) for k in bi) assert collect(bi.inv.keys()) == collect(bi.inv) == vals assert collect(bi.inv.values()) == collect(bi.inv[k] for k in bi.inv) == keys assert collect(bi.inv.items()) == collect((k, bi.inv[k]) for k in bi.inv) if not getattr(bi.keys(), '__reversed__', None): # Python < 3.8 return assert collect(reversed(bi.keys())) == collect(reversed(bi.inv.values())) assert collect(reversed(bi.values())) == collect(reversed(bi.inv.keys())) assert collect(reversed(bi.items())) == collect((k, v) for (v, k) in reversed(bi.inv.items())) @given(st.MUTABLE_BIDICTS, st.L_PAIRS, st.ON_DUP) # These test cases ensure coverage of all branches in [Ordered]BidictBase._undo_write # (Hypothesis doesn't always generate examples that cover all the branches otherwise). @example(bidict({1: 1, 2: 2}), [(1, 3), (1, 2)], OnDup(DROP_OLD, RAISE)) @example(bidict({1: 1, 2: 2}), [(3, 1), (2, 4)], OnDup(RAISE, DROP_OLD)) @example(bidict({1: 1, 2: 2}), [(1, 2), (1, 1)], OnDup(RAISE, RAISE, DROP_OLD)) @example(OrderedBidict({1: 1, 2: 2}), [(1, 3), (1, 2)], OnDup(DROP_OLD, RAISE)) @example(OrderedBidict({1: 1, 2: 2}), [(3, 1), (2, 4)], OnDup(RAISE, DROP_OLD)) @example(OrderedBidict({1: 1, 2: 2}), [(1, 2), (1, 1)], OnDup(RAISE, RAISE, DROP_OLD)) @example(OrderedBidict(), [(1, 1), (2, 2), (1, 2), (1, 1), (2, 1)], OnDup(DROP_OLD, RAISE, DROP_OLD)) @example(OrderedBidict(), [(1, 2), (2, 1), (1, 1), (1, 2)], OnDup(RAISE, DROP_NEW, DROP_OLD)) @example(OrderedBidict(), [(1, 1), (2, 1), (1, 1)], OnDup(DROP_NEW, DROP_OLD, DROP_NEW)) def test_putall_same_as_put_for_each_item(bi: MBi, items: Items[t.Any, t.Any], on_dup: OnDup) -> None: """*bi.putall(items) <==> for i in items: bi.put(i)* for all values of OnDup.""" check = bi.copy() expect = bi.copy() checkexc = None expectexc = None for (key, val) in items: try: expect.put(key, val, on_dup) except DuplicationError as exc: expectexc = type(exc) expect = bi # Bulk updates fail clean -> roll back to original state. break try: check.putall(items, on_dup) except DuplicationError as exc: checkexc = type(exc) assert checkexc == expectexc assert check == expect assert check.inv == expect.inv @given(st.BI_AND_MAP_FROM_SAME_ND_ITEMS) def test_bidict_iter(bi_and_mapping: t.Any) -> None: """iter(bi) should yield the keys in a bidict in insertion order.""" bi, mapping = bi_and_mapping assert all(i == j for (i, j) in zip(bi, mapping)) @given(st.RBI_AND_RMAP_FROM_SAME_ND_ITEMS) def test_bidict_reversed(rb_and_rd: t.Any) -> None: """reversed(bi) should yield the keys in a bidict in reverse insertion order.""" rb, rd = rb_and_rd assert all(i == j for (i, j) in zip(reversed(rb), reversed(rd))) @given(st.FROZEN_BIDICTS) def test_frozenbidicts_hashable(bi: Bi) -> None: """Frozen bidicts can be hashed and inserted into sets and mappings.""" assert hash(bi) assert {bi} assert {bi: bi} @given(st.NAMEDBIDICT_NAMES_SOME_INVALID) def test_namedbidict_raises_on_invalid_name(names: tuple[str, str, str]) -> None: """:func:`bidict.namedbidict` should raise if given invalid names.""" typename, keyname, valname = names with pytest.raises(ValueError): namedbidict(typename, keyname, valname) @given(st.NAMEDBIDICT_NAMES_ALL_VALID) def test_namedbidict_raises_on_same_keyname_as_valname(names: tuple[str, str, str]) -> None: """:func:`bidict.namedbidict` should raise if given same keyname as valname.""" typename, keyname, _ = names with pytest.raises(ValueError): namedbidict(typename, keyname, keyname) @given(st.NAMEDBIDICT_NAMES_ALL_VALID, st.NON_BI_MAPPING_TYPES) def test_namedbidict_raises_on_invalid_base_type(names: tuple[str, str, str], invalid_base_type: t.Any) -> None: """:func:`bidict.namedbidict` should raise if given a non-bidict base_type.""" with pytest.raises(TypeError): namedbidict(*names, base_type=invalid_base_type) @given(st.NAMEDBIDICTS) def test_namedbidict(nb: t.Any) -> None: """Test :func:`bidict.namedbidict` custom accessors.""" valfor = getattr(nb, nb.valname + '_for') keyfor = getattr(nb, nb.keyname + '_for') assert all(valfor[key] == val for (key, val) in nb.items()) assert all(keyfor[val] == key for (key, val) in nb.items()) # The same custom accessors should work on the inverse. inv = nb.inv valfor = getattr(inv, nb.valname + '_for') keyfor = getattr(inv, nb.keyname + '_for') assert all(valfor[key] == val for (key, val) in nb.items()) assert all(keyfor[val] == key for (key, val) in nb.items()) @require_cpython_refcounting @given(st.BIDICT_TYPES) def test_bidicts_freed_on_zero_refcount(bi_cls: t.Type[Bi]) -> None: """On CPython, the moment you have no more (strong) references to a bidict, there are no remaining (internal) strong references to it (i.e. no reference cycle was created between it and its inverse), allowing the memory to be reclaimed immediately, even with GC disabled. """ gc.disable() try: bi = bi_cls() weak = weakref.ref(bi) assert weak() is not None del bi assert weak() is None finally: gc.enable() @require_cpython_refcounting @given(st.ORDERED_BIDICTS) def test_orderedbidict_nodes_freed_on_zero_refcount(ob: OBi) -> None: """On CPython, the moment you have no more references to an ordered bidict, the refcount of each of its internal nodes drops to 0 (i.e. the linked list of nodes does not create a reference cycle), allowing the memory to be reclaimed immediately. """ gc.disable() try: # Make a local copy of the bidict passed in by hypothesis # so that its refcount actually drops to 0 when we del it below. ob = ob.copy() nodes = weakref.WeakSet(ob._sntl.iternodes()) assert len(nodes) == len(ob) del ob assert len(nodes) == 0 finally: gc.enable() @given(st.ORDERED_BIDICTS) def test_orderedbidict_nodes_consistent(ob: OBi) -> None: """The nodes in an ordered bidict's backing linked list should be the same as those in its backing mapping.""" mapnodes = set(ob._node_by_korv.inverse) listnodes = set(ob._sntl.iternodes()) assert mapnodes == listnodes def test_abc_slots() -> None: """Bidict ABCs should define __slots__. Ref: https://docs.python.org/3/reference/datamodel.html#notes-on-using-slots """ assert BidirectionalMapping.__dict__['__slots__'] == () assert MutableBidirectionalMapping.__dict__['__slots__'] == () @given(st.BIDICTS) def test_inv_aliases_inverse(bi: Bi) -> None: """bi.inv should alias bi.inverse.""" assert bi.inverse is bi.inv assert bi.inv.inverse is bi.inverse.inv @given(st.BIDICTS) def test_inverse_readonly(bi: Bi) -> None: """Attempting to set the .inverse attribute should raise AttributeError.""" with pytest.raises(AttributeError): bi.inverse = bi.__class__(inverted(bi)) # type: ignore with pytest.raises(AttributeError): bi.inv = bi.__class__(inverted(bi)) # type: ignore @given(st.BIDICTS) @example(BIDICT_TYPE_WHOSE_MODULE_HAS_REF_TO_INV_CLS({1: 'one'}).inverse) @example(BIDICT_TYPE_WHOSE_MODULE_HAS_NO_REF_TO_INV_CLS({1: 'one'}).inverse) def test_pickle(bi: Bi) -> None: """All bidicts should work with pickle.""" pickled = pickle.dumps(bi) roundtripped = pickle.loads(pickled) assert roundtripped is roundtripped.inv.inv assert roundtripped.equals_order_sensitive(bi) assert roundtripped.inv.equals_order_sensitive(bi.inv) assert roundtripped.inv.inv.equals_order_sensitive(bi.inv.inv) assert dict(roundtripped) == dict(bi) roundtripped_inv = pickle.loads(pickle.dumps(bi.inv)) assert roundtripped_inv.equals_order_sensitive(bi.inv) assert roundtripped_inv.inv.equals_order_sensitive(bi) assert roundtripped_inv.inv.inv.equals_order_sensitive(bi.inv) assert dict(roundtripped_inv) == dict(bi.inv) def test_pickle_orderedbi_whose_order_disagrees_w_fwdm() -> None: """An OrderedBidict whose order does not match its _fwdm's should pickle with the correct order.""" ob = OrderedBidict({0: 1, 2: 3}) # First get ob._fwdm's order to disagree with ob's, and confirm: ob.inverse[1] = 4 assert next(iter(ob.items())) == (4, 1) assert next(iter(ob.inverse.items())) == (1, 4) assert next(iter(ob._fwdm.items())) == (2, 3) # Now check that its order is preserved after pickling and unpickling: roundtripped = pickle.loads(pickle.dumps(ob)) assert roundtripped.equals_order_sensitive(ob) class _UserBidict(bidict[t.Any, t.Any]): """See :func:`test_pickle_dynamically_generated_inverse_bidict` below.""" _invm_cls = UserDict def test_pickle_dynamically_generated_inverse_bidict() -> None: """Even instances of dynamically-generated inverse bidict classes should be pickleable.""" # The @example(BIDICT_TYPE_WHOSE_MODULE_HAS_NO_REF_TO_INV_CLS...) in test_pickle above # covers this, but this is an even more explicit test for clarity. # First pickle a non-inverse instance (whose class we have a direct reference to). ub = _UserBidict(one=1, two=2) roundtripped = pickle.loads(pickle.dumps(ub)) assert roundtripped == ub == _UserBidict({'one': 1, 'two': 2}) assert dict(roundtripped) == dict(ub) # Now for the inverse: assert repr(ub.inverse) == "_UserBidictInv({1: 'one', 2: 'two'})" # We can still pickle the inverse, even though its class, _UserBidictInv, was # dynamically generated, and we didn't save a reference to it named "_UserBidictInv" # anywhere that pickle could find it in sys.modules: ubinv = pickle.loads(pickle.dumps(ub.inverse)) assert repr(ubinv) == "_UserBidictInv({1: 'one', 2: 'two'})" assert ub._inv_cls.__name__ not in (name for m in sys.modules for name in dir(m)) @given(st.BIDICTS) def test_copy(bi: Bi) -> None: """A bidict should equal its copy.""" cp = bi.copy() assert cp is not bi assert cp.inv is not bi.inv assert bi == cp assert bi.inv == cp.inv collect = list if isinstance(bi, ORDER_PRESERVING_BIDICT_TYPES) else set assert collect(bi.items()) == collect(cp.items()) assert collect(bi.inv.items()) == collect(cp.inv.items()) @given(st.BIDICTS) def test_deepcopy(bi: Bi) -> None: """A bidict should equal its deepcopy.""" cp = deepcopy(bi) assert cp is not bi assert cp.inv is not bi.inv assert cp.inv.inv is cp assert cp.inv.inv is not bi assert bi == cp assert bi.inv == cp.inv collect = list if isinstance(bi, ORDER_PRESERVING_BIDICT_TYPES) else set assert collect(bi.items()) == collect(cp.items()) assert collect(bi.inv.items()) == collect(cp.inv.items()) def test_iteritems_raises_on_too_many_args() -> None: """:func:`iteritems` should raise if given too many arguments.""" with pytest.raises(TypeError): iteritems('too', 'many', 'args') # type: ignore @given(st.I_PAIRS, st.DICTS_KW_PAIRS) def test_iteritems(arg0: t.Any, kw: t.Any) -> None: """:func:`iteritems` should work correctly.""" arg0_1, arg0_2 = tee(arg0) it = iteritems(arg0_1, **kw) # Consume the first `len(arg0)` pairs, checking that they match `arg0`. assert all(check == expect for (check, expect) in zip(it, arg0_2)) with pytest.raises(StopIteration): next(arg0_1) # Iterating `it` should have consumed all of `arg0_1`. # Consume the remaining pairs, checking that they match `kw`. # Once min PY version required is higher, can check that the order matches `kw` too. assert all(kw[k] == v for (k, v) in it) with pytest.raises(StopIteration): next(it) @given(st.L_PAIRS) def test_inverted_pairs(pairs: t.Any) -> None: """:func:`bidict.inverted` should yield the inverses of a list of pairs.""" inv = [(v, k) for (k, v) in pairs] assert list(inverted(pairs)) == inv assert list(inverted(inverted(pairs))) == pairs @given(st.BI_AND_MAP_FROM_SAME_ND_ITEMS) def test_inverted_bidict(bi_and_mapping: t.Any) -> None: """:func:`bidict.inverted` should yield the inverse items of an ordered bidict.""" bi, mapping = bi_and_mapping mapping_inv = {v: k for (k, v) in mapping.items()} assert all(i == j for (i, j) in zip(inverted(bi), mapping_inv.items())) assert all(i == j for (i, j) in zip(inverted(inverted(bi)), mapping.items())) _SET_OPS: t.Iterable[t.Callable[[t.Any, t.Any], t.Any]] = ( op.le, op.lt, op.gt, op.ge, op.eq, op.ne, op.and_, op.or_, op.sub, op.xor, (lambda x, y: x.isdisjoint(y)), ) @given(st.BIDICTS, st.DATA) def test_views(bi: t.Any, data: t.Any) -> None: """Optimized view APIs should be equivalent to using the corresponding MappingViews from :mod:`collections.abc`.""" for check, oracle in (bi.keys(), KeysView(bi)), (bi.values(), ValuesView(bi)), (bi.items(), ItemsView(bi)): assert isinstance(oracle, t.Iterable) and isinstance(oracle, t.Container) # appease mypy # 0-arity methods: __len__, __iter__ assert check.__len__() == oracle.__len__() assert list(check.__iter__()) == list(oracle.__iter__()) # 1-arity methods: __contains__ arg = data.draw(st.PAIRS if isinstance(oracle, ItemsView) else st.ATOMS) assert check.__contains__(arg) == oracle.__contains__(arg) # Methods of set-like views if isinstance(oracle, ValuesView): continue arg = data.draw(st.KEYSVIEW_SET_OP_ARGS if isinstance(oracle, KeysView) else st.ITEMSVIEW_SET_OP_ARGS) for so in _SET_OPS: try: expect = so(oracle, arg) except TypeError: with pytest.raises(TypeError): so(check, arg) else: check_ = so(check, arg) assert check_ == expect, (check, so, arg) try: expect = so(arg, oracle) except TypeError: with pytest.raises(TypeError): so(arg, check) else: check_ = so(arg, check) assert check_ == expect, (check, so, arg) bidict-0.22.1/tests/test_bidict.txt000066400000000000000000000172571435416354600172610ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. Test script for bidict.bidict: >>> from bidict import bidict >>> bi = bidict({1: 'one', 2: 'two', 3: 'three'}) >>> bi bidict({1: 'one', 2: 'two', 3: 'three'}) Works like dict for getting and changing forward mappings: >>> bi[2] 'two' >>> bi[2] = 'twain' >>> bi[2] 'twain' >>> bi[4] Traceback (most recent call last): ... KeyError: 4 >>> del bi[2] >>> bi.pop(3) 'three' >>> bi.update({4: 'four'}) >>> bi.popitem() (4, 'four') >>> bi bidict({1: 'one'}) ``put`` can also be used to insert a mapping as long as its key and value don't already exist: >>> bi.put(0, 'zero') >>> bi[0] 'zero' >>> bi.put(1, 'aught') Traceback (most recent call last): ... bidict.KeyDuplicationError: 1 >>> del bi[1] >>> bi.put(1, 'aught') >>> bi[1] 'aught' >>> del bi[0] >>> bi bidict({1: 'aught'}) bidicts maintain references to their inverses via the ``inverse`` property, which can also be used to access or modify them: >>> bi.inverse bidict({'aught': 1}) >>> bi.inverse['aught'] 1 >>> bi.inverse['aught'] = 'one' >>> bi bidict({'one': 'aught'}) >>> bi.inverse.pop('aught') 'one' >>> bi == bi.inverse == bidict() True >>> bi.inverse.update(one=1) >>> bi bidict({1: 'one'}) >>> bi is bi.inverse.inverse True >>> bi.inverse is bi.inverse.inverse.inverse True bidicts work with ``inverted`` as expected: >>> from bidict import inverted >>> biinv = bidict(inverted(bi)) >>> biinv bidict({'one': 1}) This created a new object (equivalent but not identical): >>> biinv == bi.inverse True >>> biinv is bi.inverse False Inverting the inverse should round-trip: >>> bi == bidict(inverted(inverted(bi))) True >>> bi = bi.inverse >>> bi == bidict(inverted(inverted(bi))) True The rest of the ``MutableMapping`` interface is supported: >>> bi.get('one') 1 >>> bi.get('zero') >>> bi.get('zero', 'default') 'default' >>> list(bi.keys()) ['one'] >>> list(bi.values()) [1] >>> list(bi.items()) [('one', 1)] >>> bi.setdefault('one', 2) 1 >>> bi.setdefault('two', 2) 2 >>> bi.pop('one') 1 >>> bi bidict({'two': 2}) >>> bi.inverse bidict({2: 'two'}) >>> bi.pop('no-such-key') Traceback (most recent call last): ... KeyError: 'no-such-key' >>> bi.pop('no-such-key', 'default') 'default' >>> bi.pop('wrong', 'number', 'of', 'args') Traceback (most recent call last): ... TypeError: ...pop() takes from 2 to 3 positional arguments but 5 were given >>> bi.popitem() ('two', 2) >>> bi.popitem() Traceback (most recent call last): ... KeyError: 'popitem(): dictionary is empty' >>> bi.inverse.setdefault(3, 'three') 'three' >>> bi bidict({'three': 3}) >>> len(bi) # calls __len__ 1 >>> [key for key in bi] # calls __iter__, returns keys like dict ['three'] >>> 'three' in bi # calls __contains__ True >>> list(bi.keys()) ['three'] >>> list(bi.values()) [3] >>> bi.update([('four', 4)]) >>> bi.update({'five': 5}, six=6, seven=7) >>> sorted(bi.items(), key=lambda x: x[1]) [('three', 3), ('four', 4), ('five', 5), ('six', 6), ('seven', 7)] >>> bi.clear() >>> bi bidict() Empty updates are a no-op: >>> bi.update() >>> bi bidict() >>> bi.forceupdate() >>> bi bidict() Initializing with wrong number of positional args is a TypeError: >>> bidict('wrong', 'number', 'of', 'args') Traceback (most recent call last): ... TypeError: Expected at most 1 positional argument, got 4 Initializing with different keys mapping to the same value fails: >>> bidict([(1, 1), (2, 1)]) Traceback (most recent call last): ... bidict.ValueDuplicationError: 1 Adding a new key associated with an existing value fails: >>> b = bidict({1: 1}) >>> b[2] = 1 Traceback (most recent call last): ... bidict.ValueDuplicationError: 1 >>> b.update({2: 1}) Traceback (most recent call last): ... bidict.ValueDuplicationError: 1 ``forceput`` and ``forceupdate`` can be used instead: >>> b.forceput(2, 1) >>> b bidict({2: 1}) >>> b.forceupdate({1: 1}) >>> b bidict({1: 1}) Trying to insert an existing mapping does not raise, and is a no-op: >>> b = bidict({1: 'one'}) >>> b[1] = 'one' >>> b[1] 'one' >>> b.inverse['one'] = 1 >>> b.inverse['one'] 1 The following case does not half-succeed, i.e. the bidict is not in an inconsistent state after: >>> b = bidict(one=1, two=2) >>> b['one'] = 2 Traceback (most recent call last): ... bidict.KeyAndValueDuplicationError: ('one', 2) >>> len(b) == len(b.inverse) True ``put`` and ``putall`` allow you to have per-call control over duplication behavior (see doctests in ``../docs/unique-values.rst.inc``). Even with RAISE duplication behavior, inserting existing items is a no-op (i.e. it doesn't raise): >>> from bidict import RAISE, OnDup >>> b.putall( ... [('three', 3), ('one', 1)], ... OnDup(key=RAISE, val=RAISE) ... ) # does not raise an error because these items were already contained >>> b0 = b.copy() >>> b.putall([]) # no-op >>> b == b0 True Make sure copy.copy and copy.deepcopy create shallow and deep copies, respectively: >>> from copy import copy, deepcopy >>> from bidict import frozenbidict >>> b = frozenbidict({1: frozenbidict()}) >>> c = copy(b) >>> d = deepcopy(b) >>> b == c == d True >>> b[1] is c[1] True >>> b[1] is d[1] False Bidicts support PEP 584-style dict merge operators: >>> b = bidict({'one': 1}) >>> b | {} bidict({'one': 1}) >>> b | {'one': 1} bidict({'one': 1}) >>> b | {'two': 2} bidict({'one': 1, 'two': 2}) >>> b | {'two': 2} | {'three': 3} bidict({'one': 1, 'two': 2, 'three': 3}) >>> b | {'one': 111} # duplicate key -> succeeds, last one wins (as with dict.__or__) bidict({'one': 111}) >>> {'one': 111} | b # ditto bidict({'one': 1}) >>> b | {'uno': 1} # duplicate value -> ValueDuplicationError Traceback (most recent call last): ... bidict.ValueDuplicationError: 1 >>> {'uno': 1} | b # ditto Traceback (most recent call last): ... bidict.ValueDuplicationError: 1 >>> b = bidict({'one': 1, 'two': 2}) >>> b | {'one': 2} Traceback (most recent call last): ... bidict.KeyAndValueDuplicationError: ('one', 2) >>> b |= {'three': 3} # in-place merge >>> b bidict({'one': 1, 'two': 2, 'three': 3}) >>> b |= {'one': 111} # in-place merge, duplicate key -> succeeds, last one wins >>> b bidict({'one': 111, 'two': 2, 'three': 3}) >>> b |= {'dos': 2} # duplicate value -> ValueDuplicationError Traceback (most recent call last): ... bidict.ValueDuplicationError: 2 >>> b bidict({'one': 111, 'two': 2, 'three': 3}) >>> b |= {'one': 3} Traceback (most recent call last): ... bidict.KeyAndValueDuplicationError: ('one', 3) >>> b | 1 Traceback (most recent call last): ... TypeError: unsupported operand type(s) for |: 'bidict' and 'int' >>> 1 | b Traceback (most recent call last): ... TypeError: unsupported operand type(s) for |: 'int' and 'bidict' bidict-0.22.1/tests/test_class_relationships.py000066400000000000000000000144511435416354600216760ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """Test various issubclass checks.""" from __future__ import annotations from collections.abc import Hashable, Mapping, MutableMapping, Reversible from collections import OrderedDict import sys import typing as t import pytest from bidict import ( bidict, frozenbidict, namedbidict, FrozenOrderedBidict, OrderedBidict, BidirectionalMapping, MutableBidirectionalMapping, BidictBase, MutableBidict, OrderedBidictBase, NamedBidictBase, GeneratedBidictInverse, ) from bidict._typing import TypeAlias class AbstractBimap(BidirectionalMapping[t.Any, t.Any]): """Does not override `inverse` and therefore should not be instantiatable.""" BiT: TypeAlias = t.Type[BidictBase[t.Any, t.Any]] BIDICT_BASE_TYPES: tuple[BiT, ...] = (BidictBase, MutableBidict, OrderedBidictBase) BIDICT_TYPES = BIDICT_BASE_TYPES + (bidict, frozenbidict, FrozenOrderedBidict, OrderedBidict) MyNamedBidict = namedbidict('MyNamedBidict', 'key', 'val') # type: ignore BIMAP_TYPES = BIDICT_TYPES + (AbstractBimap, MyNamedBidict) NOT_BIMAP_TYPES = (dict, OrderedDict, int, object) MUTABLE_BIDICT_TYPES = (bidict, OrderedBidict) HASHABLE_BIDICT_TYPES = (frozenbidict, FrozenOrderedBidict) ORDERED_BIDICT_TYPES = (OrderedBidict, FrozenOrderedBidict) @pytest.mark.parametrize('bi_cls', BIMAP_TYPES) def test_issubclass_bimap(bi_cls: BiT) -> None: """All bidict types should be considered subclasses of :class:`BidirectionalMapping`.""" assert issubclass(bi_cls, BidirectionalMapping) @pytest.mark.parametrize('not_bi_cls', NOT_BIMAP_TYPES) def test_not_issubclass_not_bimap(not_bi_cls: t.Any) -> None: """Classes that do not conform to :class:`BidirectionalMapping` interface should not be considered subclasses of it. """ assert not issubclass(not_bi_cls, BidirectionalMapping) @pytest.mark.parametrize('bi_cls', BIDICT_TYPES) def test_issubclass_mapping(bi_cls: BiT) -> None: """All bidict types should be :class:`collections.abc.Mapping`s.""" assert issubclass(bi_cls, Mapping) @pytest.mark.parametrize('bi_cls', MUTABLE_BIDICT_TYPES) def test_issubclass_mutable_and_mutable_bidirectional_mapping(bi_cls: BiT) -> None: """All mutable bidict types should be mutable (bidirectional) mappings.""" assert issubclass(bi_cls, MutableMapping) assert issubclass(bi_cls, MutableBidirectionalMapping) def test_issubclass_namedbidict() -> None: """Named bidicts should derive from NamedBidictBase and their inverse classes from GeneratedBidictInverse.""" assert issubclass(MyNamedBidict, NamedBidictBase) assert issubclass(MyNamedBidict._inv_cls, GeneratedBidictInverse) @pytest.mark.parametrize('bi_cls', HASHABLE_BIDICT_TYPES) def test_hashable_not_mutable(bi_cls: BiT) -> None: """All hashable bidict types should not be mutable (bidirectional) mappings.""" assert not issubclass(bi_cls, MutableMapping) assert not issubclass(bi_cls, MutableBidirectionalMapping) @pytest.mark.parametrize('bi_cls', HASHABLE_BIDICT_TYPES) def test_issubclass_hashable(bi_cls: BiT) -> None: """All hashable bidict types should implement :class:`collections.abc.Hashable`.""" assert issubclass(bi_cls, Hashable) @pytest.mark.parametrize('bi_cls', ORDERED_BIDICT_TYPES) def test_ordered_reversible(bi_cls: BiT) -> None: """All ordered bidict types should be reversible.""" assert issubclass(bi_cls, Reversible) def test_issubclass_internal() -> None: """The docs specifically recommend using ABCs over concrete classes when checking whether an interface is provided (see :ref:`polymorphism`). The relationships tested here are not guaranteed to hold in the future, but are still tested so that any unintentional changes won't go unnoticed. """ assert not issubclass(bidict, FrozenOrderedBidict) assert not issubclass(bidict, OrderedBidict) assert not issubclass(bidict, frozenbidict) assert not issubclass(FrozenOrderedBidict, OrderedBidict) assert not issubclass(FrozenOrderedBidict, bidict) assert not issubclass(FrozenOrderedBidict, frozenbidict) assert not issubclass(OrderedBidict, FrozenOrderedBidict) assert not issubclass(OrderedBidict, bidict) assert not issubclass(OrderedBidict, frozenbidict) assert not issubclass(frozenbidict, FrozenOrderedBidict) assert not issubclass(frozenbidict, OrderedBidict) assert not issubclass(frozenbidict, bidict) # Regression test for #111, Bug in BidirectionalMapping.__subclasshook__(): # Any class with an inverse attribute is considered a collections.abc.Mapping OnlyHasInverse = type('OnlyHasInverse', (), {'inverse': 'foo'}) assert not issubclass(OnlyHasInverse, Mapping) def test_abstract_bimap_init_fails() -> None: """Instantiating `AbstractBimap` should fail with expected TypeError.""" excmatch = r"Can't instantiate abstract class AbstractBimap with abstract methods .* inverse" with pytest.raises(TypeError, match=excmatch): AbstractBimap() # type: ignore def test_bimap_inverse_notimplemented() -> None: """Calling .inverse on a BidirectionalMapping should raise :class:`NotImplementedError`.""" with pytest.raises(NotImplementedError): # Can't instantiate a BidirectionalMapping that hasn't overridden the abstract methods of # the interface, so only way to call this implementation is on the class. BidirectionalMapping.inverse.fget(bidict()) # type: ignore @pytest.mark.parametrize('bi_cls', BIDICT_BASE_TYPES) def test_bidict_bases_init_succeed(bi_cls: BiT) -> None: """Bidict base classes should be initializable and have a working .inverse property.""" b = bi_cls(one=1, two=2) assert dict(b.inverse) == {1: 'one', 2: 'two'} def test_bidict_reversible_matches_dict_reversible() -> None: """Reversibility of bidict matches dict's on all supported Python versions.""" assert issubclass(bidict, Reversible) == issubclass(dict, Reversible) @pytest.mark.skipif(sys.version_info < (3, 8), reason='reversible unordered bidicts require Python 3.8+') def test_bidict_reversible() -> None: """All bidicts are Reversible on Python 3.8+.""" assert issubclass(bidict, Reversible) bidict-0.22.1/tests/test_metadata.py000066400000000000000000000011301435416354600173730ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """Test bidict metadata.""" from __future__ import annotations import bidict METADATA_ATTRS = """ __author__ __copyright__ __description__ __license__ __url__ __version__ """.split() def test_metadata() -> None: """Ensure bidict has expected metadata attributes.""" for i in METADATA_ATTRS: assert getattr(bidict, i) bidict-0.22.1/tests/test_microbenchmarks.py000066400000000000000000000210441435416354600207700ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """Microbenchmarks.""" from __future__ import annotations from collections import deque from functools import partial import pickle import typing as t import pytest import bidict as b consume = partial(deque, maxlen=0) LENS = (99, 999, 9_999) DICTS_BY_LEN = {n: dict(zip(range(n), range(n))) for n in LENS} BIDICTS_BY_LEN = {n: b.bidict(DICTS_BY_LEN[n]) for n in LENS} ORDERED_BIDICTS_BY_LEN = {n: b.OrderedBidict(DICTS_BY_LEN[n]) for n in LENS} DICTS_BY_LEN_LAST_ITEM_DUPVAL = {n: {**DICTS_BY_LEN[n], **{n - 1: 0}} for n in LENS} if isinstance(dict, t.Reversible): _checkd = next(iter(DICTS_BY_LEN_LAST_ITEM_DUPVAL.values())) _lastk, _lastv = next(reversed(_checkd.items())) _firstk, _firstv = next(iter(_checkd.items())) assert _firstk != _lastk and _firstv == _lastv BIDICT_AND_DICT_ONLY_LAST_ITEM_DIFFERENT = { n: (BIDICTS_BY_LEN[n], DICTS_BY_LEN_LAST_ITEM_DUPVAL[n]) for n in LENS } _checkbi, _checkd = next(iter(BIDICT_AND_DICT_ONLY_LAST_ITEM_DIFFERENT.values())) assert _checkbi != _checkd assert tuple(_checkbi.items())[:-1] == tuple(_checkd.items())[:-1] ORDERED_BIDICT_AND_DICT_ONLY_LAST_ITEM_DIFFERENT = { n: (b.OrderedBidict(bi_and_d[0]), bi_and_d[1]) for (n, bi_and_d) in BIDICT_AND_DICT_ONLY_LAST_ITEM_DIFFERENT.items() } BIDICT_AND_DICT_LAST_TWO_ITEMS_DIFFERENT_ORDER = {} ORDERED_BIDICT_AND_DICT_LAST_TWO_ITEMS_DIFFERENT_ORDER = {} for _i in LENS: _bi = BIDICTS_BY_LEN[_i] _d = dict(_bi) _last, _secondlast = _d.popitem(), _d.popitem() _d[_last[0]] = _last[1] # new second-last _d[_secondlast[0]] = _secondlast[1] # new last BIDICT_AND_DICT_LAST_TWO_ITEMS_DIFFERENT_ORDER[_i] = (_bi, _d) ORDERED_BIDICT_AND_DICT_LAST_TWO_ITEMS_DIFFERENT_ORDER[_i] = (b.OrderedBidict(_bi), _d) @pytest.mark.parametrize('n', LENS) def test_bi_init_from_dict(n: int, benchmark: t.Any) -> None: """Benchmark initializing a new bidict from a dict.""" other = DICTS_BY_LEN[n] benchmark(b.bidict, other) @pytest.mark.parametrize('n', LENS) def test_bi_init_from_bi(n: int, benchmark: t.Any) -> None: """Benchmark initializing a bidict from another bidict.""" other = BIDICTS_BY_LEN[n] benchmark(b.bidict, other) @pytest.mark.parametrize('n', LENS) def test_bi_init_fail_worst_case(n: int, benchmark: t.Any) -> None: """Benchmark initializing a bidict from a dict with a final duplicate value.""" other = DICTS_BY_LEN_LAST_ITEM_DUPVAL[n] def expect_failing_init() -> None: try: b.bidict(other) except b.DuplicationError: pass else: raise Exception('Expected DuplicationError') benchmark(expect_failing_init) @pytest.mark.parametrize('n', LENS) def test_empty_bi_update_from_bi(n: int, benchmark: t.Any) -> None: """Benchmark updating an empty bidict from another bidict.""" bi: b.bidict[int, int] = b.bidict() other = BIDICTS_BY_LEN[n] benchmark(bi.update, other) assert bi == other @pytest.mark.parametrize('n', LENS) def test_small_bi_large_update_fails_worst_case(n: int, benchmark: t.Any) -> None: """Benchmark updating a small bidict with a large update that fails on the final item and then rolls back.""" bi = b.bidict(zip(range(-9, 0), range(-9, 0))) other = DICTS_BY_LEN_LAST_ITEM_DUPVAL[n] def apply_failing_update() -> None: try: bi.update(other) except b.DuplicationError: pass # Rollback should happen here. else: raise Exception('Expected DuplicationError') benchmark(apply_failing_update) assert list(bi.items()) == list(zip(range(-9, 0), range(-9, 0))) @pytest.mark.parametrize('n', LENS) def test_bi_iter(n: int, benchmark: t.Any) -> None: """Benchmark iterating over a bidict.""" bi = BIDICTS_BY_LEN[n] benchmark(consume, iter(bi)) @pytest.mark.parametrize('n', LENS) def test_orderedbi_iter(n: int, benchmark: t.Any) -> None: """Benchmark iterating over an OrderedBidict.""" ob = ORDERED_BIDICTS_BY_LEN[n] benchmark(consume, iter(ob)) @pytest.mark.parametrize('n', LENS) def test_bi_contains_key_present(n: int, benchmark: t.Any) -> None: """Benchmark bidict.__contains__ with a contained key.""" bi = BIDICTS_BY_LEN[n] key = next(iter(bi)) result = benchmark(bi.__contains__, key) assert result @pytest.mark.parametrize('n', LENS) def test_bi_contains_key_missing(n: int, benchmark: t.Any) -> None: """Benchmark bidict.__contains__ with a missing key.""" bi = BIDICTS_BY_LEN[n] result = benchmark(bi.__contains__, object()) assert not result @pytest.mark.parametrize('n', LENS) def test_bi_equals_with_equal_dict(n: int, benchmark: t.Any) -> None: """Benchmark bidict.__eq__ with an equivalent dict.""" bi, d = BIDICT_AND_DICT_LAST_TWO_ITEMS_DIFFERENT_ORDER[n] result = benchmark(bi.__eq__, d) assert result @pytest.mark.parametrize('n', LENS) def test_orderedbi_equals_with_equal_dict(n: int, benchmark: t.Any) -> None: """Benchmark OrderedBidict.__eq__ with an equivalent dict.""" ob, d = ORDERED_BIDICT_AND_DICT_LAST_TWO_ITEMS_DIFFERENT_ORDER[n] result = benchmark(ob.__eq__, d) assert result @pytest.mark.parametrize('n', LENS) def test_orderedbi_items_equals_with_equal_dict_items(n: int, benchmark: t.Any) -> None: """Benchmark OrderedBidict.items().__eq__ with an equivalent dict_items.""" ob, d = ORDERED_BIDICT_AND_DICT_LAST_TWO_ITEMS_DIFFERENT_ORDER[n] obi, di = ob.items(), d.items() result = benchmark(obi.__eq__, di) assert result @pytest.mark.parametrize('n', LENS) def test_orderedbi_items_equals_with_unequal_dict_items(n: int, benchmark: t.Any) -> None: """Benchmark OrderedBidict.items().__eq__ with an unequal dict_items.""" ob, d = ORDERED_BIDICT_AND_DICT_ONLY_LAST_ITEM_DIFFERENT[n] obi, di = ob.items(), d.items() result = benchmark(obi.__eq__, di) assert not result @pytest.mark.parametrize('n', LENS) def test_bi_equals_with_unequal_dict(n: int, benchmark: t.Any) -> None: """Benchmark bidict.__eq__ with an unequal dict.""" bi, d = BIDICT_AND_DICT_ONLY_LAST_ITEM_DIFFERENT[n] result = benchmark(bi.__eq__, d) assert not result @pytest.mark.parametrize('n', LENS) def test_orderedbi_equals_with_unequal_dict(n: int, benchmark: t.Any) -> None: """Benchmark OrderedBidict.__eq__ with an unequal dict.""" ob, d = ORDERED_BIDICT_AND_DICT_ONLY_LAST_ITEM_DIFFERENT[n] result = benchmark(ob.__eq__, d) assert not result @pytest.mark.parametrize('n', LENS) def test_bi_order_sensitive_equals_dict(n: int, benchmark: t.Any) -> None: """Benchmark bidict.equals_order_sensitive with an order-sensitive-equal dict.""" bi, d = BIDICTS_BY_LEN[n], DICTS_BY_LEN[n] result = benchmark(bi.equals_order_sensitive, d) assert result @pytest.mark.parametrize('n', LENS) def test_orderedbi_order_sensitive_equals_dict(n: int, benchmark: t.Any) -> None: """Benchmark OrderedBidict.equals_order_sensitive with an order-sensitive-equal dict.""" ob, d = ORDERED_BIDICTS_BY_LEN[n], DICTS_BY_LEN[n] result = benchmark(ob.equals_order_sensitive, d) assert result @pytest.mark.parametrize('n', LENS) def test_bi_equals_order_sensitive_with_unequal_dict(n: int, benchmark: t.Any) -> None: """Benchmark bidict.equals_order_sensitive with an order-sensitive-unequal dict.""" bi, d = BIDICT_AND_DICT_LAST_TWO_ITEMS_DIFFERENT_ORDER[n] result = benchmark(bi.equals_order_sensitive, d) assert not result @pytest.mark.parametrize('n', LENS) def test_orderedbi_equals_order_sensitive_with_unequal_dict(n: int, benchmark: t.Any) -> None: """Benchmark OrderedBidict.equals_order_sensitive with an order-sensitive-unequal dict.""" ob, d = ORDERED_BIDICT_AND_DICT_LAST_TWO_ITEMS_DIFFERENT_ORDER[n] result = benchmark(ob.equals_order_sensitive, d) assert not result @pytest.mark.parametrize('n', LENS) def test_copy(n: int, benchmark: t.Any) -> None: """Benchmark creating a copy of a bidict.""" bi = BIDICTS_BY_LEN[n] benchmark(bi.copy) @pytest.mark.parametrize('n', LENS) def test_pickle(n: int, benchmark: t.Any) -> None: """Benchmark pickling a bidict.""" bi = BIDICTS_BY_LEN[n] benchmark(pickle.dumps, bi) @pytest.mark.parametrize('n', LENS) def test_unpickle(n: int, benchmark: t.Any) -> None: """Benchmark unpickling a bidict.""" bp = pickle.dumps(BIDICTS_BY_LEN[n]) benchmark(pickle.loads, bp) bidict-0.22.1/tests/test_namedbidict.txt000066400000000000000000000017021435416354600202520ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. Test script for bidict.namedbidict: >>> from bidict import namedbidict >>> ElBySym = namedbidict('ElBySym', 'sym', 'el') >>> el_by_sym = ElBySym(H='hydrogen') >>> el_by_sym['H'] 'hydrogen' >>> el_by_sym.inverse['hydrogen'] 'H' >>> el_by_sym.keyname 'sym' >>> el_by_sym.valname 'el' >>> el_by_sym.el_for['H'] 'hydrogen' >>> el_by_sym.sym_for['hydrogen'] 'H' >>> el_by_sym.inverse.el_for['H'] 'hydrogen' >>> el_by_sym.inverse.sym_for['hydrogen'] 'H' >>> el_by_sym['He'] = 'helium' >>> el_by_sym ElBySym({'H': 'hydrogen', 'He': 'helium'}) >>> el_by_sym.inverse ElBySymInv({'hydrogen': 'H', 'helium': 'He'}) bidict-0.22.1/tests/test_orderedbidict.txt000066400000000000000000000107371435416354600206220ustar00rootroot00000000000000# Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. Test for consistency in ordered bidicts after handling duplicate keys/values: >>> from bidict import OrderedBidict, RAISE, DROP_OLD, OnDup >>> b = OrderedBidict([(0, 1)]) >>> b.update([(0, 2), (3, 4), (5, 4)]) Traceback (most recent call last): ... bidict.ValueDuplicationError: 4 >>> b OrderedBidict([(0, 1)]) >>> b.inv OrderedBidict([(1, 0)]) >>> b.putall([(2, 1), (2, 3)], OnDup(key=RAISE, val=DROP_OLD)) Traceback (most recent call last): ... bidict.KeyDuplicationError: 2 >>> b OrderedBidict([(0, 1)]) >>> list(b._node_by_korv) [0] >>> b.inv OrderedBidict([(1, 0)]) >>> b.forceupdate([(0, 1), (2, 3), (0, 3)]) >>> b OrderedBidict([(0, 3)]) >>> list(b._node_by_korv) [0] >>> b.inv OrderedBidict([(3, 0)]) Test for consistency updating an ordered bidict's inverse: >>> b.inv[3] = 'UPDATED-KEY' >>> b OrderedBidict([('UPDATED-KEY', 3)]) >>> list(b._node_by_korv) ['UPDATED-KEY'] >>> b.inv OrderedBidict([(3, 'UPDATED-KEY')]) >>> b.inv.forceput('UPDATED-VAL', 'UPDATED-KEY') >>> b OrderedBidict([('UPDATED-KEY', 'UPDATED-VAL')]) >>> list(b._node_by_korv) ['UPDATED-KEY'] >>> b.inv OrderedBidict([('UPDATED-VAL', 'UPDATED-KEY')]) >>> b.inv['NEW-VAL'] = 'NEW-KEY' >>> b OrderedBidict([('UPDATED-KEY', 'UPDATED-VAL'), ('NEW-KEY', 'NEW-VAL')]) >>> list(b._node_by_korv) ['UPDATED-KEY', 'NEW-KEY'] >>> b.inv OrderedBidict([('UPDATED-VAL', 'UPDATED-KEY'), ('NEW-VAL', 'NEW-KEY')]) >>> b.inv.forceput('NEW-VAL', 'UPDATED-KEY') >>> b OrderedBidict([('UPDATED-KEY', 'NEW-VAL')]) >>> list(b._node_by_korv) ['UPDATED-KEY'] >>> b.inv OrderedBidict([('NEW-VAL', 'UPDATED-KEY')]) >>> b.inv.update([('NEWER-VAL', 'NEWER-KEY'), ('NEW-VAL', 'NEW-KEY'), ('FAIL!', 'NEW-KEY')]) Traceback (most recent call last): ... bidict.ValueDuplicationError: NEW-KEY >>> b OrderedBidict([('UPDATED-KEY', 'NEW-VAL')]) >>> b.inv OrderedBidict([('NEW-VAL', 'UPDATED-KEY')]) >>> b.inv.forceupdate([('NEWER-VAL', 'NEWER-KEY'), ('NEW-VAL', 'NEW-KEY'), ('SUCCESS!', 'NEW-KEY')]) >>> b OrderedBidict([('NEW-KEY', 'SUCCESS!'), ('NEWER-KEY', 'NEWER-VAL')]) >>> b.inv OrderedBidict([('SUCCESS!', 'NEW-KEY'), ('NEWER-VAL', 'NEWER-KEY')]) Test the following here so it shows up in pytest's coverage report (the hypothesis tests may not always hit all code paths, and the doctests in the Sphinx docs don't get counted in the coverage report): >>> b.move_to_end('NEW-KEY') >>> b OrderedBidict([('NEWER-KEY', 'NEWER-VAL'), ('NEW-KEY', 'SUCCESS!')]) >>> b.inverse.move_to_end('SUCCESS!', last=False) >>> b OrderedBidict([('NEW-KEY', 'SUCCESS!'), ('NEWER-KEY', 'NEWER-VAL')]) >>> b.move_to_end('NOT-FOUND') Traceback (most recent call last): ... KeyError: 'NOT-FOUND' >>> b.inverse.move_to_end('NOT-FOUND') Traceback (most recent call last): ... KeyError: 'NOT-FOUND' >>> b.popitem(last=False) ('NEW-KEY', 'SUCCESS!') >>> b.inverse.popitem(last=True) ('NEWER-VAL', 'NEWER-KEY') >>> b.popitem() Traceback (most recent call last): ... KeyError: 'OrderedBidict is empty' >>> b.inverse.popitem() Traceback (most recent call last): ... KeyError: 'OrderedBidict is empty' >>> b._update(arg=()) >>> b OrderedBidict() >>> OrderedBidict({0: 1}).pop(0) 1 >>> OrderedBidict({0: 1}).inverse.pop(1) 0 >>> OrderedBidict().pop(0) Traceback (most recent call last): ... KeyError: 0 >>> OrderedBidict().inverse.pop(1) Traceback (most recent call last): ... KeyError: 1 The views returned by .keys(), values(), and items() are reversible: >>> b = OrderedBidict([(0, 1), (2, 3)]) >>> list(reversed(b.keys())) [2, 0] >>> list(reversed(b.values())) [3, 1] >>> list(reversed(b.items())) [(2, 3), (0, 1)] And they compare as expected: >>> b1 = OrderedBidict([(0, 1)]) >>> b2 = OrderedBidict([(0, 1)]) >>> b1.items() == b2.items() True >>> b1 = OrderedBidict([(False, None)]).inverse >>> b2 = b1.copy() >>> list(b1.items()) == list(b2.items()) True bidict-0.22.1/tox.ini000066400000000000000000000014731435416354600143650ustar00rootroot00000000000000[tox] envlist = py3{11,10,9,8,7} pypy3{9,8,7} lint docs skip_missing_interpreters = true isolated_build = true [testenv] deps = -r dev-deps/test.txt passenv = PYTEST_ADDOPTS # https://docs.pytest.org/en/7.1.x/reference/reference.html#envvar-FORCE_COLOR FORCE_COLOR # https://hypothesis.readthedocs.io/en/latest/settings.html#hypothesis.settings.print_blob # https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables CI allowlist_externals = ./run_tests.py commands = ./run_tests.py [testenv:lint] deps = -r dev-deps/lint.txt skip_install = true commands = pre-commit run --all-files --verbose --show-diff-on-failure [testenv:docs] deps = -r dev-deps/docs.txt commands = sphinx-build -W --keep-going -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html bidict-0.22.1/upgrade-dev-dependencies.sh000077500000000000000000000030121435416354600202270ustar00rootroot00000000000000#!/usr/bin/env bash # # Copyright 2009-2022 Joshua Bronson. All rights reserved. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. set -euo pipefail log() { >&2 echo -e "$@" } main() { if ! type pre-commit || ! type pip-compile; then log "Error: pre-commit or pip-compile not found." exit 1 fi local -r gitbranch=$(git branch --show-current) if [ "$gitbranch" = "deps" ] || [ "$gitbranch" = "dev" ]; then log "Already on branch '$gitbranch'" elif [ "$gitbranch" = "main" ]; then git checkout -b deps main else log "On unsupported branch '$gitbranch'. Switch to 'main' and try again." exit 1 fi # Not adding --generate-hashes due to https://github.com/jazzband/pip-tools/issues/1326 local -r pip_compile="pip-compile --upgrade --resolver=backtracking --allow-unsafe" printf '%s\0' docs test lint | xargs -0 -P0 -I% ${pip_compile} pyproject.toml --extra=% -o dev-deps/%.txt ${pip_compile} dev-deps/dev.in pip uninstall -y -r <(pip freeze) pip install -r dev-deps/dev.txt pre-commit autoupdate pre-commit clean log "Dev dependencies upgraded." log "Reminders:" \ "\n - Check release notes of upgraded packages for anything that affects bidict." \ "\n - Run tests via 'tox' or by pushing to the 'deps' branch to ensure everything still works." \ "\n - Check output for any new warnings, not just test failures." } main