├── generic ├── __init__.py ├── event.py ├── multimethod.py ├── multidispatch.py └── registry.py ├── docs ├── registry.rst ├── index.rst ├── event_system.rst ├── Makefile ├── multidispatching.rst └── conf.py ├── .github ├── github-requirements.txt ├── pr-labeler.yml ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── pr-labeler.yml │ ├── release-drafter.yml │ ├── dependency-review.yml │ ├── codeql.yml │ ├── scorecard.yml │ ├── pre-commit-updater.yml │ └── build.yml ├── scripts │ └── metadata.sh ├── release-drafter.yml └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .readthedocs.yml ├── .reuse └── dep5 ├── SECURITY.md ├── .pre-commit-config.yaml ├── LICENSES └── BSD-3-Clause.txt ├── tests ├── test_event_exception.py ├── test_multimethod.py ├── test_registry.py ├── test_event.py └── test_multidispatch.py ├── pyproject.toml ├── README.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── poetry.lock /generic/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/registry.rst: -------------------------------------------------------------------------------- 1 | Registry 2 | ======== 3 | -------------------------------------------------------------------------------- /.github/github-requirements.txt: -------------------------------------------------------------------------------- 1 | poetry==2.1.1 2 | pre-commit==4.5.0 3 | -------------------------------------------------------------------------------- /.github/pr-labeler.yml: -------------------------------------------------------------------------------- 1 | feature: ['feature/*', 'feat/*'] 2 | fix: fix/* 3 | chore: chore/* 4 | skip-changelog: ['sourcery/*', 'dependabot/*', 'pre-commit-ci-*'] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | generic.egg-info/ 3 | docs/_build/ 4 | .coverage 5 | .ropeproject 6 | __pycache__ 7 | .mypy_cache 8 | cc-test-reporter 9 | htmlcov/ 10 | .python-version 11 | 12 | # Virtualenv 13 | .venv 14 | 15 | # Tox 16 | .tox 17 | 18 | .vscode/ 19 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | formats: all 3 | build: 4 | os: ubuntu-24.04 5 | tools: 6 | python: "3.13" 7 | jobs: 8 | pre_install: 9 | - python -m pip install --constraint=.github/github-requirements.txt poetry 10 | - poetry config virtualenvs.create false 11 | post_install: 12 | - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry install --with docs --no-interaction 13 | sphinx: 14 | configuration: docs/conf.py 15 | -------------------------------------------------------------------------------- /.reuse/dep5: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: Generic 3 | Upstream-Contact: Generic Library contributors 4 | Source: https://github.com/gaphor/generic 5 | 6 | Files: * 7 | Copyright: 2010 Andrey Popp, 2019 Arjan Molenaar & Dan Yeaw 8 | License: BSD-3-Clause 9 | 10 | Files: generic/registry.py 11 | Copyright: 2009-2010 Christopher Michael Rossi, 2019 Arjan Molenaar & Dan Yeaw 12 | License: BSD-3-Clause 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | groups: 6 | github-action-updates: 7 | patterns: 8 | - "*" 9 | schedule: 10 | interval: weekly 11 | labels: ["skip-changelog"] 12 | 13 | - package-ecosystem: "pip" 14 | directory: "/" 15 | groups: 16 | pip-updates: 17 | patterns: 18 | - "*" 19 | schedule: 20 | interval: weekly 21 | labels: ["skip-changelog"] 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We are currently supporting the latest released version of the library. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | Generic has GitHub's Private Security Vulnerability Reporting enabled. Please 10 | go to the Security tab to report security vulnerabilities. For more 11 | information, please see the [GitHub docs on privately reporting]( 12 | https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability). 13 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/mirrors-mypy 3 | rev: a66e98df7b4aeeb3724184b332785976d062b92e # frozen: v1.19.1 4 | hooks: 5 | - id: mypy 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0 8 | hooks: 9 | - id: check-toml 10 | - id: check-yaml 11 | - id: end-of-file-fixer 12 | - repo: https://github.com/astral-sh/ruff-pre-commit 13 | rev: '5ba58aca0bd5bc7c0e1c0fc45af2e88d6a2bde83' # frozen: v0.14.10 14 | hooks: 15 | - id: ruff 16 | args: [--fix] 17 | - id: ruff-format 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/pr-labeler.yml: -------------------------------------------------------------------------------- 1 | name: PR Labeler 2 | on: 3 | pull_request_target: 4 | types: [opened] 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | pr-labeler: 11 | permissions: 12 | pull-requests: write # for TimonVS/pr-labeler-action to add labels in PR 13 | runs-on: ubuntu-24.04 14 | if: "!contains(github.event.head_commit.message, 'skip ci')" 15 | steps: 16 | - name: Harden Runner 17 | uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 18 | with: 19 | egress-policy: audit 20 | 21 | - uses: TimonVS/pr-labeler-action@f9c084306ce8b3f488a8f3ee1ccedc6da131d1af # v5.0.0 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | -------------------------------------------------------------------------------- /.github/scripts/metadata.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "GITHUB_REF is $GITHUB_REF" 4 | TAG="${GITHUB_REF/refs\/tags\//}" 5 | echo "TAG is $TAG" 6 | if ! [ -x "$(command -v poetry)" ]; then 7 | echo 'Poetry not found!' >&2 8 | exit 1 9 | fi 10 | VERSION="$(poetry version --no-ansi | cut -d' ' -f2)" 11 | echo "VERSION is $VERSION" 12 | 13 | if [[ "$GITHUB_REF" =~ refs\/tags\/.* && "$TAG" == "${VERSION}" ]] 14 | then 15 | REV="" 16 | RELEASE="true" 17 | else 18 | # PEP440 version scheme, different from semver 2.0 19 | REV=".dev${GITHUB_RUN_NUMBER:-0}+${GITHUB_SHA:0:8}" 20 | RELEASE="false" 21 | 22 | poetry version "${VERSION}""${REV}" 23 | fi 24 | 25 | echo "version=${VERSION}${REV}" >> "$GITHUB_OUTPUT" 26 | echo "release=${RELEASE}" >> "$GITHUB_OUTPUT" 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **OS** 27 | - [ ] Linux (Please put in notes the specific distro) 28 | - [ ] MacOS 29 | - [ ] Windows 30 | 31 | NOTES: 32 | 33 | **Version** 34 | Version of Generic: 35 | 36 | **Additional context** 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'Version $NEXT_PATCH_VERSION - Summary Here🌈' 2 | tag-template: 'v$NEXT_PATCH_VERSION' 3 | exclude-labels: 4 | - 'skip-changelog' 5 | categories: 6 | - title: '🚀 Features' 7 | labels: 8 | - 'feature' 9 | - 'enhancement' 10 | - title: '🐛 Bug Fixes' 11 | labels: 12 | - 'fix' 13 | - 'bugfix' 14 | - 'bug' 15 | - title: '🧰 Maintenance' 16 | label: 'chore' 17 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 18 | template: | 19 | ## Changes 20 | 21 | $CHANGES 22 | 23 | Thanks again to $CONTRIBUTORS! 🎉 24 | no-changes-template: 'Changes are coming soon 😎' 25 | replacers: 26 | - search: '(?:and )?@dependabot(?:\[bot\])?,?' 27 | replace: '' 28 | - search: '(?:and )?@sourcery-ai-bot(?:\[bot\])?,?' 29 | replace: '' 30 | - search: '(?:and )?@allcontributors(?:\[bot\])?,?' 31 | replace: '' 32 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: main 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | update-release-draft: 12 | permissions: 13 | contents: write # for release-drafter/release-drafter to create a github release 14 | pull-requests: write # for release-drafter/release-drafter to add label to PR 15 | runs-on: ubuntu-24.04 16 | if: "!contains(github.event.head_commit.message, 'skip ci')" 17 | steps: 18 | # Drafts your next Release notes as Pull Requests are merged into "main" 19 | - name: Harden Runner 20 | uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 21 | with: 22 | egress-policy: audit 23 | 24 | - uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6.1.0 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, 4 | # surfacing known-vulnerable versions of the packages declared or updated in the PR. 5 | # Once installed, if the workflow run is marked as required, 6 | # PRs introducing known-vulnerable packages will be blocked from merging. 7 | # 8 | # Source repository: https://github.com/actions/dependency-review-action 9 | name: 'Dependency Review' 10 | on: [pull_request] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | dependency-review: 17 | runs-on: ubuntu-24.04 18 | steps: 19 | - name: Harden Runner 20 | uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 21 | with: 22 | egress-policy: audit 23 | 24 | - name: 'Checkout Repository' 25 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 26 | - name: 'Dependency Review' 27 | uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ### PR Checklist 5 | Please check if your PR fulfills the following requirements: 6 | 7 | - [ ] I have read, and I am following the [Contributor guide](https://github.com/gaphor/generic/blob/main/CONTRIBUTING.md) 8 | - [ ] I have read and understand the [Code of Conduct](https://github.com/gaphor/generic/blob/main/CODE_OF_CONDUCT.md) 9 | 10 | ### PR Type 11 | What kind of change does this PR introduce? 12 | 13 | 14 | - [ ] Bugfix 15 | - [ ] Feature 16 | - [ ] Code style update (formatting, local variables) 17 | - [ ] Refactoring (no functional changes, no api changes) 18 | - [ ] Documentation content changes 19 | 20 | ### What is the current behavior? 21 | 22 | 23 | Issue Number: N/A 24 | 25 | ### What is the new behavior? 26 | 27 | ### Does this PR introduce a breaking change? 28 | - [ ] Yes 29 | - [ ] No 30 | 31 | 32 | 33 | 34 | ### Other information 35 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Generic programming library for Python 2 | ====================================== 3 | 4 | Generic is trying to provide a Python programmer with primitives for creating 5 | reusable software components by employing advanced techniques of OOP and other 6 | programming paradigms. 7 | 8 | This documentation suits both needs in a tutorial and an API reference for 9 | generic: 10 | 11 | .. toctree:: 12 | :maxdepth: 3 13 | :hidden: 14 | 15 | multidispatching 16 | event_system 17 | registry 18 | 19 | Installation 20 | ------------ 21 | 22 | You can get generic with *pip*:: 23 | 24 | % python -m pip install generic 25 | 26 | In case you find a bug or have a feature request, please file a ticket at 27 | `GitHub Issues`_. 28 | 29 | .. _GitHub Issues: https://github.com/gaphor/generic/issues 30 | 31 | Development process 32 | ------------------- 33 | 34 | Development takes place at `GitHub`_, you can clone the source code repository with the 35 | following command:: 36 | 37 | % git clone git://github.com/gaphor/generic.git 38 | 39 | We love contributions! If you are submitting a GitHub pull request please ensure you 40 | have tests for your bugfix or new functionality. 41 | 42 | .. _GitHub: https://github.com/gaphor/generic 43 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: ["main"] 9 | schedule: 10 | - cron: "0 0 * * 1" 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | analyze: 17 | name: Analyze 18 | runs-on: ubuntu-24.04 19 | permissions: 20 | actions: read 21 | contents: read 22 | security-events: write 23 | 24 | steps: 25 | - name: Harden Runner 26 | uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 27 | with: 28 | egress-policy: audit 29 | 30 | - name: Checkout repository 31 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 32 | 33 | # Initializes the CodeQL tools for scanning. 34 | - name: Initialize CodeQL 35 | uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 36 | with: 37 | languages: python 38 | 39 | - name: Perform CodeQL Analysis 40 | uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 41 | with: 42 | category: "/language:python" 43 | -------------------------------------------------------------------------------- /LICENSES/BSD-3-Clause.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Andrey Popp, 2019 Arjan Molenaar & Dan Yeaw. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /tests/test_event_exception.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from sys import version_info 4 | from typing import Callable 5 | 6 | import pytest 7 | 8 | if version_info < (3, 11): 9 | from exceptiongroup import ExceptionGroup 10 | 11 | from generic.event import Event, Manager 12 | 13 | 14 | @pytest.fixture 15 | def events(): 16 | return Manager() 17 | 18 | 19 | def make_handler(effect: object) -> Callable[[Event], None]: 20 | def handler(e): 21 | e.effects.append(effect) 22 | raise ValueError(effect) 23 | 24 | return handler 25 | 26 | 27 | def test_handle_all_subscribers(events): 28 | events.subscribe(make_handler("handler1"), MyEvent) 29 | events.subscribe(make_handler("handler2"), MyEvent) 30 | e = MyEvent() 31 | with pytest.raises(ExceptionGroup): 32 | events.handle(e) 33 | 34 | assert len(e.effects) == 2 35 | assert "handler1" in e.effects 36 | assert "handler2" in e.effects 37 | 38 | 39 | def test_collect_all_exceptions(events): 40 | events.subscribe(make_handler("handler1"), MyEvent) 41 | events.subscribe(make_handler("handler2"), MyEvent) 42 | e = MyEvent() 43 | with pytest.raises(ExceptionGroup) as excinfo: 44 | events.handle(e) 45 | 46 | exc = excinfo.value 47 | nested_exc = [str(e) for e in exc.exceptions] 48 | assert len(exc.exceptions) == 2 49 | assert "handler1" in nested_exc 50 | assert "handler2" in nested_exc 51 | 52 | 53 | class MyEvent: 54 | def __init__(self) -> None: 55 | self.effects: list[object] = [] 56 | -------------------------------------------------------------------------------- /docs/event_system.rst: -------------------------------------------------------------------------------- 1 | Event system 2 | ============ 3 | 4 | Generic library provides ``generic.event`` module which helps you implement 5 | event systems in your application. By event system I mean an API for 6 | *subscribing* for some types of events and to *handle* those events so previously 7 | subscribed *handlers* are being executed. 8 | 9 | Basic usage 10 | ----------- 11 | 12 | First you need to describe event types you want to use in your application, 13 | ``generic.event`` dispatches events to corresponding handlers by inspecting 14 | events' types, so it's natural to model those as classes:: 15 | 16 | >>> class CommentAdded(object): 17 | ... def __init__(self, post_id, comment): 18 | ... self.post_id = post_id 19 | ... self.comment = comment 20 | 21 | Now you want to register handler for your event type:: 22 | 23 | >>> from generic.event import Manager 24 | 25 | >>> manager = Manager() 26 | 27 | >>> @manager.subscriber(CommentAdded) 28 | ... def print_comment(ev): 29 | ... print(f"Got new comment: {ev.comment}") 30 | 31 | Then you just call ``generic.event.handle`` function with ``CommentAdded`` 32 | instance as its argument:: 33 | 34 | >>> manager.handle(CommentAdded(167, "Hello!")) 35 | Got new comment: Hello! 36 | 37 | This is how it works. 38 | 39 | Event inheritance 40 | ----------------- 41 | 42 | Using per-application event API 43 | ------------------------------- 44 | 45 | API reference 46 | ------------- 47 | 48 | .. autoclass:: generic.event.Manager 49 | :members: subscribe, subscriber, handle, unsubscribe 50 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | name: Scorecard supply-chain security 2 | on: 3 | branch_protection_rule: 4 | schedule: 5 | - cron: '17 12 * * 6' 6 | push: 7 | branches: [ "main" ] 8 | 9 | # Declare default permissions as read only. 10 | permissions: read-all 11 | 12 | jobs: 13 | analysis: 14 | name: Scorecard analysis 15 | runs-on: ubuntu-24.04 16 | permissions: 17 | # Needed to upload the results to code-scanning dashboard. 18 | security-events: write 19 | # Needed to publish results and get a badge (see publish_results below). 20 | id-token: write 21 | 22 | steps: 23 | - name: Harden Runner 24 | uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 25 | with: 26 | egress-policy: audit 27 | 28 | - name: "Checkout code" 29 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 30 | with: 31 | persist-credentials: false 32 | 33 | - name: "Run analysis" 34 | uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 35 | with: 36 | results_file: results.sarif 37 | results_format: sarif 38 | publish_results: true 39 | 40 | - name: "Upload artifact" 41 | uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 42 | with: 43 | name: SARIF file 44 | path: results.sarif 45 | retention-days: 5 46 | 47 | - name: "Upload to code-scanning" 48 | uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 49 | with: 50 | sarif_file: results.sarif 51 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit-updater.yml: -------------------------------------------------------------------------------- 1 | name: Pre-commit updater 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | # min hour dom month dow 6 | - cron: '0 5 * * 3' 7 | env: 8 | python_version: '3.13' 9 | 10 | jobs: 11 | 12 | updater: 13 | name: Update 14 | runs-on: ubuntu-24.04 15 | steps: 16 | - name: Harden Runner 17 | uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 18 | with: 19 | disable-sudo: true 20 | egress-policy: block 21 | allowed-endpoints: > 22 | files.pythonhosted.org:443 23 | pypi.org:443 24 | github.com:443 25 | api.github.com:443 26 | *.githubusercontent.com:443 27 | ghcr.io 28 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 29 | with: 30 | ref: main 31 | - name: Set up Python 32 | uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 33 | with: 34 | python-version: ${{ env.python_version }} 35 | cache: pip 36 | - name: Install pre-commit 37 | run: python -m pip install pre-commit 38 | - name: Update pre-commit hooks 39 | run: pre-commit autoupdate --freeze 40 | - name: Run pre-commit hooks 41 | run: pre-commit run --all-files 42 | - name: Create GitHub App Token 43 | uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 44 | id: generate-token 45 | with: 46 | app-id: ${{ secrets.GAPHOR_UPDATER_APP_ID }} 47 | private-key: ${{ secrets.GAPHOR_UPDATER_APP_PRIVATE_KEY }} 48 | - name: Create Pull Request 49 | uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 50 | with: 51 | token: ${{ steps.generate-token.outputs.token }} 52 | commit-message: Update pre-commit hooks 53 | branch: pre-commit-update 54 | delete-branch: true 55 | title: 'Update pre-commit hooks' 56 | body: | 57 | This PR was automatically created to make the following update: 58 | - Update pre-commit hooks 59 | labels: | 60 | skip-changelog 61 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "generic" 3 | version = "1.1.6" 4 | description = "Generic programming library for Python" 5 | authors = [ 6 | { name = "Andrey Popp", email = "8mayday@gmail.com" }, 7 | { name = "Arjan Molenaar", email = "gaphor@gmail.com" }, 8 | ] 9 | maintainers = [ 10 | { name = "Arjan Molenaar", email = "gaphor@gmail.com" }, 11 | { name = "Dan Yeaw", email = "dan@yeaw.me" }, 12 | ] 13 | 14 | readme = "README.md" 15 | 16 | keywords = ["generic", "multi dispatch", "dispatch", "event"] 17 | 18 | classifiers = [ 19 | "Development Status :: 5 - Production/Stable", 20 | "Intended Audience :: Developers", 21 | "Intended Audience :: Information Technology", 22 | "License :: OSI Approved :: BSD License", 23 | "Programming Language :: Python", 24 | ] 25 | 26 | requires-python = ">=3.9" 27 | 28 | dependencies = [ 29 | "exceptiongroup>=1.0.0 ; python_version < '3.11'", 30 | ] 31 | 32 | [project.urls] 33 | homepage = "https://generic.readthedocs.io/" 34 | repository = "https://github.com/gaphor/generic" 35 | documentation = "https://generic.readthedocs.io/" 36 | 37 | [tool.poetry] 38 | requires-poetry = ">=2.0" 39 | 40 | [tool.poetry.group.dev.dependencies] 41 | pytest = "^8.3" 42 | pytest-cov = "^7.0" 43 | 44 | [tool.poetry.group.docs] 45 | optional=true 46 | 47 | [tool.poetry.group.docs.dependencies] 48 | sphinx = ">=4.3,<8.0" 49 | furo = ">=2022,<2026" 50 | 51 | [tool.pytest.ini_options] 52 | testpaths = [ 53 | "tests", 54 | "docs", 55 | ] 56 | addopts = [ 57 | "--doctest-modules", 58 | "--doctest-glob='*.rst'", 59 | "--import-mode=importlib", 60 | ] 61 | 62 | [tool.coverage.run] 63 | source = ["generic"] 64 | 65 | [tool.mypy] 66 | python_version = 3.9 67 | warn_return_any = true 68 | warn_unused_configs = true 69 | warn_redundant_casts = true 70 | check_untyped_defs = true 71 | strict_optional = true 72 | show_error_codes = true 73 | ignore_missing_imports=true 74 | warn_unused_ignores = true 75 | namespace_packages = true 76 | 77 | [[tool.mypy.overrides]] 78 | module = [ 79 | "pytest.*", 80 | "conf", 81 | ] 82 | ignore_missing_imports = true 83 | warn_unreachable = true 84 | 85 | [tool.ruff] 86 | exclude = [ 87 | ".venv", 88 | "dist", 89 | ] 90 | line-length = 88 91 | 92 | [tool.ruff.lint] 93 | ignore = ["E501"] 94 | select = [ 95 | "B", 96 | "B9", 97 | "C", 98 | "E", 99 | "F", 100 | "W", 101 | ] 102 | 103 | [tool.ruff.mccabe] 104 | max-complexity = 18 105 | 106 | [build-system] 107 | requires = ["poetry-core>=2.0.0,<3.0.0"] 108 | build-backend = "poetry.core.masonry.api" 109 | -------------------------------------------------------------------------------- /generic/event.py: -------------------------------------------------------------------------------- 1 | """Event management system. 2 | 3 | This module provides API for event management. 4 | """ 5 | 6 | from sys import version_info 7 | from typing import Callable, Set, Type 8 | 9 | if version_info < (3, 11): 10 | from exceptiongroup import ExceptionGroup 11 | 12 | from generic.registry import Registry, TypeAxis 13 | 14 | __all__ = "Manager" 15 | 16 | Event = object 17 | Handler = Callable[[object], None] 18 | HandlerSet = Set[Handler] 19 | 20 | 21 | class Manager: 22 | """Event manager. 23 | 24 | Provides API for subscribing for and firing events. 25 | """ 26 | 27 | registry: Registry[HandlerSet] 28 | 29 | def __init__(self) -> None: 30 | axes = (("event_type", TypeAxis()),) 31 | self.registry = Registry(*axes) 32 | 33 | def subscribe(self, handler: Handler, event_type: Type[Event]) -> None: 34 | """Subscribe ``handler`` to specified ``event_type``""" 35 | handler_set = self.registry.get_registration(event_type) 36 | if handler_set is None: 37 | handler_set = self._register_handler_set(event_type) 38 | handler_set.add(handler) 39 | 40 | def unsubscribe(self, handler: Handler, event_type: Type[Event]) -> None: 41 | """Unsubscribe ``handler`` from ``event_type``""" 42 | handler_set = self.registry.get_registration(event_type) 43 | if handler_set and handler in handler_set: 44 | handler_set.remove(handler) 45 | 46 | def handle(self, event: Event) -> None: 47 | """Fire ``event`` 48 | 49 | All subscribers will be executed with no determined order. If a 50 | handler raises an exceptions, an `ExceptionGroup` will be raised 51 | containing all raised exceptions. 52 | """ 53 | handler_sets = self.registry.query(event) 54 | for handler_set in handler_sets: 55 | if handler_set: 56 | exceptions = [] 57 | for handler in set(handler_set): 58 | try: 59 | handler(event) 60 | except BaseException as e: 61 | exceptions.append(e) 62 | if exceptions: 63 | raise ExceptionGroup("Error while handling events", exceptions) 64 | 65 | def _register_handler_set(self, event_type: Type[Event]) -> HandlerSet: 66 | """Register new handler set for ``event_type``.""" 67 | handler_set: HandlerSet = set() 68 | self.registry.register(handler_set, event_type) 69 | return handler_set 70 | 71 | def subscriber(self, event_type: Type[Event]) -> Callable[[Handler], Handler]: 72 | """Decorator for subscribing handlers. 73 | 74 | Works like this: 75 | 76 | >>> mymanager = Manager() 77 | >>> class MyEvent(): 78 | ... pass 79 | >>> @mymanager.subscriber(MyEvent) 80 | ... def mysubscriber(evt): 81 | ... # handle event 82 | ... return 83 | 84 | >>> mymanager.handle(MyEvent()) 85 | """ 86 | 87 | def registrator(func: Handler) -> Handler: 88 | self.subscribe(func, event_type) 89 | return func 90 | 91 | return registrator 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Generic programming library for Python 2 | 3 | [![Build state](https://github.com/gaphor/generic/workflows/build/badge.svg)](https://github.com/gaphor/generic/actions) 4 | [![Maintainability](https://qlty.sh/gh/gaphor/projects/generic/maintainability.svg)](https://qlty.sh/gh/gaphor/projects/generic) 5 | [![Code Coverage](https://qlty.sh/gh/gaphor/projects/generic/coverage.svg)](https://qlty.sh/gh/gaphor/projects/generic) 6 | [![Documentation Status](https://readthedocs.org/projects/generic/badge/?version=latest)](https://generic.readthedocs.io/en/latest/?badge=latest) 7 | [![Matrix](https://img.shields.io/badge/chat-on%20Matrix-success)](https://app.element.io/#/room/#gaphor_Lobby:gitter.im) 8 | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/gaphor/generic/badge)](https://securityscorecards.dev/viewer/?platform=github.com&org=gaphor&repo=generic) 9 | 10 | Generic is a library for [Generic programming](https://en.wikipedia.org/wiki/Generic_programming), also known as [Multiple dispatch](https://en.wikipedia.org/wiki/Multiple_dispatch). 11 | 12 | The Generic library supports: 13 | 14 | * multi-dispatch: like `functools.singledispatch`, but for more than one parameter 15 | * multi-methods: multi-dispatch, but for methods 16 | * event dispatching: based on a hierarchical event structure (event objects) 17 | 18 | You can read 19 | [documentation](http://generic.readthedocs.org/en/latest/index.html) hosted at 20 | excellent readthedocs.org project. Development takes place on 21 | [github](http://github.com/gaphor/generic). 22 | 23 | 24 | # Changes 25 | 26 | ## 1.1.6 27 | 28 | - Refactoring 29 | 30 | ## 1.1.5 31 | 32 | - Fix regression with super type dispatching 33 | - Dependency updates 34 | 35 | ## 1.1.4 36 | 37 | - Dependency updates 38 | 39 | ## 1.1.3 40 | 41 | - Dependency updates 42 | 43 | ## 1.1.2 44 | 45 | - Replace print statements with logging 46 | - Enable trusted publisher for PyPI 47 | - Create Security Policy 48 | - Update LICENSE to BSD 3-Clause 49 | - Add support for Python 3.12 50 | - Simplify build: drop tox 51 | - Update documentation theme to Furo 52 | - Switch linting to ruff 53 | 54 | ## 1.1.1 55 | 56 | - Add support for Python 3.11 57 | - Move mypy configuration to pyproject.toml 58 | - Enable automatic release of new versions with CI 59 | 60 | ## 1.1.0 61 | 62 | - Rename `master` branch to `main` 63 | - `generic.event.Manager` executes all handlers and throws an `ExceptionGroup` in case of errors 64 | 65 | ## 1.0.1 66 | 67 | - Add Support for Python 3.10, Drop Support for Python 3.7 68 | - Enable Pre-commit Hooks for isort, toml, yaml, pyupgrade, docformatter, and flake8 69 | - Migrate to GitHub Actions 70 | 71 | ## 1.0.0 72 | 73 | - Updated documentation on [Readthedocs](https://generic.readthedocs.io) 74 | - Fix `multimethod.otherwise` clause 75 | 76 | ## 1.0.0b1 77 | 78 | - Ported the code to Python 3.7, Python 2 is no longer supported 79 | - Multimethods now have their own module 80 | - The interface now mimics `functools.singledispatch`: 81 | - the `when` method has been renamed to `register` 82 | - overriding of methods is no longer possible 83 | 84 | ## 0.3.1 85 | 86 | - Minor fixes in distribution. 87 | 88 | ## 0.3 89 | 90 | - Event management with event inheritance support. 91 | 92 | ## 0.2 93 | 94 | - Methods with multidispatch by object type and positional arguments. 95 | - Override multifunctions with ``override`` method. 96 | 97 | ## 0.1 98 | 99 | - Registry with simple and type axes. 100 | - Functions with multidispatch by positional arguments. 101 | -------------------------------------------------------------------------------- /tests/test_multimethod.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from generic.multimethod import has_multimethods, multimethod 4 | 5 | 6 | def test_multimethod(): 7 | @has_multimethods 8 | class Dummy: 9 | @multimethod(int) 10 | def foo(self, x): 11 | return x + 1 12 | 13 | @foo.register(str) # type: ignore[no-redef] 14 | def foo(self, x): 15 | return f"{x}1" 16 | 17 | assert Dummy().foo(1) == 2 18 | assert Dummy().foo("1") == "11" 19 | with pytest.raises(TypeError): 20 | Dummy().foo([]) 21 | 22 | 23 | def test_multimethod_with_two_arguments(): 24 | @has_multimethods 25 | class Dummy: 26 | @multimethod(int, int) 27 | def foo(self, x, y): 28 | return x * y 29 | 30 | @foo.register(str, int) # type: ignore[no-redef] 31 | def foo(self, s, x): 32 | return s * x 33 | 34 | assert Dummy().foo(1, 1) == 1 35 | assert Dummy().foo("1", 2) == "11" 36 | with pytest.raises(TypeError): 37 | Dummy().foo([]) 38 | 39 | 40 | def test_multimethod_otherwise_clause(): 41 | @has_multimethods 42 | class Dummy: 43 | @multimethod(int) 44 | def foo(self, x): 45 | return x + 1 46 | 47 | @foo.otherwise # type: ignore[no-redef] 48 | def foo(self, x): 49 | return type(x) 50 | 51 | assert Dummy().foo(1) == 2 52 | assert Dummy().foo("") is str 53 | assert Dummy().foo([]) is list 54 | 55 | 56 | def test_multimethod_otherwise_clausewith_two_arguments(): 57 | @has_multimethods 58 | class Dummy: 59 | @multimethod(int, int) 60 | def foo(self, x, y): 61 | return x * y 62 | 63 | @foo.otherwise # type: ignore[no-redef] 64 | def foo(self, s, x): 65 | return f"{s} {x}" 66 | 67 | assert Dummy().foo(1, 2) == 2 68 | assert Dummy().foo("a", []) == "a []" 69 | 70 | 71 | def test_inheritance(): 72 | @has_multimethods 73 | class Dummy: 74 | @multimethod(int) 75 | def foo(self, x): 76 | return x + 1 77 | 78 | @foo.register(float) # type: ignore[no-redef] 79 | def foo(self, x): 80 | return x + 1.5 81 | 82 | @has_multimethods 83 | class DummySub(Dummy): 84 | @Dummy.foo.register(str) 85 | def foo(self, x): 86 | return f"{x}1" 87 | 88 | @foo.register(tuple) # type: ignore[no-redef] 89 | def foo(self, x): 90 | return x + (1,) 91 | 92 | @Dummy.foo.register(bool) # type: ignore[no-redef] 93 | def foo(self, x): 94 | return not x 95 | 96 | assert Dummy().foo(1) == 2 97 | assert Dummy().foo(1.5) == 3.0 98 | 99 | with pytest.raises(TypeError): 100 | Dummy().foo("1") 101 | assert DummySub().foo(1) == 2 102 | assert DummySub().foo(1.5) == 3.0 103 | assert DummySub().foo("1") == "11" 104 | assert DummySub().foo((1, 2)) == (1, 2, 1) 105 | assert DummySub().foo(True) is False 106 | with pytest.raises(TypeError): 107 | DummySub().foo([]) 108 | 109 | 110 | def test_override_in_same_class_not_allowed(): 111 | with pytest.raises(ValueError): 112 | 113 | @has_multimethods 114 | class Dummy: 115 | @multimethod(str, str) 116 | def foo(self, x, y): 117 | return x + y 118 | 119 | @foo.register(str, str) # type: ignore[no-redef] 120 | def foo(self, x, y): 121 | return y + x 122 | 123 | 124 | def test_inheritance_override(): 125 | @has_multimethods 126 | class Dummy: 127 | @multimethod(int) 128 | def foo(self, x): 129 | return x + 1 130 | 131 | @has_multimethods 132 | class DummySub(Dummy): 133 | @Dummy.foo.register(int) 134 | def foo(self, x): 135 | return x + 3 136 | 137 | assert Dummy().foo(1) == 2 138 | assert DummySub().foo(1) == 4 139 | -------------------------------------------------------------------------------- /generic/multimethod.py: -------------------------------------------------------------------------------- 1 | """Multi-method builds on the functionality provided by `multidispatch` to 2 | provide generic methods.""" 3 | 4 | from __future__ import annotations 5 | 6 | import functools 7 | import inspect 8 | import logging 9 | import threading 10 | import types 11 | from typing import Any, Callable, TypeVar, Union, cast 12 | 13 | from generic.multidispatch import FunctionDispatcher, KeyType 14 | 15 | __all__ = ("multimethod", "has_multimethods") 16 | 17 | C = TypeVar("C") 18 | T = TypeVar("T", bound=Union[Callable[..., Any], type]) 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | def multimethod(*argtypes: KeyType) -> Callable[[T], MethodDispatcher[T]]: 24 | """Declare method as multimethod. 25 | 26 | This decorator works exactly the same as :func:`.multidispatch` decorator 27 | but replaces decorated method with :class:`.MethodDispatcher` object 28 | instead. 29 | 30 | Should be used only for decorating methods and enclosing class should have 31 | :func:`.has_multimethods` decorator. 32 | """ 33 | 34 | def _replace_with_dispatcher(func): 35 | nonlocal argtypes 36 | argspec = inspect.getfullargspec(func) 37 | 38 | dispatcher = cast( 39 | MethodDispatcher, 40 | functools.update_wrapper( 41 | MethodDispatcher(argspec, len(argtypes) + 1), func 42 | ), 43 | ) 44 | dispatcher.register_unbound_rule(func, *argtypes) 45 | return dispatcher 46 | 47 | return _replace_with_dispatcher 48 | 49 | 50 | def has_multimethods(cls: type[C]) -> type[C]: 51 | """Declare class as one that have multimethods. 52 | 53 | Should only be used for decorating classes which have methods decorated with 54 | :func:`.multimethod` decorator. 55 | """ 56 | for _name, obj in cls.__dict__.items(): 57 | if isinstance(obj, MethodDispatcher): 58 | obj.proceed_unbound_rules(cls) 59 | return cls 60 | 61 | 62 | class MethodDispatcher(FunctionDispatcher[T]): 63 | """Multiple dispatch for methods. 64 | 65 | This object dispatch call to method by its class and arguments types. 66 | Usually it is produced by :func:`.multimethod` decorator. 67 | 68 | You should not manually create objects of this type. 69 | """ 70 | 71 | def __init__(self, argspec: inspect.FullArgSpec, params_arity: int) -> None: 72 | super().__init__(argspec, params_arity) 73 | 74 | # some data, that should be local to thread of execution 75 | self.local = threading.local() 76 | self.local.unbound_rules = [] 77 | 78 | def register_unbound_rule(self, func, *argtypes) -> None: 79 | """Register unbound rule that should be processed by 80 | ``proceed_unbound_rules`` later.""" 81 | self.local.unbound_rules.append((argtypes, func)) 82 | 83 | def proceed_unbound_rules(self, cls) -> None: 84 | """Process all unbound rule by binding them to ``cls`` type.""" 85 | for argtypes, func in self.local.unbound_rules: 86 | argtypes = (cls,) + argtypes 87 | logger.debug("register rule %s", argtypes) 88 | self.register_rule(func, *argtypes) 89 | self.local.unbound_rules = [] 90 | 91 | def __get__(self, obj, cls): 92 | return self if obj is None else types.MethodType(self, obj) 93 | 94 | def register(self, *argtypes: KeyType) -> Callable[[T], T]: 95 | """Register new case for multimethod for ``argtypes``""" 96 | 97 | def make_declaration(meth): 98 | self.register_unbound_rule(meth, *argtypes) 99 | return self 100 | 101 | return make_declaration 102 | 103 | @property 104 | def otherwise(self) -> Callable[[T], T]: 105 | """Decorator which registers "catch-all" case for multimethod.""" 106 | 107 | def make_declaration(meth): 108 | self.register_unbound_rule(meth, *([object] * (self.params_arity - 1))) 109 | return self 110 | 111 | return make_declaration 112 | -------------------------------------------------------------------------------- /tests/test_registry.py: -------------------------------------------------------------------------------- 1 | """Tests for :module:`generic.registry`.""" 2 | 3 | from typing import Union 4 | 5 | import pytest 6 | 7 | from generic.registry import Registry, SimpleAxis, TypeAxis 8 | 9 | 10 | class DummyA: 11 | pass 12 | 13 | 14 | class DummyB(DummyA): 15 | pass 16 | 17 | 18 | def test_one_axis_no_specificity(): 19 | registry: Registry[object] = Registry(("foo", SimpleAxis())) 20 | a = object() 21 | b = object() 22 | registry.register(a) 23 | registry.register(b, "foo") 24 | 25 | assert registry.lookup() == a 26 | assert registry.lookup("foo") == b 27 | assert registry.lookup("bar") is None 28 | 29 | 30 | def test_subtyping_on_axes(): 31 | registry: Registry[str] = Registry(("type", TypeAxis())) 32 | 33 | target1 = "one" 34 | registry.register(target1, object) 35 | 36 | target2 = "two" 37 | registry.register(target2, DummyA) 38 | 39 | target3 = "three" 40 | registry.register(target3, DummyB) 41 | 42 | assert registry.lookup(object()) == target1 43 | assert registry.lookup(DummyA()) == target2 44 | assert registry.lookup(DummyB()) == target3 45 | 46 | 47 | def test_query_subtyping_on_axes(): 48 | registry: Registry[str] = Registry(("type", TypeAxis())) 49 | 50 | target1 = "one" 51 | registry.register(target1, object) 52 | 53 | target2 = "two" 54 | registry.register(target2, DummyA) 55 | 56 | target3 = "three" 57 | registry.register(target3, DummyB) 58 | 59 | target4 = "four" 60 | registry.register(target4, int) 61 | 62 | assert list(registry.query(object())) == [target1] 63 | assert list(registry.query(DummyA())) == [target2, target1] 64 | assert list(registry.query(DummyB())) == [target3, target2, target1] 65 | assert list(registry.query(3)) == [target4, target1] 66 | 67 | 68 | def test_two_axes(): 69 | registry: Registry[Union[str, object]] = Registry( 70 | ("type", TypeAxis()), ("name", SimpleAxis()) 71 | ) 72 | 73 | target1 = "one" 74 | registry.register(target1, object) 75 | 76 | target2 = "two" 77 | registry.register(target2, DummyA) 78 | 79 | target3 = "three" 80 | registry.register(target3, DummyA, "foo") 81 | 82 | context1 = object() 83 | assert registry.lookup(context1) == target1 84 | 85 | context2 = DummyB() 86 | assert registry.lookup(context2) == target2 87 | assert registry.lookup(context2, "foo") == target3 88 | 89 | target4 = object() 90 | registry.register(target4, DummyB) 91 | 92 | assert registry.lookup(context2) == target4 93 | assert registry.lookup(context2, "foo") == target3 94 | 95 | 96 | def test_get_registration(): 97 | registry: Registry[str] = Registry(("type", TypeAxis()), ("name", SimpleAxis())) 98 | registry.register("one", object) 99 | registry.register("two", DummyA, "foo") 100 | assert registry.get_registration(object) == "one" 101 | assert registry.get_registration(DummyA, "foo") == "two" 102 | assert registry.get_registration(object, "foo") is None 103 | assert registry.get_registration(DummyA) is None 104 | 105 | 106 | def test_register_too_many_keys(): 107 | registry: Registry[type] = Registry(("name", SimpleAxis())) 108 | with pytest.raises(ValueError): 109 | registry.register(object, "one", "two") 110 | 111 | 112 | def test_lookup_too_many_keys(): 113 | registry: Registry[object] = Registry(("name", SimpleAxis())) 114 | with pytest.raises(ValueError): 115 | registry.register(registry.lookup("one", "two")) 116 | 117 | 118 | def test_conflict_error(): 119 | registry: Registry[Union[object, type]] = Registry(("name", SimpleAxis())) 120 | registry.register(object(), name="foo") 121 | with pytest.raises(ValueError): 122 | registry.register(object, "foo") 123 | 124 | 125 | def test_skip_nodes(): 126 | registry: Registry[str] = Registry( 127 | ("one", SimpleAxis()), ("two", SimpleAxis()), ("three", SimpleAxis()) 128 | ) 129 | registry.register("foo", one=1, three=3) 130 | assert registry.lookup(1, three=3) == "foo" 131 | 132 | 133 | def test_miss(): 134 | registry: Registry[str] = Registry( 135 | ("one", SimpleAxis()), ("two", SimpleAxis()), ("three", SimpleAxis()) 136 | ) 137 | registry.register("foo", 1, 2) 138 | assert registry.lookup(one=1, three=3) is None 139 | 140 | 141 | def test_bad_lookup(): 142 | registry: Registry[int] = Registry(("name", SimpleAxis()), ("grade", SimpleAxis())) 143 | with pytest.raises(ValueError): 144 | registry.register(1, foo=1) 145 | with pytest.raises(ValueError): 146 | registry.lookup(foo=1) 147 | with pytest.raises(ValueError): 148 | registry.register(1, "foo", name="foo") 149 | -------------------------------------------------------------------------------- /tests/test_event.py: -------------------------------------------------------------------------------- 1 | """Tests for :module:`generic.event`.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Callable 6 | 7 | from generic.event import Manager 8 | 9 | 10 | def make_handler(effect: object) -> Callable[[Event], None]: 11 | return lambda e: e.effects.append(effect) 12 | 13 | 14 | def create_manager(): 15 | return Manager() 16 | 17 | 18 | def test_subscribe_single_event(): 19 | events = create_manager() 20 | events.subscribe(make_handler("handler1"), EventA) 21 | e = EventA() 22 | events.handle(e) 23 | assert len(e.effects) == 1 24 | assert "handler1" in e.effects 25 | 26 | 27 | def test_subscribe_via_decorator(): 28 | events = create_manager() 29 | events.subscriber(EventA)(make_handler("handler1")) 30 | e = EventA() 31 | events.handle(e) 32 | assert len(e.effects) == 1 33 | assert "handler1" in e.effects 34 | 35 | 36 | def test_subscribe_event_inheritance(): 37 | events = create_manager() 38 | events.subscribe(make_handler("handler1"), EventA) 39 | events.subscribe(make_handler("handler2"), EventB) 40 | 41 | ea = EventA() 42 | events.handle(ea) 43 | assert len(ea.effects) == 1 44 | assert "handler1" in ea.effects 45 | 46 | eb = EventB() 47 | events.handle(eb) 48 | assert len(eb.effects) == 2 49 | assert "handler1" in eb.effects 50 | assert "handler2" in eb.effects 51 | 52 | 53 | def test_subscribe_event_multiple_inheritance(): 54 | events = create_manager() 55 | events.subscribe(make_handler("handler1"), EventA) 56 | events.subscribe(make_handler("handler2"), EventC) 57 | events.subscribe(make_handler("handler3"), EventD) 58 | 59 | ea = EventA() 60 | events.handle(ea) 61 | assert len(ea.effects) == 1 62 | assert "handler1" in ea.effects 63 | 64 | ec = EventC() 65 | events.handle(ec) 66 | assert len(ec.effects) == 1 67 | assert "handler2" in ec.effects 68 | 69 | ed = EventD() 70 | events.handle(ed) 71 | assert len(ed.effects) == 3 72 | assert "handler1" in ed.effects 73 | assert "handler2" in ed.effects 74 | assert "handler3" in ed.effects 75 | 76 | 77 | def test_subscribe_no_events(): 78 | events = create_manager() 79 | 80 | ea = EventA() 81 | events.handle(ea) 82 | assert len(ea.effects) == 0 83 | 84 | 85 | def test_subscribe_base_event(): 86 | events = create_manager() 87 | events.subscribe(make_handler("handler1"), EventA) 88 | 89 | ea = EventB() 90 | events.handle(ea) 91 | assert len(ea.effects) == 1 92 | assert "handler1" in ea.effects 93 | 94 | 95 | def test_subscribe_event_malformed_multiple_inheritance(): 96 | events = create_manager() 97 | events.subscribe(make_handler("handler1"), EventA) 98 | events.subscribe(make_handler("handler2"), EventD) 99 | events.subscribe(make_handler("handler3"), EventE) 100 | 101 | ea = EventA() 102 | events.handle(ea) 103 | assert len(ea.effects) == 1 104 | assert "handler1" in ea.effects 105 | 106 | ed = EventD() 107 | events.handle(ed) 108 | assert len(ed.effects) == 2 109 | assert "handler1" in ed.effects 110 | assert "handler2" in ed.effects 111 | 112 | ee = EventE() 113 | events.handle(ee) 114 | assert len(ee.effects) == 3 115 | assert "handler1" in ee.effects 116 | assert "handler2" in ee.effects 117 | assert "handler3" in ee.effects 118 | 119 | 120 | def test_subscribe_event_with_no_subscribers_in_the_middle_of_mro(): 121 | events = create_manager() 122 | events.subscribe(make_handler("handler1"), Event) 123 | events.subscribe(make_handler("handler2"), EventB) 124 | 125 | eb = EventB() 126 | events.handle(eb) 127 | assert len(eb.effects) == 2 128 | assert "handler1" in eb.effects 129 | assert "handler2" in eb.effects 130 | 131 | 132 | def test_unsubscribe_single_event(): 133 | events = create_manager() 134 | handler = make_handler("handler1") 135 | events.subscribe(handler, EventA) 136 | events.unsubscribe(handler, EventA) 137 | e = EventA() 138 | events.handle(e) 139 | assert len(e.effects) == 0 140 | 141 | 142 | def test_unsubscribe_event_inheritance(): 143 | events = create_manager() 144 | handler1 = make_handler("handler1") 145 | handler2 = make_handler("handler2") 146 | events.subscribe(handler1, EventA) 147 | events.subscribe(handler2, EventB) 148 | events.unsubscribe(handler1, EventA) 149 | 150 | ea = EventA() 151 | events.handle(ea) 152 | assert len(ea.effects) == 0 153 | 154 | eb = EventB() 155 | events.handle(eb) 156 | assert len(eb.effects) == 1 157 | assert "handler2" in eb.effects 158 | 159 | 160 | class Event: 161 | def __init__(self) -> None: 162 | self.effects: list[object] = [] 163 | 164 | 165 | class EventA(Event): 166 | pass 167 | 168 | 169 | class EventB(EventA): 170 | pass 171 | 172 | 173 | class EventC(Event): 174 | pass 175 | 176 | 177 | class EventD(EventA, EventC): 178 | pass 179 | 180 | 181 | class EventE(EventD, EventA): 182 | pass 183 | -------------------------------------------------------------------------------- /generic/multidispatch.py: -------------------------------------------------------------------------------- 1 | """Multidispatch for functions and methods. 2 | 3 | This code is a Python 3, slimmed down version of the 4 | generic package by Andrey Popp. 5 | 6 | Only the generic function code is left intact -- no generic methods. 7 | The interface has been made in line with `functools.singledispatch`. 8 | 9 | Note that this module does not support annotated functions. 10 | """ 11 | 12 | from __future__ import annotations 13 | 14 | import functools 15 | import inspect 16 | import logging 17 | from typing import Any, Callable, Generic, TypeVar, Union, cast 18 | 19 | from generic.registry import Registry, TypeAxis 20 | 21 | __all__ = "multidispatch" 22 | 23 | T = TypeVar("T", bound=Union[Callable[..., Any], type]) 24 | KeyType = Union[type, None] 25 | 26 | logger = logging.getLogger(__name__) 27 | 28 | 29 | def multidispatch(*argtypes: KeyType) -> Callable[[T], FunctionDispatcher[T]]: 30 | """Declare function as multidispatch. 31 | 32 | This decorator takes ``argtypes`` argument types and replace 33 | decorated function with :class:`.FunctionDispatcher` object, which 34 | is responsible for multiple dispatch feature. 35 | """ 36 | 37 | def _replace_with_dispatcher(func: T) -> FunctionDispatcher[T]: 38 | nonlocal argtypes 39 | argspec = inspect.getfullargspec(func) 40 | if not argtypes: 41 | arity = _arity(argspec) 42 | if isinstance(func, type): 43 | # It's a class we deal with: 44 | arity -= 1 45 | argtypes = (object,) * arity 46 | 47 | dispatcher = cast( 48 | FunctionDispatcher[T], 49 | functools.update_wrapper(FunctionDispatcher(argspec, len(argtypes)), func), 50 | ) 51 | dispatcher.register_rule(func, *argtypes) 52 | return dispatcher 53 | 54 | return _replace_with_dispatcher 55 | 56 | 57 | class FunctionDispatcher(Generic[T]): 58 | """Multidispatcher for functions. 59 | 60 | This object dispatch calls to function by its argument types. Usually it is 61 | produced by :func:`.multidispatch` decorator. 62 | 63 | You should not manually create objects of this type. 64 | """ 65 | 66 | registry: Registry[T] 67 | 68 | def __init__(self, argspec: inspect.FullArgSpec, params_arity: int) -> None: 69 | """Initialize dispatcher with ``argspec`` of type 70 | :class:`inspect.ArgSpec` and ``params_arity`` that represent number 71 | params.""" 72 | # Check if we have enough positional arguments for number of type params 73 | if _arity(argspec) < params_arity: 74 | raise TypeError( 75 | "Not enough positional arguments " 76 | "for number of type parameters provided." 77 | ) 78 | 79 | self.argspec = argspec 80 | self.params_arity = params_arity 81 | 82 | axis = [(f"arg_{n:d}", TypeAxis()) for n in range(params_arity)] 83 | self.registry = Registry(*axis) 84 | 85 | def check_rule(self, rule: T, *argtypes: KeyType) -> None: 86 | """Check if the argument types match wrt number of arguments. 87 | 88 | Raise TypeError in case of failure. 89 | """ 90 | # Check if we have the right number of parametrized types 91 | if len(argtypes) != self.params_arity: 92 | raise TypeError( 93 | f"Wrong number of type parameters: have {len(argtypes)}, expected {self.params_arity}." 94 | ) 95 | 96 | # Check if we have the same argspec (by number of args) 97 | rule_argspec = inspect.getfullargspec(rule) 98 | left_spec = tuple(x and len(x) or 0 for x in rule_argspec[:4]) 99 | right_spec = tuple(x and len(x) or 0 for x in self.argspec[:4]) 100 | if left_spec != right_spec: 101 | raise TypeError( 102 | f"Rule does not conform to previous implementations: {left_spec} != {right_spec}." 103 | ) 104 | 105 | def register_rule(self, rule: T, *argtypes: KeyType) -> None: 106 | """Register new ``rule`` for ``argtypes``.""" 107 | self.check_rule(rule, *argtypes) 108 | self.registry.register(rule, *argtypes) 109 | 110 | def register(self, *argtypes: KeyType) -> Callable[[T], T]: 111 | """Decorator for registering new case for multidispatch. 112 | 113 | New case will be registered for types identified by 114 | ``argtypes``. The length of ``argtypes`` should be equal to the 115 | length of ``argtypes`` argument were passed corresponding 116 | :func:`.multidispatch` call, which also indicated the number of 117 | arguments multidispatch dispatches on. 118 | """ 119 | 120 | def register_rule(func: T) -> T: 121 | """Register rule wrapper function.""" 122 | self.register_rule(func, *argtypes) 123 | return func 124 | 125 | return register_rule 126 | 127 | def __call__(self, *args: Any, **kwargs: Any) -> Any: 128 | """Dispatch call to appropriate rule.""" 129 | trimmed_args = args[: self.params_arity] 130 | rule = self.registry.lookup(*trimmed_args) 131 | if not rule: 132 | logger.debug(self.registry._tree) 133 | raise TypeError(f"No available rule found for {trimmed_args!r}") 134 | return rule(*args, **kwargs) 135 | 136 | 137 | def _arity(argspec: inspect.FullArgSpec) -> int: 138 | """Determinal positional arity of argspec.""" 139 | args = argspec.args or [] 140 | defaults: tuple[Any, ...] | list = argspec.defaults or [] 141 | return len(args) - len(defaults) 142 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, religion, or sexual identity 11 | and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the 27 | overall community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | dan@yeaw.me. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series 87 | of actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or 94 | permanent ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within 114 | the community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.0, available at 120 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 121 | 122 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 123 | enforcement ladder](https://github.com/mozilla/diversity). 124 | 125 | [homepage]: https://www.contributor-covenant.org 126 | 127 | For answers to common questions about this code of conduct, see the FAQ at 128 | https://www.contributor-covenant.org/faq. Translations are available at 129 | https://www.contributor-covenant.org/translations. 130 | -------------------------------------------------------------------------------- /generic/registry.py: -------------------------------------------------------------------------------- 1 | """Registry. 2 | 3 | This implementation was borrowed from happy[1] project by Chris Rossi. 4 | 5 | [1]: http://bitbucket.org/chrisrossi/happy 6 | """ 7 | 8 | from __future__ import annotations 9 | 10 | from typing import ( 11 | Any, 12 | Dict, 13 | Generator, 14 | Generic, 15 | KeysView, 16 | Sequence, 17 | TypeVar, 18 | Union, 19 | Iterator, 20 | ) 21 | 22 | __all__ = ("Registry", "SimpleAxis", "TypeAxis") 23 | 24 | K = TypeVar("K") 25 | S = TypeVar("S") 26 | T = TypeVar("T") 27 | V = TypeVar("V") 28 | Axis = Union["SimpleAxis", "TypeAxis"] 29 | 30 | 31 | class Registry(Generic[T]): 32 | """Registry implementation.""" 33 | 34 | def __init__(self, *axes: tuple[str, Axis]): 35 | self._tree: _TreeNode[T] = _TreeNode() 36 | self._axes = [axis for name, axis in axes] 37 | self._axes_dict = {name: (i, axis) for i, (name, axis) in enumerate(axes)} 38 | 39 | def register(self, target: T, *arg_keys: K, **kw_keys: K) -> None: 40 | tree_node = self._tree 41 | for key in self._align_with_axes(arg_keys, kw_keys): 42 | tree_node = tree_node.setdefault(key, _TreeNode()) 43 | 44 | if tree_node.target is not None: 45 | raise ValueError( 46 | f"Registration for {target} conflicts with existing registration {tree_node.target}." 47 | ) 48 | 49 | tree_node.target = target 50 | 51 | def get_registration(self, *arg_keys: K, **kw_keys: K) -> T | None: 52 | tree_node = self._tree 53 | for key in self._align_with_axes(arg_keys, kw_keys): 54 | if key not in tree_node: 55 | return None 56 | tree_node = tree_node[key] 57 | 58 | return tree_node.target 59 | 60 | def lookup(self, *arg_objs: V, **kw_objs: V) -> T | None: 61 | return next(self.query(*arg_objs, **kw_objs), None) 62 | 63 | def query(self, *arg_objs: V, **kw_objs: V) -> Iterator[T | None]: 64 | objs = self._align_with_axes(arg_objs, kw_objs) 65 | return filter(None, self._query(self._tree, objs, self._axes)) 66 | 67 | def _query( 68 | self, tree_node: _TreeNode[T], objs: Sequence[V | None], axes: Sequence[Axis] 69 | ) -> Generator[T | None, None, None]: 70 | """Recursively traverse registration tree, from left to right, most 71 | specific to least specific, returning the first target found on a 72 | matching node.""" 73 | if not objs: 74 | yield tree_node.target 75 | else: 76 | obj = objs[0] 77 | 78 | # Skip non-participating nodes 79 | if obj is None: 80 | next_node: _TreeNode[T] | None = tree_node.get(None, None) 81 | if next_node is not None: 82 | yield from self._query(next_node, objs[1:], axes[1:]) 83 | else: 84 | # Get matches on this axis and iterate from most to least specific 85 | axis = axes[0] 86 | for match_key in axis.matches(obj, tree_node.keys()): 87 | yield from self._query(tree_node[match_key], objs[1:], axes[1:]) 88 | 89 | def _align_with_axes( 90 | self, args: Sequence[S], kw: dict[str, S] 91 | ) -> Sequence[S | None]: 92 | """Create a list matching up all args and kwargs with their 93 | corresponding axes, in order, using ``None`` as a placeholder for 94 | skipped axes.""" 95 | axes_dict = self._axes_dict 96 | aligned: list[S | None] = [None for _ in range(len(axes_dict))] 97 | 98 | args_len = len(args) 99 | if args_len + len(kw) > len(aligned): 100 | raise ValueError("Cannot have more arguments than axes.") 101 | 102 | for i, arg in enumerate(args): 103 | aligned[i] = arg 104 | 105 | for k, v in kw.items(): 106 | i_axis = axes_dict.get(k, None) 107 | if i_axis is None: 108 | raise ValueError(f"No axis with name: {k}") 109 | 110 | i, _axis = i_axis 111 | if aligned[i] is not None: 112 | raise ValueError( 113 | "Axis defined twice between positional and keyword arguments" 114 | ) 115 | 116 | aligned[i] = v 117 | 118 | # Trim empty tail nodes for faster look-ups 119 | while aligned and aligned[-1] is None: 120 | del aligned[-1] 121 | 122 | return aligned 123 | 124 | 125 | class _TreeNode(Generic[T], Dict[Any, Any]): 126 | target: T | None = None 127 | 128 | def __str__(self) -> str: 129 | return f"" 130 | 131 | 132 | class SimpleAxis: 133 | """A simple axis where the key into the axis is the same as the object to 134 | be matched (aka the identity axis). This axis behaves just like a 135 | dictionary. You might use this axis if you are interested in registering 136 | something by name, where you're registering an object with the string that 137 | is the name and then using the name to look it up again later. 138 | 139 | Subclasses can override the ``get_keys`` method for implementing 140 | arbitrary axes. 141 | """ 142 | 143 | def matches( 144 | self, obj: object, keys: KeysView[object | None] 145 | ) -> Generator[object, None, None]: 146 | for key in [obj]: 147 | if key in keys: 148 | yield obj 149 | 150 | 151 | class TypeAxis: 152 | """An axis which matches the class and super classes of an object in method 153 | resolution order.""" 154 | 155 | def matches( 156 | self, obj: object, keys: KeysView[type | None] 157 | ) -> Generator[type, None, None]: 158 | for key in type(obj).mro(): 159 | if key in keys: 160 | yield key 161 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - '*.md' 9 | pull_request: 10 | branches: 11 | - main 12 | release: 13 | types: [published] 14 | workflow_dispatch: 15 | 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.ref }} 18 | cancel-in-progress: true 19 | 20 | env: 21 | python-version: '3.13' 22 | 23 | permissions: 24 | contents: read 25 | 26 | jobs: 27 | lint: 28 | runs-on: ubuntu-24.04 29 | permissions: 30 | contents: read 31 | steps: 32 | - name: Harden Runner 33 | uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 34 | with: 35 | disable-sudo: true 36 | egress-policy: block 37 | allowed-endpoints: > 38 | files.pythonhosted.org:443 39 | pypi.org:443 40 | github.com:443 41 | api.github.com:443 42 | *.githubusercontent.com:443 43 | ghcr.io 44 | 45 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 46 | with: 47 | ref: ${{ github.event.pull_request.head.sha }} 48 | - name: Set up Python 49 | uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 50 | with: 51 | python-version: ${{ env.python-version }} 52 | - name: Lint with Pre-commit 53 | run: pipx run --pip-args='--constraint=.github/github-requirements.txt' pre-commit run --all-files 54 | - name: Check REUSE compliance 55 | run: pip install reuse && python -m reuse lint 56 | - name: Check Poetry lock file integrity 57 | run: | 58 | python${{ env.python-version }} -m pip install --constraint=.github/github-requirements.txt poetry 59 | poetry config virtualenvs.in-project true 60 | poetry check 61 | 62 | build: 63 | needs: lint 64 | runs-on: ubuntu-24.04 65 | permissions: 66 | contents: write 67 | strategy: 68 | max-parallel: 4 69 | matrix: 70 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 71 | name: build (python ${{ matrix.python-version }}) 72 | outputs: 73 | targz: generic-${{ steps.meta.outputs.version }}.tar.gz 74 | wheel: generic-${{ steps.meta.outputs.version }}-py3-none-any.whl 75 | steps: 76 | - name: Harden Runner 77 | uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 78 | with: 79 | disable-sudo: true 80 | egress-policy: block 81 | allowed-endpoints: > 82 | qlty.sh/d/coverage:443 83 | qlty-releases.s3.amazonaws.com:443 84 | files.pythonhosted.org:443 85 | pypi.org:443 86 | github.com:443 87 | *.githubusercontent.com:443 88 | ghcr.io 89 | keys.openpgp.org:443 90 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 91 | - name: Set up Python ${{ matrix.python-version }} 92 | uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 93 | with: 94 | python-version: ${{ matrix.python-version }} 95 | allow-prereleases: true 96 | - name: Install Poetry 97 | run: | 98 | python${{ matrix.python-version }} -m pip install --constraint=.github/github-requirements.txt poetry 99 | poetry config virtualenvs.in-project true 100 | - name: Collect Project Data 101 | id: meta 102 | run: .github/scripts/metadata.sh 103 | - name: Install dependencies 104 | run: poetry install --no-interaction 105 | - name: Test 106 | run: | 107 | poetry run pytest --cov=generic 108 | poetry run coverage lcov 109 | - name: Upload Code Coverage to Qlty.sh 110 | uses: qltysh/qlty-action/coverage@a19242102d17e497f437d7466aa01b528537e899 # v2.2.0 111 | with: 112 | token: ${{ secrets.QLTY_COVERAGE_TOKEN }} 113 | files: coverage.lcov 114 | - name: Create Source Dist and Wheel 115 | if: ${{ matrix.python-version == env.python-version }} 116 | run: poetry build 117 | - name: Upload generic-${{ steps.meta.outputs.version }}.tar.gz 118 | if: ${{ matrix.python-version == env.python-version }} 119 | uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 120 | with: 121 | name: generic-${{ steps.meta.outputs.version }}.tar.gz 122 | path: dist/generic-${{ steps.meta.outputs.version }}.tar.gz 123 | - name: Upload generic-${{ steps.meta.outputs.version }}-py3-none-any.whl 124 | if: ${{ matrix.python-version == env.python-version }} 125 | uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 126 | with: 127 | name: generic-${{ steps.meta.outputs.version }}-py3-none-any.whl 128 | path: dist/generic-${{ steps.meta.outputs.version }}-py3-none-any.whl 129 | 130 | publish-to-pypi: 131 | name: Publish to PyPI (release only) 132 | needs: build 133 | runs-on: ubuntu-24.04 134 | permissions: 135 | id-token: write 136 | if: ${{ github.event_name == 'release' }} 137 | steps: 138 | - name: Harden Runner 139 | uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 140 | with: 141 | egress-policy: audit 142 | 143 | - name: Download tar.gz 144 | uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 145 | with: 146 | name: ${{ needs.build.outputs.targz }} 147 | path: dist 148 | - name: Download wheel 149 | uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 150 | with: 151 | name: ${{ needs.build.outputs.wheel }} 152 | path: dist 153 | - name: Publish package distributions to PyPI 154 | uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1 155 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = -a 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/generic.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/generic.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/generic" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/generic" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ### We :heart: our Contributors! 4 | 5 | First off, thank you for considering contributing to Generic. It's people like 6 | you that make Generic such a great library. 7 | 8 | ### Why a Guideline? 9 | 10 | Following these guidelines helps to communicate that you respect the time of 11 | the developers managing and developing this open source project. In return, 12 | they should reciprocate that respect in addressing your issue, assessing 13 | changes, and helping you finalize your pull requests. 14 | 15 | ### What we are Looking For 16 | 17 | Generic is an open source project and we love to receive contributions from our 18 | community — you! There are many ways to contribute, from writing tutorials or 19 | blog posts, improving the documentation, submitting bug reports and feature 20 | requests or writing code which can be incorporated into Generic itself. 21 | 22 | ### What we are not Looking For 23 | 24 | Please, don't use the issue tracker for support questions. Check whether the 25 | your question can be answered on the 26 | [Gaphor Gitter Channel](https://gitter.im/gaphor/Lobby). 27 | 28 | # Ground Rules 29 | ### Responsibilities 30 | 31 | * Ensure cross-platform compatibility for every change that's accepted. 32 | Windows, Mac, Debian & Ubuntu Linux. 33 | * Ensure that code that goes into core meets all requirements in this 34 | [PR Review Checklist](https://gist.github.com/audreyr/4feef90445b9680475f2). 35 | * Create issues for any major changes and enhancements that you wish to make. 36 | * Discuss things transparently and get community feedback. 37 | * Don't add any classes to the codebase unless absolutely needed. Err on the side of using 38 | functions. 39 | * Keep feature versions as small as possible, preferably one new feature per 40 | version. 41 | * Be welcoming to newcomers and encourage diverse new contributors from all 42 | backgrounds. See the 43 | [Python Community Code of Conduct](https://www.python.org/psf/codeofconduct/). 44 | 45 | # Your First Contribution 46 | 47 | Unsure where to begin contributing to Generic? You can start by looking through 48 | these `first-timers-only` and `up-for-grabs` issues: 49 | 50 | * [First-timers-only issues](https://github.com/gaphor/generic/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+label%3Afirst-timers-only) - 51 | issues which should only require a few lines of code, and a test or two. 52 | * [Up-for-grabs issues](https://github.com/gaphor/generic/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+label%3Aup-for-grabs) - 53 | issues which should be a bit more involved than `first-timers-only` issues. 54 | 55 | ### Is This Your First Open Source Contribution? 56 | 57 | Working on your first Pull Request? You can learn how from this *free* series, 58 | [How to Contribute to an Open Source Project on 59 | GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github). 60 | 61 | At this point, you're ready to make your changes! Feel free to ask for help; 62 | everyone is a beginner at first :smile_cat: 63 | 64 | If a maintainer asks you to "rebase" your PR, they're saying that a lot of code 65 | has changed, and that you need to update your branch so it's easier to merge. 66 | 67 | # Getting Started 68 | 69 | For something that is bigger than a one or two line fix: 70 | 71 | 1. Create your own fork of the code 72 | 2. Install all development dependencies using: 73 | ``` 74 | $ poetry install 75 | $ pre-commit install 76 | ``` 77 | If you haven't used poetry before, just run `pip install poetry`, and then run the commands above, it will do the correct thing. 78 | 79 | 3. Add tests for your changes, run the tests with `pytest`. 80 | 4. Do the changes in your fork. 81 | 5. If you like the change and think the project could use it: 82 | * Be sure you have the pre-commit hook installed above, it will ensure that 83 | [Ruff](https://docs.astral.sh/ruff/) is automatically run on any changes for 84 | consistent code formatting. 85 | * [Sign](https://help.github.com/articles/signing-commits/) your commits. 86 | * Note the Generic Code of Conduct. 87 | * Create a pull request. 88 | 89 | 90 | Small contributions such as fixing spelling errors, where the content is small 91 | enough to not be considered intellectual property, can be submitted by a 92 | contributor as a patch, without signing your commit. 93 | 94 | As a rule of thumb, changes are obvious fixes if they do not introduce any new 95 | functionality or creative thinking. As long as the change does not affect 96 | functionality, some likely examples include the following: 97 | * Spelling / grammar fixes 98 | * Typo correction, white space and formatting changes 99 | * Comment clean up 100 | * Bug fixes that change default return values or error codes stored in constants 101 | * Adding logging messages or debugging output 102 | * Changes to ‘metadata’ files like pyproject.toml, .gitignore, build scripts, etc. 103 | * Moving source files from one directory or package to another 104 | 105 | # How to Report a Bug 106 | If you find a security vulnerability, do NOT open an issue. Email dan@yeaw.me instead. 107 | 108 | When filing an issue, make sure to answer the questions in the issue template. 109 | 110 | 1. What version are you using? 111 | 2. What operating system are you using? 112 | 3. What did you do? 113 | 4. What did you expect to see? 114 | 5. What did you see instead? 115 | 116 | # How to Suggest a Feature or Enhancement 117 | If you find yourself wishing for a feature that doesn't exist in Generic, 118 | you are probably not alone. There are bound to be others out there with similar 119 | needs. Many of the features that Generic has today have been added 120 | because our users saw the need. Open an issue on our issues list on GitHub 121 | which describes the feature you would like to see, why you need it, and how it 122 | should work. 123 | 124 | # Code review process 125 | 126 | The core team looks at Pull Requests on a regular basis, you should expect a 127 | response within a week. After feedback has been given we expect responses 128 | within two weeks. After two weeks we may close the pull request if it isn't 129 | showing any activity. 130 | 131 | 132 | # Community 133 | You can chat with the Generic community on gitter: https://gitter.im/Gaphor/Lobby. 134 | -------------------------------------------------------------------------------- /docs/multidispatching.rst: -------------------------------------------------------------------------------- 1 | Multidispatching 2 | ================ 3 | 4 | Multidispatching allows you to define methods and functions which should behave 5 | differently based on arguments' types without cluttering ``if-elif-else`` chains 6 | and ``isinstance`` calls. 7 | 8 | All you need is inside ``generic.multidispatch`` module. See examples below to 9 | learn how to use it to define multifunctions and multimethods. 10 | 11 | First the basics: 12 | 13 | >>> class Cat: pass 14 | >>> class Dog: pass 15 | >>> class Duck: pass 16 | 17 | Multifunctions 18 | -------------- 19 | 20 | Suppose we want to define a function which behaves differently based on 21 | arguments' types. The naive solution is to inspect argument types with 22 | ``isinstance`` function calls but generic provides us with ``@multidispatch`` 23 | decorator which can easily reduce the amount of boilerplate and provide 24 | desired level of extensibility:: 25 | 26 | >>> from generic.multidispatch import multidispatch 27 | 28 | >>> @multidispatch(Dog) 29 | ... def sound(o): 30 | ... print("Woof!") 31 | 32 | >>> @sound.register(Cat) 33 | ... def cat_sound(o): 34 | ... print("Meow!") 35 | 36 | Each separate definition of ``sound`` function works for different argument 37 | types, we will call each such definition *a multifunction case* or simply *a 38 | case*. We can test if our ``sound`` multifunction works as expected:: 39 | 40 | >>> sound(Dog()) 41 | Woof! 42 | >>> sound(Cat()) 43 | Meow! 44 | >>> sound(Duck()) # doctest: +ELLIPSIS 45 | Traceback (most recent call last): 46 | ... 47 | TypeError: No available rule found for ... 48 | 49 | The main advantage of using multifunctions over single function with a bunch of 50 | ``isinstance`` checks is extensibility -- you can add more cases for other types 51 | even in separate module:: 52 | 53 | >>> @sound.register(Duck) 54 | ... def duck_sound(o): 55 | ... print("Quack!") 56 | 57 | When behaviour of multifunction depends on some argument we will say that this 58 | multifunction *dispatches* on this argument. 59 | 60 | Multifunctions of several arguments 61 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 62 | 63 | You can also define multifunctions of several arguments and even decide on which 64 | of first arguments you want to dispatch. For example the following function will 65 | only dispatch on its first argument while requiring both of them:: 66 | 67 | >>> @multidispatch(Dog) 68 | ... def walk(dog, meters): 69 | ... print("Dog walks for %d meters" % meters) 70 | 71 | But sometimes you want multifunctions to dispatch on more than one argument, 72 | then you just have to provide several arguments to ``multidispatch`` decorator 73 | and to subsequent ``when`` decorators:: 74 | 75 | >>> @multidispatch(Dog, Cat) 76 | ... def chases(dog, cat): 77 | ... return True 78 | 79 | >>> @chases.register(Dog, Dog) 80 | ... def chases_dog_dog(dog1, dog2): 81 | ... return None 82 | 83 | >>> @chases.register(Cat, Dog) 84 | ... def chases_cat_dog(cat, dog): 85 | ... return False 86 | 87 | You can have any number of arguments to dispatch on but they should be all 88 | positional, keyword arguments are allowed for multifunctions only if they're not 89 | used for dispatch. 90 | 91 | Multimethods 92 | ------------ 93 | 94 | Another functionality provided by ``generic.multimethod`` module are 95 | *multimethods*. Multimethods are similar to multifunctions except they are... 96 | methods. Technically the main and the only difference between multifunctions and 97 | multimethods is the latter is also dispatch on ``self`` argument. 98 | 99 | Implementing multimethods is similar to implementing multifunctions, you just 100 | have to decorate your methods with ``multimethod`` decorator instead of 101 | ``multidispatch``. But there's some issue with how Python's classes works which 102 | forces us to use also ``has_multimethods`` class decorator:: 103 | 104 | >>> class Vegetable: pass 105 | >>> class Meat: pass 106 | 107 | >>> from generic.multimethod import multimethod, has_multimethods 108 | 109 | >>> @has_multimethods 110 | ... class Animal(object): 111 | ... 112 | ... @multimethod(Vegetable) 113 | ... def can_eat(self, food): 114 | ... return True 115 | ... 116 | ... @can_eat.register(Meat) 117 | ... def can_eat(self, food): 118 | ... return False 119 | register rule (, ) 120 | register rule (, ) 121 | 122 | This would work like this:: 123 | 124 | >>> animal = Animal() 125 | >>> animal.can_eat(Vegetable()) 126 | True 127 | >>> animal.can_eat(Meat()) 128 | False 129 | 130 | So far we haven't seen any differences between multifunctions and multimethods 131 | but as it have already been said there's one -- multimethods use ``self`` 132 | argument for dispatch. We can see that if we would subclass our ``Animal`` class 133 | and override ``can_eat`` method definition:: 134 | 135 | >>> @has_multimethods 136 | ... class Predator(Animal): 137 | ... @Animal.can_eat.register(Meat) 138 | ... def can_eat(self, food): 139 | ... return True 140 | register rule (, ) 141 | 142 | This will override ``can_eat`` on ``Predator`` instances but *only* for the case 143 | for ``Meat`` argument, case for the ``Vegetable`` is not overridden, so class 144 | inherits it from ``Animal``:: 145 | 146 | >>> predator = Predator() 147 | >>> predator.can_eat(Vegetable()) 148 | True 149 | >>> predator.can_eat(Meat()) 150 | True 151 | 152 | The only thing to care is you should not forget to include ``@has_multimethods`` 153 | decorator on classes which define or override multimethods. 154 | 155 | You can also provide a "catch-all" case for multimethod using ``otherwise`` 156 | decorator just like in example for multifunctions. 157 | 158 | Providing "catch-all" case 159 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 160 | 161 | There should be an analog to ``else`` statement -- a case which is used when no 162 | matching case is found, we will call such case *a catch-all case*, here is how 163 | you can define it using ``otherwise`` decorator:: 164 | 165 | >>> @has_multimethods 166 | ... class Animal(object): 167 | ... 168 | ... @multimethod(Vegetable) 169 | ... def can_eat(self, food): 170 | ... return True 171 | ... 172 | ... @can_eat.register(Meat) 173 | ... def can_eat(self, food): 174 | ... return False 175 | ... 176 | ... @can_eat.otherwise 177 | ... def can_eat(self, food): 178 | ... return "?" 179 | register rule (, ) 180 | register rule (, ) 181 | register rule (, ) 182 | 183 | >>> Animal().can_eat(1) 184 | '?' 185 | 186 | You can try calling ``sound`` with whatever argument type you wish, it will 187 | never fall with ``TypeError`` anymore. 188 | 189 | API reference 190 | ------------- 191 | 192 | .. autofunction:: generic.multidispatch.multidispatch 193 | 194 | .. autofunction:: generic.multimethod.multimethod 195 | 196 | .. autofunction:: generic.multimethod.has_multimethods 197 | 198 | .. autoclass:: generic.multidispatch.FunctionDispatcher 199 | :members: register 200 | 201 | .. autoclass:: generic.multimethod.MethodDispatcher 202 | :members: register, otherwise 203 | -------------------------------------------------------------------------------- /tests/test_multidispatch.py: -------------------------------------------------------------------------------- 1 | """Tests for :module:`generic.multidispatch`.""" 2 | 3 | import logging 4 | from inspect import FullArgSpec 5 | 6 | import pytest 7 | 8 | from generic.multidispatch import FunctionDispatcher, multidispatch 9 | 10 | 11 | def create_dispatcher( 12 | params_arity, args=None, varargs=None, keywords=None, defaults=None 13 | ) -> FunctionDispatcher: 14 | return FunctionDispatcher( 15 | FullArgSpec( 16 | args=args, 17 | varargs=varargs, 18 | varkw=keywords, 19 | defaults=defaults, 20 | kwonlyargs=[], 21 | kwonlydefaults={}, 22 | annotations={}, 23 | ), 24 | params_arity, 25 | ) 26 | 27 | 28 | def test_one_argument(): 29 | dispatcher = create_dispatcher(1, args=["x"]) 30 | 31 | dispatcher.register_rule(lambda x: x + 1, int) 32 | assert dispatcher(1) == 2 33 | with pytest.raises(TypeError): 34 | dispatcher("s") 35 | 36 | dispatcher.register_rule(lambda x: f"{x}1", str) 37 | assert dispatcher(1) == 2 38 | assert dispatcher("1") == "11" 39 | with pytest.raises(TypeError): 40 | dispatcher(()) 41 | 42 | 43 | def test_two_arguments(): 44 | dispatcher = create_dispatcher(2, args=["x", "y"]) 45 | 46 | dispatcher.register_rule(lambda x, y: x + y + 1, int, int) 47 | assert dispatcher(1, 2) == 4 48 | with pytest.raises(TypeError): 49 | dispatcher("s", "ss") 50 | with pytest.raises(TypeError): 51 | dispatcher(1, "ss") 52 | with pytest.raises(TypeError): 53 | dispatcher("s", 2) 54 | 55 | dispatcher.register_rule(lambda x, y: x + y + "1", str, str) 56 | assert dispatcher(1, 2) == 4 57 | assert dispatcher("1", "2") == "121" 58 | with pytest.raises(TypeError): 59 | dispatcher("1", 1) 60 | with pytest.raises(TypeError): 61 | dispatcher(1, "1") 62 | 63 | dispatcher.register_rule(lambda x, y: str(x) + y + "1", int, str) 64 | assert dispatcher(1, 2) == 4 65 | assert dispatcher("1", "2") == "121" 66 | assert dispatcher(1, "2") == "121" 67 | with pytest.raises(TypeError): 68 | dispatcher("1", 1) 69 | 70 | 71 | def test_bottom_rule(): 72 | dispatcher = create_dispatcher(1, args=["x"]) 73 | 74 | dispatcher.register_rule(lambda x: x, object) 75 | assert dispatcher(1) == 1 76 | assert dispatcher("1") == "1" 77 | assert dispatcher([1]) == [1] 78 | assert dispatcher((1,)) == (1,) 79 | 80 | 81 | def test_subtype_evaluation(): 82 | class Super: 83 | pass 84 | 85 | class Sub(Super): 86 | pass 87 | 88 | dispatcher = create_dispatcher(1, args=["x"]) 89 | 90 | dispatcher.register_rule(lambda x: x, Super) 91 | o_super = Super() 92 | assert dispatcher(o_super) == o_super 93 | o_sub = Sub() 94 | assert dispatcher(o_sub) == o_sub 95 | with pytest.raises(TypeError): 96 | dispatcher(object()) 97 | 98 | dispatcher.register_rule(lambda x: (x, x), Sub) 99 | o_super = Super() 100 | assert dispatcher(o_super) == o_super 101 | o_sub = Sub() 102 | assert dispatcher(o_sub) == (o_sub, o_sub) 103 | 104 | 105 | def test_subtype_and_none_evaluation(): 106 | class Super: 107 | pass 108 | 109 | class Sub(Super): 110 | pass 111 | 112 | dispatcher = create_dispatcher(2, args=["x", "y"]) 113 | 114 | dispatcher.register_rule(lambda x, y: (x, y), Super, None) 115 | dispatcher.register_rule(lambda x, y: x == y, Sub, Sub) 116 | 117 | o_super = Super() 118 | assert dispatcher(o_super, None) == (o_super, None) 119 | o_sub = Sub() 120 | assert dispatcher(o_sub, None) == (o_sub, None) 121 | with pytest.raises(TypeError): 122 | dispatcher(object()) 123 | 124 | 125 | def test_register_rule_with_wrong_arity(): 126 | dispatcher = create_dispatcher(1, args=["x"]) 127 | dispatcher.register_rule(lambda x: x, int) 128 | with pytest.raises(TypeError): 129 | dispatcher.register_rule(lambda x, y: x, str) 130 | 131 | 132 | def test_register_rule_with_different_arg_names(): 133 | dispatcher = create_dispatcher(1, args=["x"]) 134 | dispatcher.register_rule(lambda y: y, int) 135 | assert dispatcher(1) == 1 136 | 137 | 138 | def test_dispatching_with_varargs(): 139 | dispatcher = create_dispatcher(1, args=["x"], varargs="va") 140 | dispatcher.register_rule(lambda x, *va: x, int) 141 | assert dispatcher(1) == 1 142 | with pytest.raises(TypeError): 143 | dispatcher("1", 2, 3) 144 | 145 | 146 | def test_dispatching_with_varkw(): 147 | dispatcher = create_dispatcher(1, args=["x"], keywords="vk") 148 | dispatcher.register_rule(lambda x, **vk: x, int) 149 | assert dispatcher(1) == 1 150 | with pytest.raises(TypeError): 151 | dispatcher("1", a=1, b=2) 152 | 153 | 154 | def test_dispatching_with_kw(): 155 | dispatcher = create_dispatcher(1, args=["x", "y"], defaults=["vk"]) 156 | dispatcher.register_rule(lambda x, y=1: x, int) 157 | assert dispatcher(1) == 1 158 | with pytest.raises(TypeError): 159 | dispatcher("1", k=1) 160 | 161 | 162 | def test_create_dispatcher_with_pos_args_less_multi_arity(): 163 | with pytest.raises(TypeError): 164 | create_dispatcher(2, args=["x"]) 165 | with pytest.raises(TypeError): 166 | create_dispatcher(2, args=["x", "y"], defaults=["x"]) 167 | 168 | 169 | def test_register_rule_with_wrong_number_types_parameters(): 170 | dispatcher = create_dispatcher(1, args=["x", "y"]) 171 | with pytest.raises(TypeError): 172 | dispatcher.register_rule(lambda x, y: x, int, str) 173 | 174 | 175 | def test_register_rule_with_partial_dispatching(): 176 | dispatcher = create_dispatcher(1, args=["x", "y"]) 177 | dispatcher.register_rule(lambda x, y: x, int) 178 | assert dispatcher(1, 2) == 1 179 | assert dispatcher(1, "2") == 1 180 | with pytest.raises(TypeError): 181 | dispatcher("2", 1) 182 | dispatcher.register_rule(lambda x, y: x, str) 183 | assert dispatcher(1, 2) == 1 184 | assert dispatcher(1, "2") == 1 185 | assert dispatcher("1", "2") == "1" 186 | assert dispatcher("1", 2) == "1" 187 | 188 | 189 | def test_default_dispatcher(): 190 | @multidispatch(int, str) 191 | def func(x, y): 192 | return str(x) + y 193 | 194 | assert func(1, "2") == "12" 195 | with pytest.raises(TypeError): 196 | func(1, 2) 197 | with pytest.raises(TypeError): 198 | func("1", 2) 199 | with pytest.raises(TypeError): 200 | func("1", "2") 201 | 202 | 203 | def test_multiple_functions(): 204 | @multidispatch(int, str) 205 | def func(x, y): 206 | return str(x) + y 207 | 208 | @func.register(str, str) 209 | def _(x, y): 210 | return x + y 211 | 212 | assert func(1, "2") == "12" 213 | assert func("1", "2") == "12" 214 | with pytest.raises(TypeError): 215 | func(1, 2) 216 | with pytest.raises(TypeError): 217 | func("1", 2) 218 | 219 | 220 | def test_default(): 221 | @multidispatch() 222 | def func(x, y): 223 | return x + y 224 | 225 | @func.register(str, str) 226 | def _(x, y): 227 | return y + x 228 | 229 | assert func(1, 1) == 2 230 | assert func("1", "2") == "21" 231 | 232 | 233 | def test_on_classes(): 234 | @multidispatch() 235 | class A: 236 | def __init__(self, a, b): 237 | self.v = a + b 238 | 239 | @A.register(str, str) # type: ignore[attr-defined] 240 | class B: 241 | def __init__(self, a, b): 242 | self.v = b + a 243 | 244 | assert A(1, 1).v == 2 245 | assert A("1", "2").v == "21" 246 | 247 | 248 | def test_logging(caplog): 249 | @multidispatch(str, str) 250 | def func(x, y): 251 | return x + y 252 | 253 | caplog.set_level(logging.DEBUG) 254 | with pytest.raises(TypeError): 255 | func(1, 2) 256 | 257 | rec = caplog.records[0] 258 | 259 | assert rec.levelname == "DEBUG" 260 | assert rec.module == "multidispatch" 261 | assert rec.name == "generic.multidispatch" 262 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # 2 | # generic documentation build configuration file, created by 3 | # sphinx-quickstart on Sat Dec 3 16:59:36 2011. 4 | # 5 | # This file is execfile()d with the current directory set to its containing dir. 6 | # 7 | # Note that not all possible configuration values are present in this 8 | # autogenerated file. 9 | # 10 | # All configuration values have a default; values that are commented out 11 | # serve to show the default. 12 | 13 | import importlib.metadata 14 | import sys 15 | from pathlib import Path 16 | 17 | project_dir = Path(__file__).resolve().parent.parent 18 | sys.path.insert(0, str(project_dir)) 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | 24 | # -- General configuration ----------------------------------------------------- 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | # needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be extensions 30 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 31 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.intersphinx"] 32 | 33 | # Add any paths that contain templates here, relative to this directory. 34 | templates_path = ["_templates"] 35 | 36 | # The suffix of source filenames. 37 | source_suffix = ".rst" 38 | 39 | # The encoding of source files. 40 | # source_encoding = 'utf-8-sig' 41 | 42 | # The master toctree document. 43 | master_doc = "index" 44 | 45 | # General information about the project. 46 | project = "generic" 47 | copyright = "2011, Andrey Popp, 2019, Arjan Molenaar and Dan Yeaw" 48 | 49 | # The version info for the project you're documenting, acts as replacement for 50 | # |version| and |release|, also used in various other places throughout the 51 | # built documents. 52 | version = importlib.metadata.version("generic") 53 | 54 | # The language for content autogenerated by Sphinx. Refer to documentation 55 | # for a list of supported languages. 56 | # language = None 57 | 58 | # There are two options for replacing |today|: either, you set today to some 59 | # non-false value, then it is used: 60 | # today = '' 61 | # Else, today_fmt is used as the format for a strftime call. 62 | # today_fmt = '%B %d, %Y' 63 | 64 | # List of patterns, relative to source directory, that match files and 65 | # directories to ignore when looking for source files. 66 | exclude_patterns = ["_build"] 67 | 68 | # The reST default role (used for this markup: `text`) to use for all documents. 69 | # default_role = None 70 | 71 | # If true, '()' will be appended to :func: etc. cross-reference text. 72 | # add_function_parentheses = True 73 | 74 | # If true, the current module name will be prepended to all description 75 | # unit titles (such as .. function::). 76 | # add_module_names = True 77 | 78 | # If true, sectionauthor and moduleauthor directives will be shown in the 79 | # output. They are ignored by default. 80 | # show_authors = False 81 | 82 | # The name of the Pygments (syntax highlighting) style to use. 83 | # pygments_style = "bw" 84 | 85 | # A list of ignored prefixes for module index sorting. 86 | # modindex_common_prefix = [] 87 | 88 | 89 | # -- Options for HTML output --------------------------------------------------- 90 | 91 | # The theme to use for HTML and HTML Help pages. See the documentation for 92 | # a list of builtin themes. 93 | html_theme = "furo" 94 | html_title = f"Generic v{version}" 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | # html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | # html_theme_path = [pkg_resources.resource_filename("bw_sphinxtheme", "themes")] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | # html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | # html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | # html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | # html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | # html_static_path = ["_static"] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | # html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | # html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | # html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | # html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | # html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | # html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | # html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | # html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | # html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | # html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | # html_use_opensearch = '' 162 | 163 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 164 | # html_file_suffix = None 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = "genericdoc" 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | latex_elements = { # type: ignore[var-annotated] 173 | # The paper size ('letterpaper' or 'a4paper'). 174 | # 'papersize': 'letterpaper', 175 | # The font size ('10pt', '11pt' or '12pt'). 176 | # 'pointsize': '10pt', 177 | # Additional stuff for the LaTeX preamble. 178 | # 'preamble': '', 179 | } 180 | 181 | # Grouping the document tree into LaTeX files. List of tuples 182 | # (source start file, target name, title, author, documentclass [howto/manual]). 183 | latex_documents = [ 184 | ("index", "generic.tex", "generic Documentation", "Andrey Popp", "manual"), 185 | ] 186 | 187 | # The name of an image file (relative to this directory) to place at the top of 188 | # the title page. 189 | # latex_logo = None 190 | 191 | # For "manual" documents, if this is true, then toplevel headings are parts, 192 | # not chapters. 193 | # latex_use_parts = False 194 | 195 | # If true, show page references after internal links. 196 | # latex_show_pagerefs = False 197 | 198 | # If true, show URL addresses after external links. 199 | # latex_show_urls = False 200 | 201 | # Documents to append as an appendix to all manuals. 202 | # latex_appendices = [] 203 | 204 | # If false, no module index is generated. 205 | # latex_domain_indices = True 206 | 207 | 208 | # -- Options for manual page output -------------------------------------------- 209 | 210 | # One entry per manual page. List of tuples 211 | # (source start file, name, description, authors, manual section). 212 | man_pages = [("index", "generic", "generic Documentation", ["Andrey Popp"], 1)] 213 | 214 | # If true, show URL addresses after external links. 215 | # man_show_urls = False 216 | 217 | 218 | # -- Options for Texinfo output ------------------------------------------------ 219 | 220 | # Grouping the document tree into Texinfo files. List of tuples 221 | # (source start file, target name, title, author, 222 | # dir menu entry, description, category) 223 | texinfo_documents = [ 224 | ( 225 | "index", 226 | "generic", 227 | "generic Documentation", 228 | "Andrey Popp", 229 | "generic", 230 | "One line description of project.", 231 | "Miscellaneous", 232 | ), 233 | ] 234 | 235 | # Documents to append as an appendix to all manuals. 236 | # texinfo_appendices = [] 237 | 238 | # If false, no module index is generated. 239 | # texinfo_domain_indices = True 240 | 241 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 242 | # texinfo_show_urls = 'footnote' 243 | 244 | 245 | # Example configuration for intersphinx: refer to the Python standard library. 246 | intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} 247 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "accessible-pygments" 5 | version = "0.0.5" 6 | description = "A collection of accessible pygments styles" 7 | optional = false 8 | python-versions = ">=3.9" 9 | groups = ["docs"] 10 | files = [ 11 | {file = "accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7"}, 12 | {file = "accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872"}, 13 | ] 14 | 15 | [package.dependencies] 16 | pygments = ">=1.5" 17 | 18 | [package.extras] 19 | dev = ["pillow", "pkginfo (>=1.10)", "playwright", "pre-commit", "setuptools", "twine (>=5.0)"] 20 | tests = ["hypothesis", "pytest"] 21 | 22 | [[package]] 23 | name = "alabaster" 24 | version = "0.7.16" 25 | description = "A light, configurable Sphinx theme" 26 | optional = false 27 | python-versions = ">=3.9" 28 | groups = ["docs"] 29 | files = [ 30 | {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, 31 | {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, 32 | ] 33 | 34 | [[package]] 35 | name = "babel" 36 | version = "2.17.0" 37 | description = "Internationalization utilities" 38 | optional = false 39 | python-versions = ">=3.8" 40 | groups = ["docs"] 41 | files = [ 42 | {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, 43 | {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, 44 | ] 45 | 46 | [package.extras] 47 | dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] 48 | 49 | [[package]] 50 | name = "beautifulsoup4" 51 | version = "4.13.5" 52 | description = "Screen-scraping library" 53 | optional = false 54 | python-versions = ">=3.7.0" 55 | groups = ["docs"] 56 | files = [ 57 | {file = "beautifulsoup4-4.13.5-py3-none-any.whl", hash = "sha256:642085eaa22233aceadff9c69651bc51e8bf3f874fb6d7104ece2beb24b47c4a"}, 58 | {file = "beautifulsoup4-4.13.5.tar.gz", hash = "sha256:5e70131382930e7c3de33450a2f54a63d5e4b19386eab43a5b34d594268f3695"}, 59 | ] 60 | 61 | [package.dependencies] 62 | soupsieve = ">1.2" 63 | typing-extensions = ">=4.0.0" 64 | 65 | [package.extras] 66 | cchardet = ["cchardet"] 67 | chardet = ["chardet"] 68 | charset-normalizer = ["charset-normalizer"] 69 | html5lib = ["html5lib"] 70 | lxml = ["lxml"] 71 | 72 | [[package]] 73 | name = "certifi" 74 | version = "2025.8.3" 75 | description = "Python package for providing Mozilla's CA Bundle." 76 | optional = false 77 | python-versions = ">=3.7" 78 | groups = ["docs"] 79 | files = [ 80 | {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, 81 | {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, 82 | ] 83 | 84 | [[package]] 85 | name = "charset-normalizer" 86 | version = "3.4.3" 87 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 88 | optional = false 89 | python-versions = ">=3.7" 90 | groups = ["docs"] 91 | files = [ 92 | {file = "charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"}, 93 | {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"}, 94 | {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601"}, 95 | {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c"}, 96 | {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2"}, 97 | {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0"}, 98 | {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0"}, 99 | {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0"}, 100 | {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a"}, 101 | {file = "charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f"}, 102 | {file = "charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669"}, 103 | {file = "charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b"}, 104 | {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64"}, 105 | {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91"}, 106 | {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f"}, 107 | {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07"}, 108 | {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30"}, 109 | {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14"}, 110 | {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c"}, 111 | {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae"}, 112 | {file = "charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849"}, 113 | {file = "charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c"}, 114 | {file = "charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1"}, 115 | {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884"}, 116 | {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018"}, 117 | {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392"}, 118 | {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f"}, 119 | {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154"}, 120 | {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491"}, 121 | {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93"}, 122 | {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f"}, 123 | {file = "charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37"}, 124 | {file = "charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc"}, 125 | {file = "charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe"}, 126 | {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8"}, 127 | {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9"}, 128 | {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31"}, 129 | {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f"}, 130 | {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927"}, 131 | {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9"}, 132 | {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5"}, 133 | {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc"}, 134 | {file = "charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce"}, 135 | {file = "charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef"}, 136 | {file = "charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15"}, 137 | {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db"}, 138 | {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d"}, 139 | {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096"}, 140 | {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa"}, 141 | {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049"}, 142 | {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0"}, 143 | {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92"}, 144 | {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16"}, 145 | {file = "charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce"}, 146 | {file = "charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c"}, 147 | {file = "charset_normalizer-3.4.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c"}, 148 | {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b"}, 149 | {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4"}, 150 | {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b"}, 151 | {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9"}, 152 | {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb"}, 153 | {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a"}, 154 | {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942"}, 155 | {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b"}, 156 | {file = "charset_normalizer-3.4.3-cp38-cp38-win32.whl", hash = "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557"}, 157 | {file = "charset_normalizer-3.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40"}, 158 | {file = "charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05"}, 159 | {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e"}, 160 | {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99"}, 161 | {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7"}, 162 | {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7"}, 163 | {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19"}, 164 | {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312"}, 165 | {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc"}, 166 | {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34"}, 167 | {file = "charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432"}, 168 | {file = "charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca"}, 169 | {file = "charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a"}, 170 | {file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}, 171 | ] 172 | 173 | [[package]] 174 | name = "colorama" 175 | version = "0.4.6" 176 | description = "Cross-platform colored terminal text." 177 | optional = false 178 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 179 | groups = ["dev", "docs"] 180 | markers = "sys_platform == \"win32\"" 181 | files = [ 182 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 183 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 184 | ] 185 | 186 | [[package]] 187 | name = "coverage" 188 | version = "7.10.6" 189 | description = "Code coverage measurement for Python" 190 | optional = false 191 | python-versions = ">=3.9" 192 | groups = ["dev"] 193 | files = [ 194 | {file = "coverage-7.10.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70e7bfbd57126b5554aa482691145f798d7df77489a177a6bef80de78860a356"}, 195 | {file = "coverage-7.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e41be6f0f19da64af13403e52f2dec38bbc2937af54df8ecef10850ff8d35301"}, 196 | {file = "coverage-7.10.6-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c61fc91ab80b23f5fddbee342d19662f3d3328173229caded831aa0bd7595460"}, 197 | {file = "coverage-7.10.6-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10356fdd33a7cc06e8051413140bbdc6f972137508a3572e3f59f805cd2832fd"}, 198 | {file = "coverage-7.10.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80b1695cf7c5ebe7b44bf2521221b9bb8cdf69b1f24231149a7e3eb1ae5fa2fb"}, 199 | {file = "coverage-7.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2e4c33e6378b9d52d3454bd08847a8651f4ed23ddbb4a0520227bd346382bbc6"}, 200 | {file = "coverage-7.10.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c8a3ec16e34ef980a46f60dc6ad86ec60f763c3f2fa0db6d261e6e754f72e945"}, 201 | {file = "coverage-7.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7d79dabc0a56f5af990cc6da9ad1e40766e82773c075f09cc571e2076fef882e"}, 202 | {file = "coverage-7.10.6-cp310-cp310-win32.whl", hash = "sha256:86b9b59f2b16e981906e9d6383eb6446d5b46c278460ae2c36487667717eccf1"}, 203 | {file = "coverage-7.10.6-cp310-cp310-win_amd64.whl", hash = "sha256:e132b9152749bd33534e5bd8565c7576f135f157b4029b975e15ee184325f528"}, 204 | {file = "coverage-7.10.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c706db3cabb7ceef779de68270150665e710b46d56372455cd741184f3868d8f"}, 205 | {file = "coverage-7.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e0c38dc289e0508ef68ec95834cb5d2e96fdbe792eaccaa1bccac3966bbadcc"}, 206 | {file = "coverage-7.10.6-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:752a3005a1ded28f2f3a6e8787e24f28d6abe176ca64677bcd8d53d6fe2ec08a"}, 207 | {file = "coverage-7.10.6-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:689920ecfd60f992cafca4f5477d55720466ad2c7fa29bb56ac8d44a1ac2b47a"}, 208 | {file = "coverage-7.10.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec98435796d2624d6905820a42f82149ee9fc4f2d45c2c5bc5a44481cc50db62"}, 209 | {file = "coverage-7.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b37201ce4a458c7a758ecc4efa92fa8ed783c66e0fa3c42ae19fc454a0792153"}, 210 | {file = "coverage-7.10.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2904271c80898663c810a6b067920a61dd8d38341244a3605bd31ab55250dad5"}, 211 | {file = "coverage-7.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5aea98383463d6e1fa4e95416d8de66f2d0cb588774ee20ae1b28df826bcb619"}, 212 | {file = "coverage-7.10.6-cp311-cp311-win32.whl", hash = "sha256:e3fb1fa01d3598002777dd259c0c2e6d9d5e10e7222976fc8e03992f972a2cba"}, 213 | {file = "coverage-7.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:f35ed9d945bece26553d5b4c8630453169672bea0050a564456eb88bdffd927e"}, 214 | {file = "coverage-7.10.6-cp311-cp311-win_arm64.whl", hash = "sha256:99e1a305c7765631d74b98bf7dbf54eeea931f975e80f115437d23848ee8c27c"}, 215 | {file = "coverage-7.10.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b2dd6059938063a2c9fee1af729d4f2af28fd1a545e9b7652861f0d752ebcea"}, 216 | {file = "coverage-7.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:388d80e56191bf846c485c14ae2bc8898aa3124d9d35903fef7d907780477634"}, 217 | {file = "coverage-7.10.6-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:90cb5b1a4670662719591aa92d0095bb41714970c0b065b02a2610172dbf0af6"}, 218 | {file = "coverage-7.10.6-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:961834e2f2b863a0e14260a9a273aff07ff7818ab6e66d2addf5628590c628f9"}, 219 | {file = "coverage-7.10.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf9a19f5012dab774628491659646335b1928cfc931bf8d97b0d5918dd58033c"}, 220 | {file = "coverage-7.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99c4283e2a0e147b9c9cc6bc9c96124de9419d6044837e9799763a0e29a7321a"}, 221 | {file = "coverage-7.10.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:282b1b20f45df57cc508c1e033403f02283adfb67d4c9c35a90281d81e5c52c5"}, 222 | {file = "coverage-7.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cdbe264f11afd69841bd8c0d83ca10b5b32853263ee62e6ac6a0ab63895f972"}, 223 | {file = "coverage-7.10.6-cp312-cp312-win32.whl", hash = "sha256:a517feaf3a0a3eca1ee985d8373135cfdedfbba3882a5eab4362bda7c7cf518d"}, 224 | {file = "coverage-7.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:856986eadf41f52b214176d894a7de05331117f6035a28ac0016c0f63d887629"}, 225 | {file = "coverage-7.10.6-cp312-cp312-win_arm64.whl", hash = "sha256:acf36b8268785aad739443fa2780c16260ee3fa09d12b3a70f772ef100939d80"}, 226 | {file = "coverage-7.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ffea0575345e9ee0144dfe5701aa17f3ba546f8c3bb48db62ae101afb740e7d6"}, 227 | {file = "coverage-7.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d91d7317cde40a1c249d6b7382750b7e6d86fad9d8eaf4fa3f8f44cf171e80"}, 228 | {file = "coverage-7.10.6-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e23dd5408fe71a356b41baa82892772a4cefcf758f2ca3383d2aa39e1b7a003"}, 229 | {file = "coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f3f56e4cb573755e96a16501a98bf211f100463d70275759e73f3cbc00d4f27"}, 230 | {file = "coverage-7.10.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db4a1d897bbbe7339946ffa2fe60c10cc81c43fab8b062d3fcb84188688174a4"}, 231 | {file = "coverage-7.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fd7879082953c156d5b13c74aa6cca37f6a6f4747b39538504c3f9c63d043d"}, 232 | {file = "coverage-7.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:28395ca3f71cd103b8c116333fa9db867f3a3e1ad6a084aa3725ae002b6583bc"}, 233 | {file = "coverage-7.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:61c950fc33d29c91b9e18540e1aed7d9f6787cc870a3e4032493bbbe641d12fc"}, 234 | {file = "coverage-7.10.6-cp313-cp313-win32.whl", hash = "sha256:160c00a5e6b6bdf4e5984b0ef21fc860bc94416c41b7df4d63f536d17c38902e"}, 235 | {file = "coverage-7.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:628055297f3e2aa181464c3808402887643405573eb3d9de060d81531fa79d32"}, 236 | {file = "coverage-7.10.6-cp313-cp313-win_arm64.whl", hash = "sha256:df4ec1f8540b0bcbe26ca7dd0f541847cc8a108b35596f9f91f59f0c060bfdd2"}, 237 | {file = "coverage-7.10.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c9a8b7a34a4de3ed987f636f71881cd3b8339f61118b1aa311fbda12741bff0b"}, 238 | {file = "coverage-7.10.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd5af36092430c2b075cee966719898f2ae87b636cefb85a653f1d0ba5d5393"}, 239 | {file = "coverage-7.10.6-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0353b0f0850d49ada66fdd7d0c7cdb0f86b900bb9e367024fd14a60cecc1e27"}, 240 | {file = "coverage-7.10.6-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6b9ae13d5d3e8aeca9ca94198aa7b3ebbc5acfada557d724f2a1f03d2c0b0df"}, 241 | {file = "coverage-7.10.6-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:675824a363cc05781b1527b39dc2587b8984965834a748177ee3c37b64ffeafb"}, 242 | {file = "coverage-7.10.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:692d70ea725f471a547c305f0d0fc6a73480c62fb0da726370c088ab21aed282"}, 243 | {file = "coverage-7.10.6-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:851430a9a361c7a8484a36126d1d0ff8d529d97385eacc8dfdc9bfc8c2d2cbe4"}, 244 | {file = "coverage-7.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d9369a23186d189b2fc95cc08b8160ba242057e887d766864f7adf3c46b2df21"}, 245 | {file = "coverage-7.10.6-cp313-cp313t-win32.whl", hash = "sha256:92be86fcb125e9bda0da7806afd29a3fd33fdf58fba5d60318399adf40bf37d0"}, 246 | {file = "coverage-7.10.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6b3039e2ca459a70c79523d39347d83b73f2f06af5624905eba7ec34d64d80b5"}, 247 | {file = "coverage-7.10.6-cp313-cp313t-win_arm64.whl", hash = "sha256:3fb99d0786fe17b228eab663d16bee2288e8724d26a199c29325aac4b0319b9b"}, 248 | {file = "coverage-7.10.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6008a021907be8c4c02f37cdc3ffb258493bdebfeaf9a839f9e71dfdc47b018e"}, 249 | {file = "coverage-7.10.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5e75e37f23eb144e78940b40395b42f2321951206a4f50e23cfd6e8a198d3ceb"}, 250 | {file = "coverage-7.10.6-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0f7cb359a448e043c576f0da00aa8bfd796a01b06aa610ca453d4dde09cc1034"}, 251 | {file = "coverage-7.10.6-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c68018e4fc4e14b5668f1353b41ccf4bc83ba355f0e1b3836861c6f042d89ac1"}, 252 | {file = "coverage-7.10.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd4b2b0707fc55afa160cd5fc33b27ccbf75ca11d81f4ec9863d5793fc6df56a"}, 253 | {file = "coverage-7.10.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cec13817a651f8804a86e4f79d815b3b28472c910e099e4d5a0e8a3b6a1d4cb"}, 254 | {file = "coverage-7.10.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f2a6a8e06bbda06f78739f40bfb56c45d14eb8249d0f0ea6d4b3d48e1f7c695d"}, 255 | {file = "coverage-7.10.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:081b98395ced0d9bcf60ada7661a0b75f36b78b9d7e39ea0790bb4ed8da14747"}, 256 | {file = "coverage-7.10.6-cp314-cp314-win32.whl", hash = "sha256:6937347c5d7d069ee776b2bf4e1212f912a9f1f141a429c475e6089462fcecc5"}, 257 | {file = "coverage-7.10.6-cp314-cp314-win_amd64.whl", hash = "sha256:adec1d980fa07e60b6ef865f9e5410ba760e4e1d26f60f7e5772c73b9a5b0713"}, 258 | {file = "coverage-7.10.6-cp314-cp314-win_arm64.whl", hash = "sha256:a80f7aef9535442bdcf562e5a0d5a5538ce8abe6bb209cfbf170c462ac2c2a32"}, 259 | {file = "coverage-7.10.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0de434f4fbbe5af4fa7989521c655c8c779afb61c53ab561b64dcee6149e4c65"}, 260 | {file = "coverage-7.10.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6e31b8155150c57e5ac43ccd289d079eb3f825187d7c66e755a055d2c85794c6"}, 261 | {file = "coverage-7.10.6-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:98cede73eb83c31e2118ae8d379c12e3e42736903a8afcca92a7218e1f2903b0"}, 262 | {file = "coverage-7.10.6-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f863c08f4ff6b64fa8045b1e3da480f5374779ef187f07b82e0538c68cb4ff8e"}, 263 | {file = "coverage-7.10.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b38261034fda87be356f2c3f42221fdb4171c3ce7658066ae449241485390d5"}, 264 | {file = "coverage-7.10.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e93b1476b79eae849dc3872faeb0bf7948fd9ea34869590bc16a2a00b9c82a7"}, 265 | {file = "coverage-7.10.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ff8a991f70f4c0cf53088abf1e3886edcc87d53004c7bb94e78650b4d3dac3b5"}, 266 | {file = "coverage-7.10.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ac765b026c9f33044419cbba1da913cfb82cca1b60598ac1c7a5ed6aac4621a0"}, 267 | {file = "coverage-7.10.6-cp314-cp314t-win32.whl", hash = "sha256:441c357d55f4936875636ef2cfb3bee36e466dcf50df9afbd398ce79dba1ebb7"}, 268 | {file = "coverage-7.10.6-cp314-cp314t-win_amd64.whl", hash = "sha256:073711de3181b2e204e4870ac83a7c4853115b42e9cd4d145f2231e12d670930"}, 269 | {file = "coverage-7.10.6-cp314-cp314t-win_arm64.whl", hash = "sha256:137921f2bac5559334ba66122b753db6dc5d1cf01eb7b64eb412bb0d064ef35b"}, 270 | {file = "coverage-7.10.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90558c35af64971d65fbd935c32010f9a2f52776103a259f1dee865fe8259352"}, 271 | {file = "coverage-7.10.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8953746d371e5695405806c46d705a3cd170b9cc2b9f93953ad838f6c1e58612"}, 272 | {file = "coverage-7.10.6-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c83f6afb480eae0313114297d29d7c295670a41c11b274e6bca0c64540c1ce7b"}, 273 | {file = "coverage-7.10.6-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7eb68d356ba0cc158ca535ce1381dbf2037fa8cb5b1ae5ddfc302e7317d04144"}, 274 | {file = "coverage-7.10.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b15a87265e96307482746d86995f4bff282f14b027db75469c446da6127433b"}, 275 | {file = "coverage-7.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fc53ba868875bfbb66ee447d64d6413c2db91fddcfca57025a0e7ab5b07d5862"}, 276 | {file = "coverage-7.10.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:efeda443000aa23f276f4df973cb82beca682fd800bb119d19e80504ffe53ec2"}, 277 | {file = "coverage-7.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9702b59d582ff1e184945d8b501ffdd08d2cee38d93a2206aa5f1365ce0b8d78"}, 278 | {file = "coverage-7.10.6-cp39-cp39-win32.whl", hash = "sha256:2195f8e16ba1a44651ca684db2ea2b2d4b5345da12f07d9c22a395202a05b23c"}, 279 | {file = "coverage-7.10.6-cp39-cp39-win_amd64.whl", hash = "sha256:f32ff80e7ef6a5b5b606ea69a36e97b219cd9dc799bcf2963018a4d8f788cfbf"}, 280 | {file = "coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3"}, 281 | {file = "coverage-7.10.6.tar.gz", hash = "sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90"}, 282 | ] 283 | 284 | [package.dependencies] 285 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 286 | 287 | [package.extras] 288 | toml = ["tomli ; python_full_version <= \"3.11.0a6\""] 289 | 290 | [[package]] 291 | name = "docutils" 292 | version = "0.21.2" 293 | description = "Docutils -- Python Documentation Utilities" 294 | optional = false 295 | python-versions = ">=3.9" 296 | groups = ["docs"] 297 | files = [ 298 | {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, 299 | {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, 300 | ] 301 | 302 | [[package]] 303 | name = "exceptiongroup" 304 | version = "1.3.0" 305 | description = "Backport of PEP 654 (exception groups)" 306 | optional = false 307 | python-versions = ">=3.7" 308 | groups = ["main", "dev"] 309 | markers = "python_version < \"3.11\"" 310 | files = [ 311 | {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, 312 | {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, 313 | ] 314 | 315 | [package.dependencies] 316 | typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} 317 | 318 | [package.extras] 319 | test = ["pytest (>=6)"] 320 | 321 | [[package]] 322 | name = "furo" 323 | version = "2025.12.19" 324 | description = "A clean customisable Sphinx documentation theme." 325 | optional = false 326 | python-versions = ">=3.8" 327 | groups = ["docs"] 328 | files = [ 329 | {file = "furo-2025.12.19-py3-none-any.whl", hash = "sha256:bb0ead5309f9500130665a26bee87693c41ce4dbdff864dbfb6b0dae4673d24f"}, 330 | {file = "furo-2025.12.19.tar.gz", hash = "sha256:188d1f942037d8b37cd3985b955839fea62baa1730087dc29d157677c857e2a7"}, 331 | ] 332 | 333 | [package.dependencies] 334 | accessible-pygments = ">=0.0.5" 335 | beautifulsoup4 = "*" 336 | pygments = ">=2.7" 337 | sphinx = ">=7.0,<10.0" 338 | sphinx-basic-ng = ">=1.0.0.beta2" 339 | 340 | [[package]] 341 | name = "idna" 342 | version = "3.10" 343 | description = "Internationalized Domain Names in Applications (IDNA)" 344 | optional = false 345 | python-versions = ">=3.6" 346 | groups = ["docs"] 347 | files = [ 348 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 349 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 350 | ] 351 | 352 | [package.extras] 353 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 354 | 355 | [[package]] 356 | name = "imagesize" 357 | version = "1.4.1" 358 | description = "Getting image size from png/jpeg/jpeg2000/gif file" 359 | optional = false 360 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 361 | groups = ["docs"] 362 | files = [ 363 | {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, 364 | {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, 365 | ] 366 | 367 | [[package]] 368 | name = "importlib-metadata" 369 | version = "8.7.0" 370 | description = "Read metadata from Python packages" 371 | optional = false 372 | python-versions = ">=3.9" 373 | groups = ["docs"] 374 | markers = "python_version == \"3.9\"" 375 | files = [ 376 | {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, 377 | {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, 378 | ] 379 | 380 | [package.dependencies] 381 | zipp = ">=3.20" 382 | 383 | [package.extras] 384 | check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] 385 | cover = ["pytest-cov"] 386 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 387 | enabler = ["pytest-enabler (>=2.2)"] 388 | perf = ["ipython"] 389 | test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] 390 | type = ["pytest-mypy"] 391 | 392 | [[package]] 393 | name = "iniconfig" 394 | version = "2.1.0" 395 | description = "brain-dead simple config-ini parsing" 396 | optional = false 397 | python-versions = ">=3.8" 398 | groups = ["dev"] 399 | files = [ 400 | {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, 401 | {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, 402 | ] 403 | 404 | [[package]] 405 | name = "jinja2" 406 | version = "3.1.6" 407 | description = "A very fast and expressive template engine." 408 | optional = false 409 | python-versions = ">=3.7" 410 | groups = ["docs"] 411 | files = [ 412 | {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, 413 | {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, 414 | ] 415 | 416 | [package.dependencies] 417 | MarkupSafe = ">=2.0" 418 | 419 | [package.extras] 420 | i18n = ["Babel (>=2.7)"] 421 | 422 | [[package]] 423 | name = "markupsafe" 424 | version = "3.0.2" 425 | description = "Safely add untrusted strings to HTML/XML markup." 426 | optional = false 427 | python-versions = ">=3.9" 428 | groups = ["docs"] 429 | files = [ 430 | {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, 431 | {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, 432 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, 433 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, 434 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, 435 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, 436 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, 437 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, 438 | {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, 439 | {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, 440 | {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, 441 | {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, 442 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, 443 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, 444 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, 445 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, 446 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, 447 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, 448 | {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, 449 | {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, 450 | {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, 451 | {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, 452 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, 453 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, 454 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, 455 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, 456 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, 457 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, 458 | {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, 459 | {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, 460 | {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, 461 | {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, 462 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, 463 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, 464 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, 465 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, 466 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, 467 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, 468 | {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, 469 | {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, 470 | {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, 471 | {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, 472 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, 473 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, 474 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, 475 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, 476 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, 477 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, 478 | {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, 479 | {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, 480 | {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, 481 | {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, 482 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, 483 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, 484 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, 485 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, 486 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, 487 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, 488 | {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, 489 | {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, 490 | {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, 491 | ] 492 | 493 | [[package]] 494 | name = "packaging" 495 | version = "25.0" 496 | description = "Core utilities for Python packages" 497 | optional = false 498 | python-versions = ">=3.8" 499 | groups = ["dev", "docs"] 500 | files = [ 501 | {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, 502 | {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, 503 | ] 504 | 505 | [[package]] 506 | name = "pluggy" 507 | version = "1.6.0" 508 | description = "plugin and hook calling mechanisms for python" 509 | optional = false 510 | python-versions = ">=3.9" 511 | groups = ["dev"] 512 | files = [ 513 | {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, 514 | {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, 515 | ] 516 | 517 | [package.extras] 518 | dev = ["pre-commit", "tox"] 519 | testing = ["coverage", "pytest", "pytest-benchmark"] 520 | 521 | [[package]] 522 | name = "pygments" 523 | version = "2.19.2" 524 | description = "Pygments is a syntax highlighting package written in Python." 525 | optional = false 526 | python-versions = ">=3.8" 527 | groups = ["dev", "docs"] 528 | files = [ 529 | {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, 530 | {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, 531 | ] 532 | 533 | [package.extras] 534 | windows-terminal = ["colorama (>=0.4.6)"] 535 | 536 | [[package]] 537 | name = "pytest" 538 | version = "8.4.2" 539 | description = "pytest: simple powerful testing with Python" 540 | optional = false 541 | python-versions = ">=3.9" 542 | groups = ["dev"] 543 | files = [ 544 | {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, 545 | {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, 546 | ] 547 | 548 | [package.dependencies] 549 | colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} 550 | exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} 551 | iniconfig = ">=1" 552 | packaging = ">=20" 553 | pluggy = ">=1.5,<2" 554 | pygments = ">=2.7.2" 555 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 556 | 557 | [package.extras] 558 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] 559 | 560 | [[package]] 561 | name = "pytest-cov" 562 | version = "7.0.0" 563 | description = "Pytest plugin for measuring coverage." 564 | optional = false 565 | python-versions = ">=3.9" 566 | groups = ["dev"] 567 | files = [ 568 | {file = "pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861"}, 569 | {file = "pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1"}, 570 | ] 571 | 572 | [package.dependencies] 573 | coverage = {version = ">=7.10.6", extras = ["toml"]} 574 | pluggy = ">=1.2" 575 | pytest = ">=7" 576 | 577 | [package.extras] 578 | testing = ["process-tests", "pytest-xdist", "virtualenv"] 579 | 580 | [[package]] 581 | name = "requests" 582 | version = "2.32.5" 583 | description = "Python HTTP for Humans." 584 | optional = false 585 | python-versions = ">=3.9" 586 | groups = ["docs"] 587 | files = [ 588 | {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, 589 | {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, 590 | ] 591 | 592 | [package.dependencies] 593 | certifi = ">=2017.4.17" 594 | charset_normalizer = ">=2,<4" 595 | idna = ">=2.5,<4" 596 | urllib3 = ">=1.21.1,<3" 597 | 598 | [package.extras] 599 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 600 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 601 | 602 | [[package]] 603 | name = "snowballstemmer" 604 | version = "3.0.1" 605 | description = "This package provides 32 stemmers for 30 languages generated from Snowball algorithms." 606 | optional = false 607 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*" 608 | groups = ["docs"] 609 | files = [ 610 | {file = "snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064"}, 611 | {file = "snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895"}, 612 | ] 613 | 614 | [[package]] 615 | name = "soupsieve" 616 | version = "2.8" 617 | description = "A modern CSS selector implementation for Beautiful Soup." 618 | optional = false 619 | python-versions = ">=3.9" 620 | groups = ["docs"] 621 | files = [ 622 | {file = "soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c"}, 623 | {file = "soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f"}, 624 | ] 625 | 626 | [[package]] 627 | name = "sphinx" 628 | version = "7.4.7" 629 | description = "Python documentation generator" 630 | optional = false 631 | python-versions = ">=3.9" 632 | groups = ["docs"] 633 | files = [ 634 | {file = "sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"}, 635 | {file = "sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe"}, 636 | ] 637 | 638 | [package.dependencies] 639 | alabaster = ">=0.7.14,<0.8.0" 640 | babel = ">=2.13" 641 | colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} 642 | docutils = ">=0.20,<0.22" 643 | imagesize = ">=1.3" 644 | importlib-metadata = {version = ">=6.0", markers = "python_version < \"3.10\""} 645 | Jinja2 = ">=3.1" 646 | packaging = ">=23.0" 647 | Pygments = ">=2.17" 648 | requests = ">=2.30.0" 649 | snowballstemmer = ">=2.2" 650 | sphinxcontrib-applehelp = "*" 651 | sphinxcontrib-devhelp = "*" 652 | sphinxcontrib-htmlhelp = ">=2.0.0" 653 | sphinxcontrib-jsmath = "*" 654 | sphinxcontrib-qthelp = "*" 655 | sphinxcontrib-serializinghtml = ">=1.1.9" 656 | tomli = {version = ">=2", markers = "python_version < \"3.11\""} 657 | 658 | [package.extras] 659 | docs = ["sphinxcontrib-websupport"] 660 | lint = ["flake8 (>=6.0)", "importlib-metadata (>=6.0)", "mypy (==1.10.1)", "pytest (>=6.0)", "ruff (==0.5.2)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-docutils (==0.21.0.20240711)", "types-requests (>=2.30.0)"] 661 | test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] 662 | 663 | [[package]] 664 | name = "sphinx-basic-ng" 665 | version = "1.0.0b2" 666 | description = "A modern skeleton for Sphinx themes." 667 | optional = false 668 | python-versions = ">=3.7" 669 | groups = ["docs"] 670 | files = [ 671 | {file = "sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b"}, 672 | {file = "sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9"}, 673 | ] 674 | 675 | [package.dependencies] 676 | sphinx = ">=4.0" 677 | 678 | [package.extras] 679 | docs = ["furo", "ipython", "myst-parser", "sphinx-copybutton", "sphinx-inline-tabs"] 680 | 681 | [[package]] 682 | name = "sphinxcontrib-applehelp" 683 | version = "2.0.0" 684 | description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" 685 | optional = false 686 | python-versions = ">=3.9" 687 | groups = ["docs"] 688 | files = [ 689 | {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, 690 | {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, 691 | ] 692 | 693 | [package.extras] 694 | lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] 695 | standalone = ["Sphinx (>=5)"] 696 | test = ["pytest"] 697 | 698 | [[package]] 699 | name = "sphinxcontrib-devhelp" 700 | version = "2.0.0" 701 | description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" 702 | optional = false 703 | python-versions = ">=3.9" 704 | groups = ["docs"] 705 | files = [ 706 | {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, 707 | {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, 708 | ] 709 | 710 | [package.extras] 711 | lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] 712 | standalone = ["Sphinx (>=5)"] 713 | test = ["pytest"] 714 | 715 | [[package]] 716 | name = "sphinxcontrib-htmlhelp" 717 | version = "2.1.0" 718 | description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" 719 | optional = false 720 | python-versions = ">=3.9" 721 | groups = ["docs"] 722 | files = [ 723 | {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, 724 | {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, 725 | ] 726 | 727 | [package.extras] 728 | lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] 729 | standalone = ["Sphinx (>=5)"] 730 | test = ["html5lib", "pytest"] 731 | 732 | [[package]] 733 | name = "sphinxcontrib-jsmath" 734 | version = "1.0.1" 735 | description = "A sphinx extension which renders display math in HTML via JavaScript" 736 | optional = false 737 | python-versions = ">=3.5" 738 | groups = ["docs"] 739 | files = [ 740 | {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, 741 | {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, 742 | ] 743 | 744 | [package.extras] 745 | test = ["flake8", "mypy", "pytest"] 746 | 747 | [[package]] 748 | name = "sphinxcontrib-qthelp" 749 | version = "2.0.0" 750 | description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" 751 | optional = false 752 | python-versions = ">=3.9" 753 | groups = ["docs"] 754 | files = [ 755 | {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, 756 | {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, 757 | ] 758 | 759 | [package.extras] 760 | lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] 761 | standalone = ["Sphinx (>=5)"] 762 | test = ["defusedxml (>=0.7.1)", "pytest"] 763 | 764 | [[package]] 765 | name = "sphinxcontrib-serializinghtml" 766 | version = "2.0.0" 767 | description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" 768 | optional = false 769 | python-versions = ">=3.9" 770 | groups = ["docs"] 771 | files = [ 772 | {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, 773 | {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, 774 | ] 775 | 776 | [package.extras] 777 | lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] 778 | standalone = ["Sphinx (>=5)"] 779 | test = ["pytest"] 780 | 781 | [[package]] 782 | name = "tomli" 783 | version = "2.2.1" 784 | description = "A lil' TOML parser" 785 | optional = false 786 | python-versions = ">=3.8" 787 | groups = ["dev", "docs"] 788 | files = [ 789 | {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, 790 | {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, 791 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, 792 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, 793 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, 794 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, 795 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, 796 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, 797 | {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, 798 | {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, 799 | {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, 800 | {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, 801 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, 802 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, 803 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, 804 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, 805 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, 806 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, 807 | {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, 808 | {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, 809 | {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, 810 | {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, 811 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, 812 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, 813 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, 814 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, 815 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, 816 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, 817 | {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, 818 | {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, 819 | {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, 820 | {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, 821 | ] 822 | markers = {dev = "python_full_version <= \"3.11.0a6\"", docs = "python_version < \"3.11\""} 823 | 824 | [[package]] 825 | name = "typing-extensions" 826 | version = "4.15.0" 827 | description = "Backported and Experimental Type Hints for Python 3.9+" 828 | optional = false 829 | python-versions = ">=3.9" 830 | groups = ["main", "dev", "docs"] 831 | files = [ 832 | {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, 833 | {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, 834 | ] 835 | markers = {main = "python_version < \"3.11\"", dev = "python_version < \"3.11\""} 836 | 837 | [[package]] 838 | name = "urllib3" 839 | version = "2.6.0" 840 | description = "HTTP library with thread-safe connection pooling, file post, and more." 841 | optional = false 842 | python-versions = ">=3.9" 843 | groups = ["docs"] 844 | files = [ 845 | {file = "urllib3-2.6.0-py3-none-any.whl", hash = "sha256:c90f7a39f716c572c4e3e58509581ebd83f9b59cced005b7db7ad2d22b0db99f"}, 846 | {file = "urllib3-2.6.0.tar.gz", hash = "sha256:cb9bcef5a4b345d5da5d145dc3e30834f58e8018828cbc724d30b4cb7d4d49f1"}, 847 | ] 848 | 849 | [package.extras] 850 | brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] 851 | h2 = ["h2 (>=4,<5)"] 852 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 853 | zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] 854 | 855 | [[package]] 856 | name = "zipp" 857 | version = "3.23.0" 858 | description = "Backport of pathlib-compatible object wrapper for zip files" 859 | optional = false 860 | python-versions = ">=3.9" 861 | groups = ["docs"] 862 | markers = "python_version == \"3.9\"" 863 | files = [ 864 | {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, 865 | {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, 866 | ] 867 | 868 | [package.extras] 869 | check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] 870 | cover = ["pytest-cov"] 871 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 872 | enabler = ["pytest-enabler (>=2.2)"] 873 | test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] 874 | type = ["pytest-mypy"] 875 | 876 | [metadata] 877 | lock-version = "2.1" 878 | python-versions = ">=3.9" 879 | content-hash = "fe031f90801fea64356576d3fe318f3f32bf5e6d2497f0895f2889bba63d588b" 880 | --------------------------------------------------------------------------------