From c6da052ee99c5b505427832a918d02f6a36a7139 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Mon, 24 Feb 2025 11:29:34 +0100 Subject: [PATCH] Adding upstream version 0.15.0. Signed-off-by: Daniel Baumann --- .github/dependabot.yml | 15 + .github/workflows/publish.yml | 47 + .github/workflows/release.yml | 56 + .github/workflows/static.yml | 46 + .github/workflows/test.yml | 92 + .gitignore | 166 ++ .pre-commit-config.yaml | 24 + CHANGELOG.md | 325 ++++ LICENSE | 21 + Makefile | 21 + README.md | 150 ++ poetry.lock | 1816 +++++++++++++++++++ pyproject.toml | 67 + src/scripts/__init__.py | 0 src/scripts/profile_startup.py | 23 + src/scripts/sample_code.py | 20 + src/scripts/screenshot.py | 32 + src/textual_textarea/__init__.py | 15 + src/textual_textarea/__main__.py | 67 + src/textual_textarea/autocomplete.py | 289 +++ src/textual_textarea/cancellable_input.py | 19 + src/textual_textarea/colors.py | 67 + src/textual_textarea/comments.py | 185 ++ src/textual_textarea/containers.py | 54 + src/textual_textarea/error_modal.py | 75 + src/textual_textarea/find_input.py | 77 + src/textual_textarea/goto_input.py | 60 + src/textual_textarea/messages.py | 38 + src/textual_textarea/path_input.py | 127 ++ src/textual_textarea/py.typed | 0 src/textual_textarea/text_editor.py | 1465 +++++++++++++++ stubs/pyperclip/__init__.pyi | 7 + tests/conftest.py | 9 + tests/data/test_open/empty.py | 0 tests/data/test_open/foo.py | 2 + tests/data/test_validator/bar/.gitkeep | 0 tests/data/test_validator/foo/baz.txt | 0 tests/functional_tests/__init__.py | 0 tests/functional_tests/conftest.py | 62 + tests/functional_tests/test_autocomplete.py | 279 +++ tests/functional_tests/test_comments.py | 29 + tests/functional_tests/test_find.py | 143 ++ tests/functional_tests/test_goto.py | 36 + tests/functional_tests/test_open.py | 70 + tests/functional_tests/test_textarea.py | 462 +++++ tests/unit_tests/test_path_validator.py | 84 + textarea.svg | 157 ++ 47 files changed, 6799 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/static.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 src/scripts/__init__.py create mode 100644 src/scripts/profile_startup.py create mode 100644 src/scripts/sample_code.py create mode 100644 src/scripts/screenshot.py create mode 100644 src/textual_textarea/__init__.py create mode 100644 src/textual_textarea/__main__.py create mode 100644 src/textual_textarea/autocomplete.py create mode 100644 src/textual_textarea/cancellable_input.py create mode 100644 src/textual_textarea/colors.py create mode 100644 src/textual_textarea/comments.py create mode 100644 src/textual_textarea/containers.py create mode 100644 src/textual_textarea/error_modal.py create mode 100644 src/textual_textarea/find_input.py create mode 100644 src/textual_textarea/goto_input.py create mode 100644 src/textual_textarea/messages.py create mode 100644 src/textual_textarea/path_input.py create mode 100644 src/textual_textarea/py.typed create mode 100644 src/textual_textarea/text_editor.py create mode 100644 stubs/pyperclip/__init__.pyi create mode 100644 tests/conftest.py create mode 100644 tests/data/test_open/empty.py create mode 100644 tests/data/test_open/foo.py create mode 100644 tests/data/test_validator/bar/.gitkeep create mode 100644 tests/data/test_validator/foo/baz.txt create mode 100644 tests/functional_tests/__init__.py create mode 100644 tests/functional_tests/conftest.py create mode 100644 tests/functional_tests/test_autocomplete.py create mode 100644 tests/functional_tests/test_comments.py create mode 100644 tests/functional_tests/test_find.py create mode 100644 tests/functional_tests/test_goto.py create mode 100644 tests/functional_tests/test_open.py create mode 100644 tests/functional_tests/test_textarea.py create mode 100644 tests/unit_tests/test_path_validator.py create mode 100644 textarea.svg diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..45c383a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "pip" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "monthly" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..c947046 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,47 @@ +name: Build and Publish Package + +on: + pull_request: + branches: + - main + types: + - closed + +jobs: + publish-package: + if: ${{ github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/v') }} + runs-on: ubuntu-latest + + steps: + - name: Check out textarea main branch + uses: actions/checkout@v4 + with: + ref: main + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + version: 1.2.2 + - name: Configure poetry + run: poetry config --no-interaction pypi-token.pypi ${{ secrets.TEXTAREA_PYPI_TOKEN }} + - name: Get textarea Version + id: textarea_version + run: echo "textarea_version=$(poetry version --short)" >> $GITHUB_OUTPUT + - name: Build package + run: poetry build --no-interaction + - name: Publish package to PyPI + run: poetry publish --no-interaction + - name: Create a Github Release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ steps.textarea_version.outputs.textarea_version }} + target_commitish: main + token: ${{ secrets.TEXTAREA_RELEASE_TOKEN }} + body_path: CHANGELOG.md + files: | + LICENSE + dist/*textual_textarea*.whl + dist/*textual_textarea*.tar.gz diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..86d8e3d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,56 @@ +name: Create Release Branch + +on: + workflow_dispatch: + inputs: + newVersion: + description: A version number for this release (e.g., "0.1.0") + required: true + +jobs: + prepare-release: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Check out textarea main branch + uses: actions/checkout@v4 + with: + ref: main + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + version: 1.2.2 + - name: Create release branch + run: | + git checkout -b release/v${{ github.event.inputs.newVersion }} + git push --set-upstream origin release/v${{ github.event.inputs.newVersion }} + - name: Bump version + run: poetry version ${{ github.event.inputs.newVersion }} --no-interaction + - name: Ensure package can be built + run: poetry build --no-interaction + - name: Update CHANGELOG + uses: thomaseizinger/keep-a-changelog-new-release@v3 + with: + version: ${{ github.event.inputs.newVersion }} + - name: Commit Changes + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: Bumps version to ${{ github.event.inputs.newVersion }} + - name: Create pull request into main + uses: thomaseizinger/create-pull-request@1.3.1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + head: release/v${{ github.event.inputs.newVersion }} + base: main + title: v${{ github.event.inputs.newVersion }} + body: > + This PR was automatically generated. It bumps the version number + in pyproject.toml and updates CHANGELOG.md. You may have to close + this PR and reopen it to get the required checks to run. diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml new file mode 100644 index 0000000..0dcd355 --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,46 @@ +name: "Perform Static Analysis" + +on: + pull_request: + +# will cancel previous workflows triggered by the same event and for the same ref for PRs or same SHA otherwise +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.ref || github.sha }} + cancel-in-progress: true + +jobs: + static: + name: Static Analysis - 3.11 + runs-on: ubuntu-latest + steps: + - name: Check out Repo + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + id: setup-python + with: + python-version: "3.11" + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + version: 1.4.2 + virtualenvs-create: true + virtualenvs-in-project: true + installer-parallel: true + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v4 + with: + path: .venv + key: static-venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} + - name: Install python dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --sync --no-interaction --without dev + - name: Run analysis + run: | + source .venv/bin/activate + ruff format . + ruff check . + mypy diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..6394c7c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,92 @@ +name: Test + +on: + push: + branches: [ main ] + pull_request: + +# will cancel previous workflows triggered by the same event and for the same ref for PRs or same SHA otherwise +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.ref || github.sha }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + test-windows: + name: Windows - 3.10 + runs-on: Windows-latest + timeout-minutes: 10 + steps: + - name: Check out Repo + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + version: 1.4.2 + - name: Install python dependencies + run: poetry install --sync --no-interaction --only main,test + - name: Run tests + run: poetry run pytest + + test: + name: ${{ matrix.os }} - ${{ matrix.py }} + runs-on: ${{ matrix.os }}-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + os: + - ubuntu + - MacOs + py: + - "3.13" + - "3.12" + - "3.11" + - "3.10" + - "3.9" + steps: + - name: Check out Repo + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Set up Python ${{ matrix.py }} + id: setup-python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.py }} + - name: Load cached Poetry installation + id: cached-poetry-install + uses: actions/cache@v4 + with: + path: ~/.local + key: poetry-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} + - name: Install Poetry + if: steps.cached-poetry-install.outputs.cache-hit != 'true' + uses: snok/install-poetry@v1 + with: + version: 1.4.2 + virtualenvs-create: true + virtualenvs-in-project: true + installer-parallel: true + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v4 + with: + path: .venv + key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} + - name: Install python dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --sync --no-interaction --only main,test + - name: Run tests + run: | + source .venv/bin/activate + pytest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b30ffe0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,166 @@ +*.db +*.db.wal +*.sql +.profiles +Pipfile + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..8b173b7 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +repos: + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.5.1 + hooks: + - id: ruff-format + - id: ruff + args: [ --fix, --exit-non-zero-on-fix ] + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.10.1 + hooks: + - id: mypy + additional_dependencies: + - textual[syntax]>=0.89.1 + - pytest + args: + - "--disallow-untyped-calls" + - "--disallow-untyped-defs" + - "--disallow-incomplete-defs" + - "--strict-optional" + - "--warn-return-any" + - "--warn-no-return" + - "--warn-redundant-casts" + - "--no-warn-unused-ignores" + - "--allow-untyped-decorators" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..365b864 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,325 @@ +# textual-textarea CHANGELOG + +All notable changes to this project will be documented in this file. + +## [Unreleased] + +## [0.15.0] - 2024-12-19 + +- **Breaking:** changes function return signature `query_syntax_tree`. +- **Breaking:** drops support for Python 3.8. +- **Breaking:** drops support for Pygments themes. +- Adds support for Python 3.13. +- Adds support for Textual app themes, including syntax highlighting using the theme colors. +- Updates syntax highlighting language libraries. + +## [0.14.4] - 2024-10-09 + +- Fixes a crash caused by copying or pasting with the system clipboard in some rare system configurations. + +## [0.14.3] - 2024-10-09 + +- Fixes a crash caused by pressing `ctrl+g` twice ([tconbeer/harlequin#654](https://github.com/tconbeer/harlequin/issues/654)). + +## [0.14.2] - 2024-08-16 + +- Fixes a TypeError raised by Textual >= v0.76 from the goto input validator. + +## [0.14.1] - 2024-08-15 + +- Fixes a bug where uncommenting a line using the `toggle_comment` action would leave behind a space in languages with comment markers that are longer than one character ([tconbeer/harlequin#616](https://github.com/tconbeer/harlequin/issues/616)). + +## [0.14.0] - 2024-07-09 + +- Updates dependencies and removes black in favor of the ruff formatter. + +## [0.13.1] - 2024-06-28 + +- Bumps the pyperclip version for improved clipboard support on Wayland (Linux) ([tconbeer/harlequin#585](https://github.com/tconbeer/harlequin/issues/585)). + +## [0.13.0] - 2024-04-19 + +- Adds a "find" action with ctrl+f and "find next" action with F3. +- Adds "go to line" action with ctrl+g. +- Adds bindings for `ctrl+shift+home` and `ctrl+shift+end` to select while moving to document start/end. +- Uses the new, native undo and redo functionality provided by the Textual TextArea widget. This has some subtly different behavior ([#240](https://github.com/tconbeer/textual-textarea/issues/240). + +## [0.12.0] - 2024-04-17 + +- Show the computed filepath in the Save and Open widgets ([#232](https://github.com/tconbeer/textual-textarea/issues/232) - thank you [@bjornasm](https://github.com/bjornasm)!). +- Fixes a crash from initializing the Error Modal incorrectly. +- Fixes a crash from saving to a path in a non-existent directory. + +## [0.11.3] - 2024-02-09 + +- No longer changes focus on `escape` (regression since 0.11.0) + +## [0.11.2] - 2024-02-08 + +- Adds a `parser` property to the `CodeEditor` class to return the document's tree-sitter parser. + +## [0.11.1] - 2024-02-08 + +- Adds a `syntax_tree` property to the `CodeEditor` class to return the document's tree-sitter syntax tree. + +## [0.11.0] - 2024-02-06 + +- Bumps textual dependency to >=0.48.1 +- Breaking change: Renames the main class from TextArea to TextEditor, to avoid naming conflicts with the built-in TextArea widget (which caused issues with selectors, CSS, etc.). +- Breaking change: Replaces the `cursor` and `selection_anchor` API with `selection`. +- Adds public APIs: `line_count`, `get_line`, `get_text_range`, `copy_to_clipboard`, `pause_blink`, `restart_blink`, `prepare_query`, and `query_syntax_tree`. + +## [0.10.0] - 2024-01-30 + +- Adds a `text` argument when initializing TextArea. +- Improves time to first paint by finding the system clipboard in a thread, instead of blocking mounting. This has an especially large impact on Windows. + +## [0.9.5] - 2023-12-18 + +- Ignore spurious `ctrl+@` keys generated by Windows. (See [textualize/textual#872](https://github.com/Textualize/textual/issues/872)). + +## [0.9.4] - 2023-12-18 + +- No longer show bindings in the footer when the open or save inputs are focussed. + +## [0.9.3] - 2023-12-15 + +- Fixes an issue where very long completions with short prefixes were truncated improperly. + +## [0.9.2] - 2023-12-13 + +- Hides the cursor and autocomplete list when the TextArea widget is not focussed. ([#177](https://github.com/tconbeer/textual-textarea/issues/177)). + +## [0.9.1] - 2023-12-13 + +- Fixes an issue where the autocomplete list was displayed in the wrong location after pressing backspace. + +## [0.9.0] - 2023-12-12 + +- TextArea now provides auto-complete. By default, it will auto-complete paths; to auto-complete words or + members of a namespace, set TextArea.word_completer, TextArea.member_completer, TextArea.path_completer + to a Callable\[[str], list\[tuple[str, str]]]. The callables will receive the current word (or path, etc.) as their + argument and should return a list of completions, where completions are (label, value) pairs. +- The TextArea is now focused when the Open or Save inputs are cancelled. + +## [0.8.0] - 2023-12-06 + +- The TextArea has been completely overhauled. It now uses the built-in TextArea widget under the hood. +- This package now requires Textual >= 0.41.0, as it requires Textual's built-in TextArea widget. +- Double-click a word to select it; triple-click to select the row; quadruple-click to select the whole document. +- Fixes a bug with toggling comments. + +## [0.7.3] - 2023-10-06 + +- The PathInput cursor no longer blinks if the app is run in headless mode (during tests). This only matters to prevent + flaky tests for snapshot testing this widget and downstream apps. + +## [0.7.2] - 2023-10-06 + +- The TextArea cursor no longer blinks if the app is run in headless mode (during tests). This only matters to prevent + flaky tests for snapshot testing this widget and downstream apps. + +## [0.7.1] - 2023-09-22 + +- TextArea now posts a `TextAreaSaved` message if it successfully saves a file. + +## [0.7.0] - 2023-09-20 + +### Features + +- TextArea now posts a `TextAreaClipboardError` message if it cannot access the system clipboard. + +### Fixes + +- TextArea now uses the contents of the system Paste message, instead of relying exclusively on the + system clipboard. This should improve compatibility when Harlequin's host does not share its + clipboard with the user's native system. +- When using the system clipboard, TextArea now initializes the clipboard on mount, resulting in + better performance when copying and pasting. +- `textual_textarea.key_handlers.Cursor` is now exported from the main `textual_textarea` package. +- Cursor position is no longer updated on a right-click. + +## [0.6.0] - 2023-09-08 + +### Features + +- Adds a new public method, `TextArea.insert_text_at_selection(text)`. + +## [0.5.4] - 2023-09-01 + +### Bug Fixes + +- up, down, pageup, and pagedown now better maintain the cursor's x-position when starting with an x-position that is longer than adjacent lines ([#94](https://github.com/tconbeer/textual-textarea/issues/94)). + +## [0.5.3] - 2023-09-01 + +### Bug Fixes + +- Undo is smarter about cursor positions and selections; it no longer saves a new checkpoint for every cursor position. ([#86](https://github.com/tconbeer/textual-textarea/issues/86)). +- Clicks within the container but outside text will still update the cursor ([#93](https://github.com/tconbeer/textual-textarea/issues/93)). +- The cursor is now scrolled into position much faster. + +## [0.5.2] - 2023-08-23 + +### Bug Fixes + +- TextArea now uses the highlight color from the Pygments Style to highlight selected text. + +## [0.5.1] - 2023-08-23 + +### Bug Fixes + +- Fixes a crash caused by shift+delete on a buffer with only one line. + +## [0.5.0] - 2023-08-22 + +### Features + +- Undo any input with ctrl+z; redo with ctrl+y ([#12](https://github.com/tconbeer/textual-textarea/issues/12)). +- shift+delete now deletes the current line if there is no selection ([#77](https://github.com/tconbeer/textual-textarea/issues/77)). + +### Tests + +- Adds basic fuzzing of text and keyboard inputs ([#50](https://github.com/tconbeer/textual-textarea/issues/50)) + +## [0.4.2] - 2023-08-03 + +### Bug Fixes + +- No longer clears selection for more keystrokes (e.g,. ctrl+j) +- Better-maintains selection and cursor position when bulk commenting or uncommenting with ctrl+/ + +## [0.4.1] - 2023-08-03 + +### Features + +- Adds a parameter to PathInput to allow tab to advance the focus. + +## [0.4.0] - 2023-08-03 + +### Features + +- Adds a suggester to autocomplete paths for the save and open file inputs. +- Adds a validator to validate paths for the save and open file inputs. +- `textual-textarea` now requires `textual` >=0.27.0 +- Adds reactive properties to the textarea for `selection_anchor` position and + `selected_text`. + +## [0.3.3] - 2023-07-28 + +### Features + +- The open and save file inputs now expand the user home directory (`~`). + +### Bug Fixes + +- Selection should be better-maintained when pressing F-keys. + +## [0.3.2] - 2023-07-14 + +### Bug Fixes + +- Improves support for pasting text with `ctrl+v` on all platforms. ([#53](https://github.com/tconbeer/textual-textarea/issues/53)). + +## [0.3.1] - 2023-06-26 + +### Bug Fixes + +- Fixes issue where text area was aggressively capturing mouse events and not responding to mouse up events, + which would cause issues if your App had widgets other than the TextArea ([#42](https://github.com/tconbeer/textual-textarea/issues/42)). +- Fixes an issue where PageUp could cause a crash ([#46](https://github.com/tconbeer/textual-textarea/issues/46)). + +## [0.3.0] - 2023-06-19 + +- Select text using click and drag ([#8](https://github.com/tconbeer/textual-textarea/issues/8)). +- Comment characters inserted with ctrl+/ are now based on the language that the + TextArea is initialized with ([#24](https://github.com/tconbeer/textual-textarea/issues/24)). +- TextArea exposes a `language` property for the currently-configured language. + +## [0.2.2] - 2023-06-15 + +### Features + +- Adds a cursor attribute to TextArea to make it easier to get and set the TextInput's cursor position. +- Adds 3 attributes to TextArea to make it easier to access the child widgets: `text_input`, `text_container`, and `footer`. + +### Bug Fixes + +- Fixes a bug that was preventing the cursor from being scrolled into view. + +## [0.2.1] - 2023-06-15 + +### Bug Fixes + +- Fixes a bug where the TextArea did not update or have focus after opening a file ([#28](https://github.com/tconbeer/textual-textarea/issues/28)) +- Fixes a bug where a missing space at the end of the buffer after opening a file could cause a crash + +## [0.2.0] - 2023-06-14 + +### Features + +- Uses the system clipboard (if it exists) for copy and paste operations, unless initialized + with `use_system_clipboard=False`. +- Adds a sample app that can be run with `python -m textual_textarea`. + +## [0.1.2] - 2023-06-01 + +- Makes top-level TextArea widget focusable +- Loosens textual dependency to >=0.21.0 +- Adds py.typed file + +## [0.1.1] - 2023-06-01 + +- Exports TextArea class under the main textual_textarea module. + +## [0.1.0] - 2023-06-01 + +- Initial release: TextArea is a feature-rich text area (multiline) input, with + support for syntax highlighting, themes, keyboard navigation, copy-paste, file + opening and saving, and more! + +[unreleased]: https://github.com/tconbeer/textual-textarea/compare/0.15.0...HEAD +[0.15.0]: https://github.com/tconbeer/textual-textarea/compare/0.14.4...0.15.0 +[0.14.4]: https://github.com/tconbeer/textual-textarea/compare/0.14.3...0.14.4 +[0.14.3]: https://github.com/tconbeer/textual-textarea/compare/0.14.2...0.14.3 +[0.14.2]: https://github.com/tconbeer/textual-textarea/compare/0.14.1...0.14.2 +[0.14.1]: https://github.com/tconbeer/textual-textarea/compare/0.14.0...0.14.1 +[0.14.0]: https://github.com/tconbeer/textual-textarea/compare/0.13.1...0.14.0 +[0.13.1]: https://github.com/tconbeer/textual-textarea/compare/0.13.0...0.13.1 +[0.13.0]: https://github.com/tconbeer/textual-textarea/compare/0.12.0...0.13.0 +[0.12.0]: https://github.com/tconbeer/textual-textarea/compare/0.11.3...0.12.0 +[0.11.3]: https://github.com/tconbeer/textual-textarea/compare/0.11.2...0.11.3 +[0.11.2]: https://github.com/tconbeer/textual-textarea/compare/0.11.1...0.11.2 +[0.11.1]: https://github.com/tconbeer/textual-textarea/compare/0.11.0...0.11.1 +[0.11.0]: https://github.com/tconbeer/textual-textarea/compare/0.10.0...0.11.0 +[0.10.0]: https://github.com/tconbeer/textual-textarea/compare/0.9.5...0.10.0 +[0.9.5]: https://github.com/tconbeer/textual-textarea/compare/0.9.4...0.9.5 +[0.9.4]: https://github.com/tconbeer/textual-textarea/compare/0.9.3...0.9.4 +[0.9.3]: https://github.com/tconbeer/textual-textarea/compare/0.9.2...0.9.3 +[0.9.2]: https://github.com/tconbeer/textual-textarea/compare/0.9.1...0.9.2 +[0.9.1]: https://github.com/tconbeer/textual-textarea/compare/0.9.0...0.9.1 +[0.9.0]: https://github.com/tconbeer/textual-textarea/compare/0.8.0...0.9.0 +[0.8.0]: https://github.com/tconbeer/textual-textarea/compare/0.7.3...0.8.0 +[0.7.3]: https://github.com/tconbeer/textual-textarea/compare/0.7.2...0.7.3 +[0.7.2]: https://github.com/tconbeer/textual-textarea/compare/0.7.1...0.7.2 +[0.7.1]: https://github.com/tconbeer/textual-textarea/compare/0.7.0...0.7.1 +[0.7.0]: https://github.com/tconbeer/textual-textarea/compare/0.6.0...0.7.0 +[0.6.0]: https://github.com/tconbeer/textual-textarea/compare/0.5.4...0.6.0 +[0.5.4]: https://github.com/tconbeer/textual-textarea/compare/0.5.3...0.5.4 +[0.5.3]: https://github.com/tconbeer/textual-textarea/compare/0.5.2...0.5.3 +[0.5.2]: https://github.com/tconbeer/textual-textarea/compare/0.5.1...0.5.2 +[0.5.1]: https://github.com/tconbeer/textual-textarea/compare/0.5.0...0.5.1 +[0.5.0]: https://github.com/tconbeer/textual-textarea/compare/0.4.2...0.5.0 +[0.4.2]: https://github.com/tconbeer/textual-textarea/compare/0.4.1...0.4.2 +[0.4.1]: https://github.com/tconbeer/textual-textarea/compare/0.4.0...0.4.1 +[0.4.0]: https://github.com/tconbeer/textual-textarea/compare/0.3.3...0.4.0 +[0.3.3]: https://github.com/tconbeer/textual-textarea/compare/0.3.2...0.3.3 +[0.3.2]: https://github.com/tconbeer/textual-textarea/compare/0.3.1...0.3.2 +[0.3.1]: https://github.com/tconbeer/textual-textarea/compare/0.3.0...0.3.1 +[0.3.0]: https://github.com/tconbeer/textual-textarea/compare/0.2.2...0.3.0 +[0.2.2]: https://github.com/tconbeer/textual-textarea/compare/0.2.1...0.2.2 +[0.2.1]: https://github.com/tconbeer/textual-textarea/compare/0.2.0...0.2.1 +[0.2.0]: https://github.com/tconbeer/textual-textarea/compare/0.1.2...0.2.0 +[0.1.2]: https://github.com/tconbeer/textual-textarea/compare/0.1.1...0.1.2 +[0.1.1]: https://github.com/tconbeer/textual-textarea/compare/0.1.0...0.1.1 +[0.1.0]: https://github.com/tconbeer/textual-textarea/compare/9832e9bbe1cd7a2ce9a4f09746eb1c2ddc8df842...0.1.0 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..04ec0c8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Ted Conbeer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..91db1c9 --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +.PHONY: check +check: + ruff format . + ruff check . --fix + mypy + pytest + +.PHONY: lint +lint: + ruff format . + ruff check . --fix + mypy + +.PHONY: serve +serve: + textual run --dev -c python -m textual_textarea + +profiles: .profiles/startup.html + +.profiles/startup.html: src/scripts/profile_startup.py pyproject.toml $(wildcard src/textual_textarea/**/*.py) + pyinstrument -r html -o .profiles/startup.html "src/scripts/profile_startup.py" diff --git a/README.md b/README.md new file mode 100644 index 0000000..9a03d7a --- /dev/null +++ b/README.md @@ -0,0 +1,150 @@ +# Textual Textarea +![Textual Textarea Screenshot](textarea.svg) + +## Note: This is **NOT** the official TextArea widget! + +With v0.38.0, Textual added a built-in TextArea widget. You probably want to use +that widget instead of this one. This project predated the official widget; versions < v0.8.0 +had a completely separate implmentation. + +Since v0.8.0, this project uses the built-in TextArea widget, but adds the features outlined below. + +## Installation + +``` +pip install textual-textarea +``` + +## Features +Full-featured text editor experience with VS-Code-like bindings, in your Textual App: +- Syntax highlighting and support for Pygments themes. +- Move cursor and scroll with mouse or keys (including ctrl+arrow, PgUp/Dn, ctrl+Home/End). +- Open (ctrl+o) and save (ctrl+s) files. +- Cut (ctrl+x), copy (ctrl+c), paste (ctrl+u/v), optionally using the system clipboard. +- Comment selections with ctrl+/. +- Indent and dedent (optionally for a multiline selection) to tab stops with Tab and shift+Tab. +- Automatic completions of quotes and brackets. +- Select text by double-, triple-, or quadruple-clicking. +- Quit with ctrl+q. + +## Usage + +### Initializing the Widget + +The TextArea is a Textual Widget. You can add it to a Textual +app using `compose` or `mount`: + +```python +from textual_textarea import TextEditor +from textual.app import App, ComposeResult + +class TextApp(App, inherit_bindings=False): + def compose(self) -> ComposeResult: + yield TextEditor(text="hi", language="python", theme="nord-darker", id="ta") + + def on_mount(self) -> None: + editor = self.query_one("#id", expect_type=TextEditor) + editor.focus() + +app = TextApp() +app.run() +``` + +In addition to the standard Widget arguments, TextArea accepts three additional, optional arguments when initializing the widget: + +- language (str): Must be `None` or the short name of a [Pygments lexer](https://pygments.org/docs/lexers/), e.g., `python`, `sql`, `as3`. Defaults to `None`. +- theme (str): Must be name of a [Pygments style](https://pygments.org/styles/), e.g., `bw`, `github-dark`, `solarized-light`. Defaults to `monokai`. +- use_system_clipboard (bool): Set to `False` to make the TextArea's copy and paste operations ignore the system clipboard. Defaults to `True`. Some Linux users may need to apt-install `xclip` or `xsel` to enable the system clipboard features. + +The TextArea supports many actions and key bindings. **For proper binding of `ctrl+c` to the COPY action, +you must initialize your App with `inherit_bindings=False`** (as shown above), so that `ctrl+c` does not quit the app. The TextArea implements `ctrl+q` as quit; you way wish to mimic that in your app so that other in-focus widgets use the same behavior. + +### Interacting with the Widget + +#### Getting and Setting Text + +The TextArea exposes a `text` property that contains the full text contained in the widget. You can retrieve or set the text by interacting with this property: + +```python +editor = self.query_one(TextEditor) +old_text = editor.text +editor.text = "New Text!\n\nMany Lines!" +``` + +Similarly, the TextEditor exposes a `selected_text` property (read-only): +```python +editor = self.query_one(TextEditor) +selection = editor.selected_text +``` + +#### Inserting Text + +You can insert text at the current selection: +```python +editor = self.query_one(TextEditor) +editor.text = "01234" +editor.selection = Selection((0, 2), (0, 2)) +editor.insert_text_at_selection("\nabc\n") +assert editor.text == "01\nabc\n234" +assert editor.selection == Selection((2, 0), (2, 0)) +``` + +#### Getting and Setting The Cursor Position + +The TextEditor exposes a `selection` property that returns a textual.widgets.text_area.Selection: + +```python +editor = self.query_one(TextEditor) +old_selection = editor.selection +editor.selection = Selection((999, 0),(999, 0)) # the cursor will move as close to line 999, pos 0 as possible +cursor_line_number = editor.selection.end[0] +cursor_x_position = editor.selection.end[1] +``` + + +#### Getting and Setting The Language + +Syntax highlighting and comment insertion depends on the configured language for the TextEditor. + +The TextArea exposes a `language` property that returns `None` or a string that is equal to the short name of an installed tree-sitter language: + +```python +editor = self.query_one(TextEditor) +old_language = editor.language +editor.language = "python" +``` + +#### Getting Theme Colors + +If you would like the rest of your app to match the colors from the TextArea's theme, they are exposed via the `theme_colors` property. + +```python +editor = self.query_one(TextEditor) +color = editor.theme_colors.contrast_text_color +bgcolor = editor.theme_colors.bgcolor +highlight = editor.theme_colors.selection_bgcolor +``` + + +#### Adding Bindings and other Behavior + +You can subclass TextEditor to add your own behavior. This snippet adds an action that posts a Submitted message containing the text of the TextEditor when the user presses ctrl+j: + +```python +from textual.message import Message +from textual_textarea import TextEditor + + +class CodeEditor(TextEditor): + BINDINGS = [ + ("ctrl+j", "submit", "Run Query"), + ] + + class Submitted(Message, bubble=True): + def __init__(self, text: str) -> None: + super().__init__() + self.text = text + + async def action_submit(self) -> None: + self.post_message(self.Submitted(self.text)) +``` diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..ac8250e --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1816 @@ +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. + +[[package]] +name = "aiohappyeyeballs" +version = "2.4.4" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8"}, + {file = "aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745"}, +] + +[[package]] +name = "aiohttp" +version = "3.11.9" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.9" +files = [ + {file = "aiohttp-3.11.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0411777249f25d11bd2964a230b3ffafcbed6cd65d0f2b132bc2b8f5b8c347c7"}, + {file = "aiohttp-3.11.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:499368eb904566fbdf1a3836a1532000ef1308f34a1bcbf36e6351904cced771"}, + {file = "aiohttp-3.11.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0b5a5009b0159a8f707879dc102b139466d8ec6db05103ec1520394fdd8ea02c"}, + {file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:176f8bb8931da0613bb0ed16326d01330066bb1e172dd97e1e02b1c27383277b"}, + {file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6435a66957cdba1a0b16f368bde03ce9c79c57306b39510da6ae5312a1a5b2c1"}, + {file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:202f40fb686e5f93908eee0c75d1e6fbe50a43e9bd4909bf3bf4a56b560ca180"}, + {file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39625703540feb50b6b7f938b3856d1f4886d2e585d88274e62b1bd273fae09b"}, + {file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c6beeac698671baa558e82fa160be9761cf0eb25861943f4689ecf9000f8ebd0"}, + {file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:96726839a42429318017e67a42cca75d4f0d5248a809b3cc2e125445edd7d50d"}, + {file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3f5461c77649358610fb9694e790956b4238ac5d9e697a17f63619c096469afe"}, + {file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4313f3bc901255b22f01663eeeae167468264fdae0d32c25fc631d5d6e15b502"}, + {file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d6e274661c74195708fc4380a4ef64298926c5a50bb10fbae3d01627d7a075b7"}, + {file = "aiohttp-3.11.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db2914de2559809fdbcf3e48f41b17a493b58cb7988d3e211f6b63126c55fe82"}, + {file = "aiohttp-3.11.9-cp310-cp310-win32.whl", hash = "sha256:27935716f8d62c1c73010428db310fd10136002cfc6d52b0ba7bdfa752d26066"}, + {file = "aiohttp-3.11.9-cp310-cp310-win_amd64.whl", hash = "sha256:afbe85b50ade42ddff5669947afde9e8a610e64d2c80be046d67ec4368e555fa"}, + {file = "aiohttp-3.11.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:afcda759a69c6a8be3aae764ec6733155aa4a5ad9aad4f398b52ba4037942fe3"}, + {file = "aiohttp-3.11.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5bba6b83fde4ca233cfda04cbd4685ab88696b0c8eaf76f7148969eab5e248a"}, + {file = "aiohttp-3.11.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:442356e8924fe1a121f8c87866b0ecdc785757fd28924b17c20493961b3d6697"}, + {file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f737fef6e117856400afee4f17774cdea392b28ecf058833f5eca368a18cf1bf"}, + {file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea142255d4901b03f89cb6a94411ecec117786a76fc9ab043af8f51dd50b5313"}, + {file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e1e9e447856e9b7b3d38e1316ae9a8c92e7536ef48373de758ea055edfd5db5"}, + {file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7f6173302f8a329ca5d1ee592af9e628d3ade87816e9958dcf7cdae2841def7"}, + {file = "aiohttp-3.11.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7c6147c6306f537cff59409609508a1d2eff81199f0302dd456bb9e7ea50c39"}, + {file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e9d036a9a41fc78e8a3f10a86c2fc1098fca8fab8715ba9eb999ce4788d35df0"}, + {file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2ac9fd83096df36728da8e2f4488ac3b5602238f602706606f3702f07a13a409"}, + {file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d3108f0ad5c6b6d78eec5273219a5bbd884b4aacec17883ceefaac988850ce6e"}, + {file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:96bbec47beb131bbf4bae05d8ef99ad9e5738f12717cfbbf16648b78b0232e87"}, + {file = "aiohttp-3.11.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fc726c3fa8f606d07bd2b500e5dc4c0fd664c59be7788a16b9e34352c50b6b6b"}, + {file = "aiohttp-3.11.9-cp311-cp311-win32.whl", hash = "sha256:5720ebbc7a1b46c33a42d489d25d36c64c419f52159485e55589fbec648ea49a"}, + {file = "aiohttp-3.11.9-cp311-cp311-win_amd64.whl", hash = "sha256:17af09d963fa1acd7e4c280e9354aeafd9e3d47eaa4a6bfbd2171ad7da49f0c5"}, + {file = "aiohttp-3.11.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c1f2d7fd583fc79c240094b3e7237d88493814d4b300d013a42726c35a734bc9"}, + {file = "aiohttp-3.11.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4b8a1b6c7a68c73191f2ebd3bf66f7ce02f9c374e309bdb68ba886bbbf1b938"}, + {file = "aiohttp-3.11.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd3f711f4c99da0091ced41dccdc1bcf8be0281dc314d6d9c6b6cf5df66f37a9"}, + {file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44cb1a1326a0264480a789e6100dc3e07122eb8cd1ad6b784a3d47d13ed1d89c"}, + {file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7a7ddf981a0b953ade1c2379052d47ccda2f58ab678fca0671c7c7ca2f67aac2"}, + {file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ffa45cc55b18d4ac1396d1ddb029f139b1d3480f1594130e62bceadf2e1a838"}, + {file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cca505829cdab58c2495ff418c96092d225a1bbd486f79017f6de915580d3c44"}, + {file = "aiohttp-3.11.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44d323aa80a867cb6db6bebb4bbec677c6478e38128847f2c6b0f70eae984d72"}, + {file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b2fab23003c4bb2249729a7290a76c1dda38c438300fdf97d4e42bf78b19c810"}, + {file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:be0c7c98e38a1e3ad7a6ff64af8b6d6db34bf5a41b1478e24c3c74d9e7f8ed42"}, + {file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5cc5e0d069c56645446c45a4b5010d4b33ac6c5ebfd369a791b5f097e46a3c08"}, + {file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9bcf97b971289be69638d8b1b616f7e557e1342debc7fc86cf89d3f08960e411"}, + {file = "aiohttp-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c7333e7239415076d1418dbfb7fa4df48f3a5b00f8fdf854fca549080455bc14"}, + {file = "aiohttp-3.11.9-cp312-cp312-win32.whl", hash = "sha256:9384b07cfd3045b37b05ed002d1c255db02fb96506ad65f0f9b776b762a7572e"}, + {file = "aiohttp-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:f5252ba8b43906f206048fa569debf2cd0da0316e8d5b4d25abe53307f573941"}, + {file = "aiohttp-3.11.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:282e0a7ddd36ebc411f156aeaa0491e8fe7f030e2a95da532cf0c84b0b70bc66"}, + {file = "aiohttp-3.11.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ebd3e6b0c7d4954cca59d241970011f8d3327633d555051c430bd09ff49dc494"}, + {file = "aiohttp-3.11.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30f9f89ae625d412043f12ca3771b2ccec227cc93b93bb1f994db6e1af40a7d3"}, + {file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a3b5b2c012d70c63d9d13c57ed1603709a4d9d7d473e4a9dfece0e4ea3d5f51"}, + {file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ef1550bb5f55f71b97a6a395286db07f7f2c01c8890e613556df9a51da91e8d"}, + {file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:317251b9c9a2f1a9ff9cd093775b34c6861d1d7df9439ce3d32a88c275c995cd"}, + {file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21cbe97839b009826a61b143d3ca4964c8590d7aed33d6118125e5b71691ca46"}, + {file = "aiohttp-3.11.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:618b18c3a2360ac940a5503da14fa4f880c5b9bc315ec20a830357bcc62e6bae"}, + {file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0cf4d814689e58f57ecd5d8c523e6538417ca2e72ff52c007c64065cef50fb2"}, + {file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:15c4e489942d987d5dac0ba39e5772dcbed4cc9ae3710d1025d5ba95e4a5349c"}, + {file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ec8df0ff5a911c6d21957a9182402aad7bf060eaeffd77c9ea1c16aecab5adbf"}, + {file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ed95d66745f53e129e935ad726167d3a6cb18c5d33df3165974d54742c373868"}, + {file = "aiohttp-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:647ec5bee7e4ec9f1034ab48173b5fa970d9a991e565549b965e93331f1328fe"}, + {file = "aiohttp-3.11.9-cp313-cp313-win32.whl", hash = "sha256:ef2c9499b7bd1e24e473dc1a85de55d72fd084eea3d8bdeec7ee0720decb54fa"}, + {file = "aiohttp-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:84de955314aa5e8d469b00b14d6d714b008087a0222b0f743e7ffac34ef56aff"}, + {file = "aiohttp-3.11.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e738aabff3586091221044b7a584865ddc4d6120346d12e28e788307cd731043"}, + {file = "aiohttp-3.11.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:28f29bce89c3b401a53d6fd4bee401ee943083bf2bdc12ef297c1d63155070b0"}, + {file = "aiohttp-3.11.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31de2f10f63f96cc19e04bd2df9549559beadd0b2ee2da24a17e7ed877ca8c60"}, + {file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f31cebd8c27a36af6c7346055ac564946e562080ee1a838da724585c67474f"}, + {file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0bcb7f6976dc0b6b56efde13294862adf68dd48854111b422a336fa729a82ea6"}, + {file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8b13b9950d8b2f8f58b6e5842c4b842b5887e2c32e3f4644d6642f1659a530"}, + {file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9c23e62f3545c2216100603614f9e019e41b9403c47dd85b8e7e5015bf1bde0"}, + {file = "aiohttp-3.11.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec656680fc53a13f849c71afd0c84a55c536206d524cbc831cde80abbe80489e"}, + {file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:36df00e0541f264ce42d62280281541a47474dfda500bc5b7f24f70a7f87be7a"}, + {file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8dcfd14c712aa9dd18049280bfb2f95700ff6a8bde645e09f17c3ed3f05a0130"}, + {file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14624d96f0d69cf451deed3173079a68c322279be6030208b045ab77e1e8d550"}, + {file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4b01d9cfcb616eeb6d40f02e66bebfe7b06d9f2ef81641fdd50b8dd981166e0b"}, + {file = "aiohttp-3.11.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:928f92f80e2e8d6567b87d3316c1fd9860ccfe36e87a9a7f5237d4cda8baa1ba"}, + {file = "aiohttp-3.11.9-cp39-cp39-win32.whl", hash = "sha256:c8a02f74ae419e3955af60f570d83187423e42e672a6433c5e292f1d23619269"}, + {file = "aiohttp-3.11.9-cp39-cp39-win_amd64.whl", hash = "sha256:0a97d657f6cf8782a830bb476c13f7d777cfcab8428ac49dde15c22babceb361"}, + {file = "aiohttp-3.11.9.tar.gz", hash = "sha256:a9266644064779840feec0e34f10a89b3ff1d2d6b751fe90017abcad1864fa7c"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.3.0" +aiosignal = ">=1.1.2" +async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +propcache = ">=0.2.0" +yarl = ">=1.17.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] + +[[package]] +name = "aiohttp-jinja2" +version = "1.6" +description = "jinja2 template renderer for aiohttp.web (http server for asyncio)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohttp-jinja2-1.6.tar.gz", hash = "sha256:a3a7ff5264e5bca52e8ae547bbfd0761b72495230d438d05b6c0915be619b0e2"}, + {file = "aiohttp_jinja2-1.6-py3-none-any.whl", hash = "sha256:0df405ee6ad1b58e5a068a105407dc7dcc1704544c559f1938babde954f945c7"}, +] + +[package.dependencies] +aiohttp = ">=3.9.0" +jinja2 = ">=3.0.0" + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" + +[[package]] +name = "async-timeout" +version = "5.0.1" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, +] + +[[package]] +name = "attrs" +version = "24.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, + {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, +] + +[package.extras] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "distlib" +version = "0.3.9" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "filelock" +version = "3.16.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, + {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] +typing = ["typing-extensions (>=4.12.2)"] + +[[package]] +name = "frozenlist" +version = "1.5.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.8" +files = [ + {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"}, + {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"}, + {file = "frozenlist-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5"}, + {file = "frozenlist-1.5.0-cp310-cp310-win32.whl", hash = "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb"}, + {file = "frozenlist-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf"}, + {file = "frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942"}, + {file = "frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f"}, + {file = "frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8"}, + {file = "frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03"}, + {file = "frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c"}, + {file = "frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e"}, + {file = "frozenlist-1.5.0-cp38-cp38-win32.whl", hash = "sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723"}, + {file = "frozenlist-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c"}, + {file = "frozenlist-1.5.0-cp39-cp39-win32.whl", hash = "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3"}, + {file = "frozenlist-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0"}, + {file = "frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3"}, + {file = "frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817"}, +] + +[[package]] +name = "identify" +version = "2.6.3" +description = "File identification library for Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd"}, + {file = "identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "linkify-it-py" +version = "2.0.3" +description = "Links recognition library with FULL unicode support." +optional = false +python-versions = ">=3.7" +files = [ + {file = "linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048"}, + {file = "linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79"}, +] + +[package.dependencies] +uc-micro-py = "*" + +[package.extras] +benchmark = ["pytest", "pytest-benchmark"] +dev = ["black", "flake8", "isort", "pre-commit", "pyproject-flake8"] +doc = ["myst-parser", "sphinx", "sphinx-book-theme"] +test = ["coverage", "pytest", "pytest-cov"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +linkify-it-py = {version = ">=1,<3", optional = true, markers = "extra == \"linkify\""} +mdit-py-plugins = {version = "*", optional = true, markers = "extra == \"plugins\""} +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.4.2" +description = "Collection of plugins for markdown-it-py" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636"}, + {file = "mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5"}, +] + +[package.dependencies] +markdown-it-py = ">=1.0.0,<4.0.0" + +[package.extras] +code-style = ["pre-commit"] +rtd = ["myst-parser", "sphinx-book-theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "msgpack" +version = "1.1.0" +description = "MessagePack serializer" +optional = false +python-versions = ">=3.8" +files = [ + {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd"}, + {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d"}, + {file = "msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5"}, + {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5"}, + {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e"}, + {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b"}, + {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f"}, + {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68"}, + {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b"}, + {file = "msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044"}, + {file = "msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f"}, + {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7"}, + {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa"}, + {file = "msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701"}, + {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6"}, + {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59"}, + {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0"}, + {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e"}, + {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6"}, + {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5"}, + {file = "msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88"}, + {file = "msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788"}, + {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d"}, + {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2"}, + {file = "msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420"}, + {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2"}, + {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39"}, + {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f"}, + {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247"}, + {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c"}, + {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b"}, + {file = "msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b"}, + {file = "msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f"}, + {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf"}, + {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330"}, + {file = "msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734"}, + {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e"}, + {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca"}, + {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915"}, + {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d"}, + {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434"}, + {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c"}, + {file = "msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc"}, + {file = "msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f"}, + {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c40ffa9a15d74e05ba1fe2681ea33b9caffd886675412612d93ab17b58ea2fec"}, + {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1ba6136e650898082d9d5a5217d5906d1e138024f836ff48691784bbe1adf96"}, + {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0856a2b7e8dcb874be44fea031d22e5b3a19121be92a1e098f46068a11b0870"}, + {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:471e27a5787a2e3f974ba023f9e265a8c7cfd373632247deb225617e3100a3c7"}, + {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:646afc8102935a388ffc3914b336d22d1c2d6209c773f3eb5dd4d6d3b6f8c1cb"}, + {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:13599f8829cfbe0158f6456374e9eea9f44eee08076291771d8ae93eda56607f"}, + {file = "msgpack-1.1.0-cp38-cp38-win32.whl", hash = "sha256:8a84efb768fb968381e525eeeb3d92857e4985aacc39f3c47ffd00eb4509315b"}, + {file = "msgpack-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:879a7b7b0ad82481c52d3c7eb99bf6f0645dbdec5134a4bddbd16f3506947feb"}, + {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1"}, + {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48"}, + {file = "msgpack-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c"}, + {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468"}, + {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74"}, + {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846"}, + {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346"}, + {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b"}, + {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8"}, + {file = "msgpack-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd"}, + {file = "msgpack-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325"}, + {file = "msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e"}, +] + +[[package]] +name = "multidict" +version = "6.1.0" +description = "multidict implementation" +optional = false +python-versions = ">=3.8" +files = [ + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7"}, + {file = "multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0"}, + {file = "multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753"}, + {file = "multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80"}, + {file = "multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"}, + {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"}, + {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6"}, + {file = "multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81"}, + {file = "multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd"}, + {file = "multidict-6.1.0-cp38-cp38-win32.whl", hash = "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167"}, + {file = "multidict-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43"}, + {file = "multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada"}, + {file = "multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a"}, + {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"}, + {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} + +[[package]] +name = "mypy" +version = "1.13.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, + {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, + {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, + {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, + {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, + {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, + {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, + {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, + {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, + {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, + {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, + {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, + {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, + {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, + {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, + {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, + {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, + {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, + {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, + {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, + {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "3.8.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, + {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "propcache" +version = "0.2.1" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.9" +files = [ + {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6"}, + {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2"}, + {file = "propcache-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6445804cf4ec763dc70de65a3b0d9954e868609e83850a47ca4f0cb64bd79fea"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9479aa06a793c5aeba49ce5c5692ffb51fcd9a7016e017d555d5e2b0045d212"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9631c5e8b5b3a0fda99cb0d29c18133bca1e18aea9effe55adb3da1adef80d3"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3156628250f46a0895f1f36e1d4fbe062a1af8718ec3ebeb746f1d23f0c5dc4d"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b6fb63ae352e13748289f04f37868099e69dba4c2b3e271c46061e82c745634"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:887d9b0a65404929641a9fabb6452b07fe4572b269d901d622d8a34a4e9043b2"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a96dc1fa45bd8c407a0af03b2d5218392729e1822b0c32e62c5bf7eeb5fb3958"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a7e65eb5c003a303b94aa2c3852ef130230ec79e349632d030e9571b87c4698c"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:999779addc413181912e984b942fbcc951be1f5b3663cd80b2687758f434c583"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:19a0f89a7bb9d8048d9c4370c9c543c396e894c76be5525f5e1ad287f1750ddf"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1ac2f5fe02fa75f56e1ad473f1175e11f475606ec9bd0be2e78e4734ad575034"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:574faa3b79e8ebac7cb1d7930f51184ba1ccf69adfdec53a12f319a06030a68b"}, + {file = "propcache-0.2.1-cp310-cp310-win32.whl", hash = "sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4"}, + {file = "propcache-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2d3af2e79991102678f53e0dbf4c35de99b6b8b58f29a27ca0325816364caaba"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e"}, + {file = "propcache-0.2.1-cp311-cp311-win32.whl", hash = "sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034"}, + {file = "propcache-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518"}, + {file = "propcache-0.2.1-cp312-cp312-win32.whl", hash = "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246"}, + {file = "propcache-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30"}, + {file = "propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6"}, + {file = "propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1"}, + {file = "propcache-0.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6a9a8c34fb7bb609419a211e59da8887eeca40d300b5ea8e56af98f6fbbb1541"}, + {file = "propcache-0.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae1aa1cd222c6d205853b3013c69cd04515f9d6ab6de4b0603e2e1c33221303e"}, + {file = "propcache-0.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:accb6150ce61c9c4b7738d45550806aa2b71c7668c6942f17b0ac182b6142fd4"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5eee736daafa7af6d0a2dc15cc75e05c64f37fc37bafef2e00d77c14171c2097"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7a31fc1e1bd362874863fdeed71aed92d348f5336fd84f2197ba40c59f061bd"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba4cfa1052819d16699e1d55d18c92b6e094d4517c41dd231a8b9f87b6fa681"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f089118d584e859c62b3da0892b88a83d611c2033ac410e929cb6754eec0ed16"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:781e65134efaf88feb447e8c97a51772aa75e48b794352f94cb7ea717dedda0d"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31f5af773530fd3c658b32b6bdc2d0838543de70eb9a2156c03e410f7b0d3aae"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:a7a078f5d37bee6690959c813977da5291b24286e7b962e62a94cec31aa5188b"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cea7daf9fc7ae6687cf1e2c049752f19f146fdc37c2cc376e7d0032cf4f25347"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:8b3489ff1ed1e8315674d0775dc7d2195fb13ca17b3808721b54dbe9fd020faf"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9403db39be1393618dd80c746cb22ccda168efce239c73af13c3763ef56ffc04"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5d97151bc92d2b2578ff7ce779cdb9174337390a535953cbb9452fb65164c587"}, + {file = "propcache-0.2.1-cp39-cp39-win32.whl", hash = "sha256:9caac6b54914bdf41bcc91e7eb9147d331d29235a7c967c150ef5df6464fd1bb"}, + {file = "propcache-0.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:92fc4500fcb33899b05ba73276dfb684a20d31caa567b7cb5252d48f896a91b1"}, + {file = "propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54"}, + {file = "propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64"}, +] + +[[package]] +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pyinstrument" +version = "5.0.0" +description = "Call stack profiler for Python. Shows you why your code is slow!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyinstrument-5.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6a83cf18f5594e1b1899b12b46df7aabca556eef895846ccdaaa3a46a37d1274"}, + {file = "pyinstrument-5.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1cc236313272d0222261be8e2b2a08e42d7ccbe54db9059babf4d77040da1880"}, + {file = "pyinstrument-5.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6dd685d68a31f3715ca61f82c37c1c2f8b75f45646bd9840e04681d91862bd85"}, + {file = "pyinstrument-5.0.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cecd0f6558f13fba74a9f036b2b168956206e9525dcb84c6add2d73ab61dc22"}, + {file = "pyinstrument-5.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40a8485c2e41082a20822001a6651667bb5327f6f5f6759987198593e45bb376"}, + {file = "pyinstrument-5.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a6294b7111348765ba4c311fc91821ed8b59c6690c4dab23aa7165a67da9e972"}, + {file = "pyinstrument-5.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a164f3dae5c7db2faa501639659d64034cde8db62a4d6744712593a369bc8629"}, + {file = "pyinstrument-5.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f6bac8a434407de6f2ebddbcdecdb19b324c9315cbb8b8c2352714f7ced8181"}, + {file = "pyinstrument-5.0.0-cp310-cp310-win32.whl", hash = "sha256:7e8dc887e535f5c5e5a2a64a0729496f11ddcef0c23b0a555d5ab6fa19759445"}, + {file = "pyinstrument-5.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c337190a1818841732643ba93065411591df526bc9de44b97ba8f56b581d2ef"}, + {file = "pyinstrument-5.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c9052f548ec5ccecc50676fbf1a1d0b60bdbd3cd67630c5253099af049d1f0ad"}, + {file = "pyinstrument-5.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:197d25487f52da3f8ec26d46db7202bc5d703cc73c1503371166417eb7cea14e"}, + {file = "pyinstrument-5.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a072d928dc16a32e0f3d1e51726f4472a69d66d838ee1d1bf248737fd70b9415"}, + {file = "pyinstrument-5.0.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2c7ae2c984879a645fce583bf3053b7e57495f60c1e158bb71ad7dfced1fbf1"}, + {file = "pyinstrument-5.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8284bf8847629c9a5054702b9306eab3ab14c2474959e01e606369ffbcf938bc"}, + {file = "pyinstrument-5.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4fd94cc725efb1dd41ae8e20a5f06a6a5363dec959e8a9dacbac3f4d12d28f03"}, + {file = "pyinstrument-5.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e0fdb9fe6f9c694940410dcc82e23a3fe2928114328efd35047fc0bb8a6c959f"}, + {file = "pyinstrument-5.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9ffe938e63173ceb8ce7b6b309ce26c9d44d16f53c0162d89d6e706eb9e69802"}, + {file = "pyinstrument-5.0.0-cp311-cp311-win32.whl", hash = "sha256:80d2a248516f372a89e0fe9ddf4a9d6388a4c6481b6ebd3dfe01b3cd028c0275"}, + {file = "pyinstrument-5.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:7ccf4267aff62de0e1d976e8f5da25dcb69737ae86e38d3cfffa24877837e7d1"}, + {file = "pyinstrument-5.0.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:dec3529a5351ea160baeef1ef2a6e28b1a7a7b3fb5e9863fae8de6da73d0f69a"}, + {file = "pyinstrument-5.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5a39e3ef84c56183f8274dfd584b8c2fae4783c6204f880513e70ab2440b9137"}, + {file = "pyinstrument-5.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3938f063ee065e05826628dadf1fb32c7d26b22df4a945c22f7fe25ea1ba6a2"}, + {file = "pyinstrument-5.0.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f18990cc16b2e23b54738aa2f222863e1d36daaaec8f67b1613ddfa41f5b24db"}, + {file = "pyinstrument-5.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3731412b5bfdcef8014518f145140c69384793e218863a33a39ccfe5fb42045"}, + {file = "pyinstrument-5.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02b2eaf38460b14eea646d6bb7f373eb5bb5691d13f788e80bdcb3a4eaa2519e"}, + {file = "pyinstrument-5.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e57db06590f13657b2bce8c4d9cf8e9e2bd90bb729bcbbe421c531ba67ad7add"}, + {file = "pyinstrument-5.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ddaa3001c1b798ec9bf1266ef476bbc0834b74d547d531f5ed99e7d05ac5d81b"}, + {file = "pyinstrument-5.0.0-cp312-cp312-win32.whl", hash = "sha256:b69ff982acf5ef2f4e0f32ce9b4b598f256faf88438f233ea3a72f1042707e5b"}, + {file = "pyinstrument-5.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:0bf4ef061d60befe72366ce0ed4c75dee5be089644de38f9936d2df0bcf44af0"}, + {file = "pyinstrument-5.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79a54def2d4aa83a4ed37c6cffc5494ae5de140f0453169eb4f7c744cc249d3a"}, + {file = "pyinstrument-5.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9538f746f166a40c8802ebe5c3e905d50f3faa189869cd71c083b8a639e574bb"}, + {file = "pyinstrument-5.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bbab65cae1483ad8a18429511d1eac9e3efec9f7961f2fd1bf90e1e2d69ef15"}, + {file = "pyinstrument-5.0.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4351ad041d208c597e296a0e9c2e6e21cc96804608bcafa40cfa168f3c2b8f79"}, + {file = "pyinstrument-5.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ceee5252f4580abec29bcc5c965453c217b0d387c412a5ffb8afdcda4e648feb"}, + {file = "pyinstrument-5.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b3050a4e7033103a13cfff9802680e2070a9173e1a258fa3f15a80b4eb9ee278"}, + {file = "pyinstrument-5.0.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3b1f44a34da7810938df615fb7cbc43cd879b42ca6b5cd72e655aee92149d012"}, + {file = "pyinstrument-5.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fde075196c8a3b2be191b8da05b92ff909c78d308f82df56d01a8cfdd6da07b9"}, + {file = "pyinstrument-5.0.0-cp313-cp313-win32.whl", hash = "sha256:1a9b62a8b54e05e7723eb8b9595fadc43559b73290c87b3b1cb2dc5944559790"}, + {file = "pyinstrument-5.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2478d2c55f77ad8e281e67b0dfe7c2176304bb824c307e86e11890f5e68d7feb"}, + {file = "pyinstrument-5.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c2e3b4283f85232fd5818e2153e6798bceb39a8c3ccfaa22fae08faf554740b7"}, + {file = "pyinstrument-5.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fb1139d2822abff1cbf1c81c018341f573b7afa23a94ce74888a0f6f47828cbc"}, + {file = "pyinstrument-5.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c971566d86ba46a7233d3f5b0d85d7ee4c9863f541f5d8f796c3947ebe17f68"}, + {file = "pyinstrument-5.0.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:429376235960179d6ab9b97e7871090059d39de160b4e3b2723672f30e8eea8e"}, + {file = "pyinstrument-5.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8599b4b0630c776b30fc3c4f7476d5e3814ee7fe42d99131644fe3c00b40fdf1"}, + {file = "pyinstrument-5.0.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a8bc688afa2a5368042a7cb56866d5a28fdff8f37a282f7be79b17cae042841b"}, + {file = "pyinstrument-5.0.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5d34c06e2276d1f549a540bccb063688ea3d876e6df7c391205f1c8b4b96d5c8"}, + {file = "pyinstrument-5.0.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4d3b2ec6e028731dbb2ba8cf06f19030162789e6696bca990a09519881ad42fb"}, + {file = "pyinstrument-5.0.0-cp38-cp38-win32.whl", hash = "sha256:5ed6f5873a7526ec5915e45d956d044334ef302653cf63649e48c41561aaa285"}, + {file = "pyinstrument-5.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:9e87d65bae7d0f5ef50908e35d67d43b7cc566909995cc99e91721bb49b4ea06"}, + {file = "pyinstrument-5.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bd953163616bc29c2ccb1e4c0e48ccdd11e0a97fc849da26bc362bba372019ba"}, + {file = "pyinstrument-5.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8d2a7279ed9b6d7cdae247bc2e57095a32f35dfe32182c334ab0ac3eb02e0eac"}, + {file = "pyinstrument-5.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68001dfcb8a37b624a1c3de5d2ee7d634f63eac7a6dd1357b7370a5cdbdcf567"}, + {file = "pyinstrument-5.0.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5c4c3cc6410ad5afe0e352a7fb09fb1ab85eb5676ec5ec8522123759d9cc68f"}, + {file = "pyinstrument-5.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d87ddab66b1b3525ad3abc49a88aaa51efcaf83578e9d2a702c03a1cea39f28"}, + {file = "pyinstrument-5.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:03182ffaa9c91687cbaba80dc0c5a47015c5ea170fe642f632d88e885cf07356"}, + {file = "pyinstrument-5.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:39b60417c9c12eed04e1886644e92aa0b281d72e5d0b097b16253cade43110f7"}, + {file = "pyinstrument-5.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7bb389b6d1573361bd1367b296133c5c69184e35fc18db22e29e8cdf56f158f9"}, + {file = "pyinstrument-5.0.0-cp39-cp39-win32.whl", hash = "sha256:ae69478815edb3c63e7ebf82e1e13e38c3fb2bab833b1c013643c3475b1b8cf5"}, + {file = "pyinstrument-5.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:83caeb4150c0334e9e290c0f9bb164ff6bdc199065ecb62016268e8a88589a51"}, + {file = "pyinstrument-5.0.0.tar.gz", hash = "sha256:144f98eb3086667ece461f66324bf1cc1ee0475b399ab3f9ded8449cc76b7c90"}, +] + +[package.extras] +bin = ["click", "nox"] +docs = ["furo (==2024.7.18)", "myst-parser (==3.0.1)", "sphinx (==7.4.7)", "sphinx-autobuild (==2024.4.16)", "sphinxcontrib-programoutput (==0.17)"] +examples = ["django", "litestar", "numpy"] +test = ["cffi (>=1.17.0)", "flaky", "greenlet (>=3)", "ipython", "pytest", "pytest-asyncio (==0.23.8)", "trio"] +types = ["typing-extensions"] + +[[package]] +name = "pyperclip" +version = "1.9.0" +description = "A cross-platform clipboard module for Python. (Only handles plain text for now.)" +optional = false +python-versions = "*" +files = [ + {file = "pyperclip-1.9.0.tar.gz", hash = "sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310"}, +] + +[[package]] +name = "pytest" +version = "8.3.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.21.2" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest_asyncio-0.21.2-py3-none-any.whl", hash = "sha256:ab664c88bb7998f711d8039cacd4884da6430886ae8bbd4eded552ed2004f16b"}, + {file = "pytest_asyncio-0.21.2.tar.gz", hash = "sha256:d67738fc232b94b326b9d060750beb16e0074210b98dd8b58a5239fa2a154f45"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "rich" +version = "13.9.4" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, + {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "ruff" +version = "0.5.7" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"}, + {file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"}, + {file = "ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e"}, + {file = "ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a"}, + {file = "ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3"}, + {file = "ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4"}, + {file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"}, +] + +[[package]] +name = "textual" +version = "0.89.1" +description = "Modern Text User Interface framework" +optional = false +python-versions = "<4.0.0,>=3.8.1" +files = [ + {file = "textual-0.89.1-py3-none-any.whl", hash = "sha256:0a5d214df6e951b4a2c421e13d0b608482882471c1e34ea74a3631adede8054f"}, + {file = "textual-0.89.1.tar.gz", hash = "sha256:66befe80e2bca5a8c876cd8ceeaf01752267b6b1dc1d0f73071f1f1e15d90cc8"}, +] + +[package.dependencies] +markdown-it-py = {version = ">=2.1.0", extras = ["linkify", "plugins"]} +platformdirs = ">=3.6.0,<5" +rich = ">=13.3.3" +tree-sitter = {version = ">=0.23.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""} +tree-sitter-bash = {version = ">=0.23.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""} +tree-sitter-css = {version = ">=0.23.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""} +tree-sitter-go = {version = ">=0.23.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""} +tree-sitter-html = {version = ">=0.23.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""} +tree-sitter-java = {version = ">=0.23.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""} +tree-sitter-javascript = {version = ">=0.23.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""} +tree-sitter-json = {version = ">=0.24.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""} +tree-sitter-markdown = {version = ">=0.3.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""} +tree-sitter-python = {version = ">=0.23.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""} +tree-sitter-regex = {version = ">=0.24.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""} +tree-sitter-rust = {version = ">=0.23.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""} +tree-sitter-sql = {version = ">=0.3.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""} +tree-sitter-toml = {version = ">=0.6.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""} +tree-sitter-xml = {version = ">=0.7.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""} +tree-sitter-yaml = {version = ">=0.6.0", optional = true, markers = "python_version >= \"3.9\" and extra == \"syntax\""} +typing-extensions = ">=4.4.0,<5.0.0" + +[package.extras] +syntax = ["tree-sitter (>=0.23.0)", "tree-sitter-bash (>=0.23.0)", "tree-sitter-css (>=0.23.0)", "tree-sitter-go (>=0.23.0)", "tree-sitter-html (>=0.23.0)", "tree-sitter-java (>=0.23.0)", "tree-sitter-javascript (>=0.23.0)", "tree-sitter-json (>=0.24.0)", "tree-sitter-markdown (>=0.3.0)", "tree-sitter-python (>=0.23.0)", "tree-sitter-regex (>=0.24.0)", "tree-sitter-rust (>=0.23.0)", "tree-sitter-sql (>=0.3.0)", "tree-sitter-toml (>=0.6.0)", "tree-sitter-xml (>=0.7.0)", "tree-sitter-yaml (>=0.6.0)"] + +[[package]] +name = "textual-dev" +version = "1.7.0" +description = "Development tools for working with Textual" +optional = false +python-versions = "<4.0.0,>=3.8.1" +files = [ + {file = "textual_dev-1.7.0-py3-none-any.whl", hash = "sha256:a93a846aeb6a06edb7808504d9c301565f7f4bf2e7046d56583ed755af356c8d"}, + {file = "textual_dev-1.7.0.tar.gz", hash = "sha256:bf1a50eaaff4cd6a863535dd53f06dbbd62617c371604f66f56de3908220ccd5"}, +] + +[package.dependencies] +aiohttp = ">=3.8.1" +click = ">=8.1.2" +msgpack = ">=1.0.3" +textual = ">=0.86.2" +textual_serve = ">=1.0.3" +typing-extensions = ">=4.4.0,<5.0.0" + +[[package]] +name = "textual-serve" +version = "1.1.1" +description = "Turn your Textual TUIs in to web applications" +optional = false +python-versions = ">=3.8" +files = [ + {file = "textual_serve-1.1.1-py3-none-any.whl", hash = "sha256:568782f1c0e60e3f7039d9121e1cb5c2f4ca1aaf6d6bd7aeb833d5763a534cb2"}, + {file = "textual_serve-1.1.1.tar.gz", hash = "sha256:71c662472c462e5e368defc660ee6e8eae3bfda88ca40c050c55474686eb0c54"}, +] + +[package.dependencies] +aiohttp = ">=3.9.5" +aiohttp-jinja2 = ">=1.6" +jinja2 = ">=3.1.4" +rich = "*" +textual = ">=0.66.0" + +[[package]] +name = "tomli" +version = "2.2.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] + +[[package]] +name = "tree-sitter" +version = "0.23.2" +description = "Python bindings to the Tree-sitter parsing library" +optional = false +python-versions = ">=3.9" +files = [ + {file = "tree-sitter-0.23.2.tar.gz", hash = "sha256:66bae8dd47f1fed7bdef816115146d3a41c39b5c482d7bad36d9ba1def088450"}, + {file = "tree_sitter-0.23.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3a937f5d8727bc1c74c4bf2a9d1c25ace049e8628273016ad0d45914ae904e10"}, + {file = "tree_sitter-0.23.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2c7eae7fe2af215645a38660d2d57d257a4c461fe3ec827cca99a79478284e80"}, + {file = "tree_sitter-0.23.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a71d607595270b6870eaf778a1032d146b2aa79bfcfa60f57a82a7b7584a4c7"}, + {file = "tree_sitter-0.23.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fe9b9ea7a0aa23b52fd97354da95d1b2580065bc12a4ac868f9164a127211d6"}, + {file = "tree_sitter-0.23.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d74d00a8021719eae14d10d1b1e28649e15d8b958c01c2b2c3dad7a2ebc4dbae"}, + {file = "tree_sitter-0.23.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6de18d8d8a7f67ab71f472d1fcb01cc506e080cbb5e13d52929e4b6fdce6bbee"}, + {file = "tree_sitter-0.23.2-cp310-cp310-win_amd64.whl", hash = "sha256:12b60dca70d2282af942b650a6d781be487485454668c7c956338a367b98cdee"}, + {file = "tree_sitter-0.23.2-cp310-cp310-win_arm64.whl", hash = "sha256:3346a4dd0447a42aabb863443b0fd8c92b909baf40ed2344fae4b94b625d5955"}, + {file = "tree_sitter-0.23.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91fda41d4f8824335cc43c64e2c37d8089c8c563bd3900a512d2852d075af719"}, + {file = "tree_sitter-0.23.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:92b2b489d5ce54b41f94c6f23fbaf592bd6e84dc2877048fd1cb060480fa53f7"}, + {file = "tree_sitter-0.23.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64859bd4aa1567d0d6016a811b2b49c59d4a4427d096e3d8c84b2521455f62b7"}, + {file = "tree_sitter-0.23.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:614590611636044e071d3a0b748046d52676dbda3bc9fa431216231e11dd98f7"}, + {file = "tree_sitter-0.23.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:08466953c78ae57be61057188fb88c89791b0a562856010228e0ccf60e2ac453"}, + {file = "tree_sitter-0.23.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8a33f03a562de91f7fd05eefcedd8994a06cd44c62f7aabace811ad82bc11cbd"}, + {file = "tree_sitter-0.23.2-cp311-cp311-win_amd64.whl", hash = "sha256:03b70296b569ef64f7b92b42ca5da9bf86d81bee2afd480bea35092687f51dae"}, + {file = "tree_sitter-0.23.2-cp311-cp311-win_arm64.whl", hash = "sha256:7cb4bb953ea7c0b50eeafc4454783e030357179d2a93c3dd5ebed2da5588ddd0"}, + {file = "tree_sitter-0.23.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a014498b6a9e6003fae8c6eb72f5927d62da9dcb72b28b3ce8cd15c6ff6a6572"}, + {file = "tree_sitter-0.23.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:04f8699b131d4bcbe3805c37e4ef3d159ee9a82a0e700587625623999ba0ea53"}, + {file = "tree_sitter-0.23.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4471577df285059c71686ecb208bc50fb472099b38dcc8e849b0e86652891e87"}, + {file = "tree_sitter-0.23.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f342c925290dd4e20ecd5787ef7ae8749981597ab364783a1eb73173efe65226"}, + {file = "tree_sitter-0.23.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a4e9e53d07dd076bede72e4f7d3a0173d7b9ad6576572dd86da008a740a9bb22"}, + {file = "tree_sitter-0.23.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8caebe65bc358759dac2500d8f8feed3aed939c4ade9a684a1783fe07bc7d5db"}, + {file = "tree_sitter-0.23.2-cp312-cp312-win_amd64.whl", hash = "sha256:fc5a72eb50d43485000dbbb309acb350467b7467e66dc747c6bb82ce63041582"}, + {file = "tree_sitter-0.23.2-cp312-cp312-win_arm64.whl", hash = "sha256:a0320eb6c7993359c5f7b371d22719ccd273f440d41cf1bd65dac5e9587f2046"}, + {file = "tree_sitter-0.23.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eff630dddee7ba05accb439b17e559e15ce13f057297007c246237ceb6306332"}, + {file = "tree_sitter-0.23.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4780ba8f3894f2dea869fad2995c2aceab3fd5ab9e6a27c45475d2acd7f7e84e"}, + {file = "tree_sitter-0.23.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0b609460b8e3e256361fb12e94fae5b728cb835b16f0f9d590b5aadbf9d109b"}, + {file = "tree_sitter-0.23.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d070d8eaeaeb36cf535f55e5578fddbfc3bf53c1980f58bf1a99d57466b3b5"}, + {file = "tree_sitter-0.23.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:878580b2ad5054c410ba3418edca4d34c81cc26706114d8f5b5541688bc2d785"}, + {file = "tree_sitter-0.23.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:29224bdc2a3b9af535b7725e249d3ee291b2e90708e82832e73acc175e40dc48"}, + {file = "tree_sitter-0.23.2-cp313-cp313-win_amd64.whl", hash = "sha256:c58d89348162fbc3aea1fe6511a66ee189fc0e4e4bbe937026f29e4ecef17763"}, + {file = "tree_sitter-0.23.2-cp313-cp313-win_arm64.whl", hash = "sha256:0ff2037be5edab7801de3f6a721b9cf010853f612e2008ee454e0e0badb225a6"}, + {file = "tree_sitter-0.23.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a5db8e585205faef8bf219da77d8993e2ef04d08eda2e3c8ad7e4df8297ee344"}, + {file = "tree_sitter-0.23.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9dbd110a30cf28be5da734ae4cd0e9031768228dbf6a79f2973962aa51de4ec7"}, + {file = "tree_sitter-0.23.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569514b9a996a0fd458b3a891c46ca125298be0c03cf82f2b6f0c13d5d8f25dc"}, + {file = "tree_sitter-0.23.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a357ed98a74e47787b812df99a74a2c35c0fe11e55c2095cc01d1cad144ef552"}, + {file = "tree_sitter-0.23.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c2dfb8e8f760f4cc67888d03ef9e2dbd3353245f67f5efba375c2a14d944ac0e"}, + {file = "tree_sitter-0.23.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3ead958df87a21d706903987e665e9e0e5df7b2c5021ff69ea349826840adc6a"}, + {file = "tree_sitter-0.23.2-cp39-cp39-win_amd64.whl", hash = "sha256:611cae16be332213c0e6ece72c0bfca202e30ff320a8b309b1526c6cb79ee4ba"}, + {file = "tree_sitter-0.23.2-cp39-cp39-win_arm64.whl", hash = "sha256:b848e0fdd522fbb8888cdb4f4d93f8fad97ae10d70c122fb922e51363c7febcd"}, +] + +[package.extras] +docs = ["sphinx (>=7.3,<8.0)", "sphinx-book-theme"] +tests = ["tree-sitter-html (>=0.23.0)", "tree-sitter-javascript (>=0.23.0)", "tree-sitter-json (>=0.23.0)", "tree-sitter-python (>=0.23.0)", "tree-sitter-rust (>=0.23.0)"] + +[[package]] +name = "tree-sitter-bash" +version = "0.23.3" +description = "Bash grammar for tree-sitter" +optional = false +python-versions = ">=3.9" +files = [ + {file = "tree_sitter_bash-0.23.3-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c1ee7a46fcbfca9937d01056be756631762f53c5afdb8c4ab64eb9fed060896b"}, + {file = "tree_sitter_bash-0.23.3-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:5a090118e887bf667d82ae445794906186216f5500e0d2cd58eb499f7502dc57"}, + {file = "tree_sitter_bash-0.23.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa4b5dde719291eea3a81b1f9ece6afeee2deadc2b2f769bee92f955da7595cf"}, + {file = "tree_sitter_bash-0.23.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff7bffc3d594e7f1054de051e19df1b24082963598a175dda64083c6b3eea1a"}, + {file = "tree_sitter_bash-0.23.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4427baccbd7549a2ebb1859b6d42cdab0739c05d53c2b3daad9cadc069a7b3f6"}, + {file = "tree_sitter_bash-0.23.3-cp39-abi3-win_amd64.whl", hash = "sha256:525c5cce28a7c5624fb016ac8f3ae33d32968567b718f7878c6351229d2e8394"}, + {file = "tree_sitter_bash-0.23.3-cp39-abi3-win_arm64.whl", hash = "sha256:1f703d1bf6235355f6c900be64bf9f61fc4b1d0cfed6829b4eeb74a6b41ea910"}, + {file = "tree_sitter_bash-0.23.3.tar.gz", hash = "sha256:7b15ed89a1ea8e3e3c2399758746413e464d4c1c3a6d3b75d643ae2bc2fb356b"}, +] + +[package.extras] +core = ["tree-sitter (>=0.22,<1.0)"] + +[[package]] +name = "tree-sitter-css" +version = "0.23.1" +description = "CSS grammar for tree-sitter" +optional = false +python-versions = ">=3.9" +files = [ + {file = "tree_sitter_css-0.23.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6b38462fef7d14b0bfa6e542faab7d3cfd267b8dc138efcf6e2cee11f6988084"}, + {file = "tree_sitter_css-0.23.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:33239e810c518b27fa7b4592d31f6cb63c43d4ea55532b4eb346ac4c9974a7f4"}, + {file = "tree_sitter_css-0.23.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c4cefceb654f89de8e79563d960f87b9a4680f288d87e20bacca7c339392070"}, + {file = "tree_sitter_css-0.23.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6524a5d097224128c9cda797c09f5704af0705e0ff272cf2f41ec192aa06aa62"}, + {file = "tree_sitter_css-0.23.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2999be3784999ced8b8d6a4470f0aec28cdc42b31fd9861041a70c834a2c8850"}, + {file = "tree_sitter_css-0.23.1-cp39-abi3-win_amd64.whl", hash = "sha256:8824f079e7454491347eda4cdbf9cde606c4e5de518cc85bb69cd9dfd67b8982"}, + {file = "tree_sitter_css-0.23.1-cp39-abi3-win_arm64.whl", hash = "sha256:6ff44819511fe517f6d32f1d8a3563da30093ca155dd1198585819598e83d755"}, + {file = "tree_sitter_css-0.23.1.tar.gz", hash = "sha256:a5dadf23e201f05606feaa638d0e423050a3d56cea2324c8859857fbbc3f69e8"}, +] + +[package.extras] +core = ["tree-sitter (>=0.22,<1.0)"] + +[[package]] +name = "tree-sitter-go" +version = "0.23.4" +description = "Go grammar for tree-sitter" +optional = false +python-versions = ">=3.9" +files = [ + {file = "tree_sitter_go-0.23.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c9320f87a05cd47fa0f627b9329bbc09b7ed90de8fe4f5882aed318d6e19962d"}, + {file = "tree_sitter_go-0.23.4-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:914e63d16b36ab0e4f52b031e574b82d17d0bbfecca138ae83e887a1cf5b71ac"}, + {file = "tree_sitter_go-0.23.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:330ecbb38d6ea4ef41eba2d473056889705e64f6a51c2fb613de05b1bcb5ba22"}, + {file = "tree_sitter_go-0.23.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd14d23056ae980debfccc0db67d0a168da03792ca2968b1b5dd58ce288084e7"}, + {file = "tree_sitter_go-0.23.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c3b40912487fdb78c4028860dd79493a521ffca0104f209849823358db3618a0"}, + {file = "tree_sitter_go-0.23.4-cp39-abi3-win_amd64.whl", hash = "sha256:ae4b231cad2ef76401d33617879cda6321c4d0853f7fd98cb5654c50a218effb"}, + {file = "tree_sitter_go-0.23.4-cp39-abi3-win_arm64.whl", hash = "sha256:2ac907362a3c347145dc1da0858248546500a323de90d2cb76d2a3fdbfc8da25"}, + {file = "tree_sitter_go-0.23.4.tar.gz", hash = "sha256:0ebff99820657066bec21690623a14c74d9e57a903f95f0837be112ddadf1a52"}, +] + +[package.extras] +core = ["tree-sitter (>=0.22,<1.0)"] + +[[package]] +name = "tree-sitter-html" +version = "0.23.2" +description = "HTML grammar for tree-sitter" +optional = false +python-versions = ">=3.9" +files = [ + {file = "tree_sitter_html-0.23.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9e1641d5edf5568a246c6c47b947ed524b5bf944664e6473b21d4ae568e28ee9"}, + {file = "tree_sitter_html-0.23.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:3d0a83dd6cd1c7d4bcf6287b5145c92140f0194f8516f329ae8b9e952fbfa8ff"}, + {file = "tree_sitter_html-0.23.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81b3775732fffc0abd275a419ef018fd4c1ad4044b2a2e422f3378d93c30eded"}, + {file = "tree_sitter_html-0.23.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bdaa7ac5030d416aea0c512d4810ef847bbbd62d61e3d213f370b64ce147293"}, + {file = "tree_sitter_html-0.23.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d2e9631b66041a4fd792d7f79a0c4128adb3bfc71f3dcb7e1a3eab5dbee77d67"}, + {file = "tree_sitter_html-0.23.2-cp39-abi3-win_amd64.whl", hash = "sha256:85095f49f9e57f0ac9087a3e830783352c8447fdda55b1c1139aa47e5eaa0e21"}, + {file = "tree_sitter_html-0.23.2-cp39-abi3-win_arm64.whl", hash = "sha256:0f65ed9e877144d0f04ade5644e5b0e88bf98a9e60bce65235c99905623e2f1a"}, + {file = "tree_sitter_html-0.23.2.tar.gz", hash = "sha256:bc9922defe23144d9146bc1509fcd00d361bf6b3303f9effee6532c6a0296961"}, +] + +[package.extras] +core = ["tree-sitter (>=0.22,<1.0)"] + +[[package]] +name = "tree-sitter-java" +version = "0.23.4" +description = "Java grammar for tree-sitter" +optional = false +python-versions = ">=3.9" +files = [ + {file = "tree_sitter_java-0.23.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:91cf4507a64529737639941e82d901891edb33e594daced2d75de829ac50962f"}, + {file = "tree_sitter_java-0.23.4-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:3f74e9c2f01b4c511cdf99e5a947642f37e8d621804d65ec7858a321bdcb1ba6"}, + {file = "tree_sitter_java-0.23.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:874d489bffc8c418a48f899ae75d3b774b999013840b6591f60b882ff03f9aa3"}, + {file = "tree_sitter_java-0.23.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79107666639d6a531565fbf6f4a00a06df1589c61101b832f5315f1969899184"}, + {file = "tree_sitter_java-0.23.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ff8aaccedaf484db9be41ddf1c44cdc4805556f0607de29c22300f14db693616"}, + {file = "tree_sitter_java-0.23.4-cp39-abi3-win_amd64.whl", hash = "sha256:e7477ffd19e0d0121e340fd7320efe028d2ae04784ffe13bec9a189cdeff0744"}, + {file = "tree_sitter_java-0.23.4-cp39-abi3-win_arm64.whl", hash = "sha256:c7688f1b004bdf390e7c0bf05384485866086675bf24caf4b2781353552b48a1"}, + {file = "tree_sitter_java-0.23.4.tar.gz", hash = "sha256:611857a92a232143ee20a4eb7cb46d6ff212b2d21cc7e3377b2943282c16a366"}, +] + +[package.extras] +core = ["tree-sitter (>=0.22,<1.0)"] + +[[package]] +name = "tree-sitter-javascript" +version = "0.23.1" +description = "JavaScript grammar for tree-sitter" +optional = false +python-versions = ">=3.9" +files = [ + {file = "tree_sitter_javascript-0.23.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6ca583dad4bd79d3053c310b9f7208cd597fd85f9947e4ab2294658bb5c11e35"}, + {file = "tree_sitter_javascript-0.23.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:94100e491a6a247aa4d14caf61230c171b6376c863039b6d9cd71255c2d815ec"}, + {file = "tree_sitter_javascript-0.23.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a6bc1055b061c5055ec58f39ee9b2e9efb8e6e0ae970838af74da0afb811f0a"}, + {file = "tree_sitter_javascript-0.23.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:056dc04fb6b24293f8c5fec43c14e7e16ba2075b3009c643abf8c85edc4c7c3c"}, + {file = "tree_sitter_javascript-0.23.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a11ca1c0f736da42967586b568dff8a465ee148a986c15ebdc9382806e0ce871"}, + {file = "tree_sitter_javascript-0.23.1-cp39-abi3-win_amd64.whl", hash = "sha256:041fa22b34250ea6eb313d33104d5303f79504cb259d374d691e38bbdc49145b"}, + {file = "tree_sitter_javascript-0.23.1-cp39-abi3-win_arm64.whl", hash = "sha256:eb28130cd2fb30d702d614cbf61ef44d1c7f6869e7d864a9cc17111e370be8f7"}, + {file = "tree_sitter_javascript-0.23.1.tar.gz", hash = "sha256:b2059ce8b150162cda05a457ca3920450adbf915119c04b8c67b5241cd7fcfed"}, +] + +[package.extras] +core = ["tree-sitter (>=0.22,<1.0)"] + +[[package]] +name = "tree-sitter-json" +version = "0.24.8" +description = "JSON grammar for tree-sitter" +optional = false +python-versions = ">=3.9" +files = [ + {file = "tree_sitter_json-0.24.8-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:59ac06c6db1877d0e2076bce54a5fddcdd2fc38ca778905662e80fa9ffcea2ab"}, + {file = "tree_sitter_json-0.24.8-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:62b4c45b561db31436a81a3f037f71ec29049f4fc9bf5269b6ec3ebaaa35a1cd"}, + {file = "tree_sitter_json-0.24.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8627f7d375fda9fc193ebee368c453f374f65c2f25c58b6fea4e6b49a7fccbc"}, + {file = "tree_sitter_json-0.24.8-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85cca779872f7278f3a74eb38533d34b9c4de4fd548615e3361fa64fe350ad0a"}, + {file = "tree_sitter_json-0.24.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:deeb45850dcc52990fbb52c80196492a099e3fa3512d928a390a91cf061068cc"}, + {file = "tree_sitter_json-0.24.8-cp39-abi3-win_amd64.whl", hash = "sha256:e4849a03cd7197267b2688a4506a90a13568a8e0e8588080bd0212fcb38974e3"}, + {file = "tree_sitter_json-0.24.8-cp39-abi3-win_arm64.whl", hash = "sha256:591e0096c882d12668b88f30d3ca6f85b9db3406910eaaab6afb6b17d65367dd"}, + {file = "tree_sitter_json-0.24.8.tar.gz", hash = "sha256:ca8486e52e2d261819311d35cf98656123d59008c3b7dcf91e61d2c0c6f3120e"}, +] + +[package.extras] +core = ["tree-sitter (>=0.22,<1.0)"] + +[[package]] +name = "tree-sitter-markdown" +version = "0.3.2" +description = "Markdown grammar for tree-sitter" +optional = false +python-versions = ">=3.9" +files = [ + {file = "tree_sitter_markdown-0.3.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2a0d60ee5185fbc20c6f3e7744348956a62f8bc9ae85b574251632e3c2220c77"}, + {file = "tree_sitter_markdown-0.3.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0a72f199966380e18f668abb3e9d0a75569c8a292967deefc432282e253f9f84"}, + {file = "tree_sitter_markdown-0.3.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3555e5732223b030c8b5742fb565b4528566d96700ea7de9a2902e51fb91be21"}, + {file = "tree_sitter_markdown-0.3.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7fd68cbbccd917067696952773a553ef4d604017d9332b7a6f6a05549f1c0a3"}, + {file = "tree_sitter_markdown-0.3.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a693f0251f13fa925631fdc9e30f2435f5569d1b3b3d2c3d3b24060d3234f98a"}, + {file = "tree_sitter_markdown-0.3.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:507b9d99500dcbeefb069815b689c3dd36892375e878669f98125d0cd0a814a4"}, + {file = "tree_sitter_markdown-0.3.2-cp39-abi3-win_amd64.whl", hash = "sha256:a89a374920d648599d07036e0ec979f54fde684ddaee1bddf406339c51565cbc"}, + {file = "tree_sitter_markdown-0.3.2-cp39-abi3-win_arm64.whl", hash = "sha256:017e7c09c44861f35a4499564ecd0d97a25341905dc9d0dec2e6a38ee4e6b52d"}, + {file = "tree_sitter_markdown-0.3.2.tar.gz", hash = "sha256:64501234ae4ce5429551624e2fd675008cf86824bd8b9352223653e39218e753"}, +] + +[package.extras] +core = ["tree-sitter (>=0.23,<1.0)"] + +[[package]] +name = "tree-sitter-python" +version = "0.23.5" +description = "Python grammar for tree-sitter" +optional = false +python-versions = ">=3.9" +files = [ + {file = "tree_sitter_python-0.23.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:095d104f7f13694ee95ca8540b39a77a57bf9f037797c2658b8400c5b5ece117"}, + {file = "tree_sitter_python-0.23.5-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:a27f874083b2a204a5c1aa85ebe15e23a441816ee60a6bcf6a4daad7044176ca"}, + {file = "tree_sitter_python-0.23.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cb5aebdd64a30e3557e481bd0f6d553a1570e088626529fac5a70165a67b84e"}, + {file = "tree_sitter_python-0.23.5-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040c0cbebf127b578e183ce084feb8f0ca7e9e26bcd0a4f6cf1a8f47e13b0b5a"}, + {file = "tree_sitter_python-0.23.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c62262e96e67f6f63467f74b37c6fa4b618b3d2ddd7ad16280c101d1f9be8a8a"}, + {file = "tree_sitter_python-0.23.5-cp39-abi3-win_amd64.whl", hash = "sha256:2b52ec8279193b0f8979aef4b0ac60c99e2856ab02eeeb1a62b55a03c012e3fd"}, + {file = "tree_sitter_python-0.23.5-cp39-abi3-win_arm64.whl", hash = "sha256:efd1a1a44322c46b27ff439e6103d8199d53ffc38c021e7856067c9c90617460"}, + {file = "tree_sitter_python-0.23.5.tar.gz", hash = "sha256:bd18325d93d633b4d411f24bb5e7d34ee653cd3254e5963fb3c2738ee3c4a1ee"}, +] + +[package.extras] +core = ["tree-sitter (>=0.22,<1.0)"] + +[[package]] +name = "tree-sitter-regex" +version = "0.24.3" +description = "Regex grammar for tree-sitter" +optional = false +python-versions = ">=3.9" +files = [ + {file = "tree_sitter_regex-0.24.3-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:16ded552d0f43dda608cec078b4a63f1dfa53c793775ba1a1bb06b2539b94fff"}, + {file = "tree_sitter_regex-0.24.3-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0a26bf77f7a8aa070299246eb3a29276030481ff380346c4085a97e448c34570"}, + {file = "tree_sitter_regex-0.24.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6abbf0708dbef6d70444bf9482528b39bae255ce59ed147dd1a731127e49a8da"}, + {file = "tree_sitter_regex-0.24.3-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdb7134e1c954a18c321053f753da1c5aea9dc6d92e814796d34d03c4b76c012"}, + {file = "tree_sitter_regex-0.24.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bf21ff69b8356d83b19ece6468ffe855d397eeeee1e34e8a11de0dc2be5ee896"}, + {file = "tree_sitter_regex-0.24.3-cp39-abi3-win_amd64.whl", hash = "sha256:7cb8f173054859a3d8b8f111833c638b1f1fef878fafb191e6974bbcaf5e930f"}, + {file = "tree_sitter_regex-0.24.3-cp39-abi3-win_arm64.whl", hash = "sha256:2eb9001e9ccb97d3d608e07f524335b0e5614abf67a004004c6c90abf0feb7cf"}, + {file = "tree_sitter_regex-0.24.3.tar.gz", hash = "sha256:58bb63f9e0ff01430da56ff158bddcb1b62a31f115abdf93cc6af76cc3aff86e"}, +] + +[package.extras] +core = ["tree-sitter (>=0.22,<1.0)"] + +[[package]] +name = "tree-sitter-rust" +version = "0.23.2" +description = "Rust grammar for tree-sitter" +optional = false +python-versions = ">=3.9" +files = [ + {file = "tree_sitter_rust-0.23.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b6b26a4c07ddc243f3701450ff34093b8e3b08f14d269db2d049c625d151677c"}, + {file = "tree_sitter_rust-0.23.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:c6224f608df559d75425e5ef428f635b9fb87d7aa8716444915ee67ec6955085"}, + {file = "tree_sitter_rust-0.23.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deced590a85ce848cda56f33728bad93b95827c1e3c736b707b24fb4280b3788"}, + {file = "tree_sitter_rust-0.23.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:540cf826932fe7cfd800361e368617e138c3d7914fad3b90786b7505af216be6"}, + {file = "tree_sitter_rust-0.23.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:880be40b220e87105b60db48c57cdd8019b5039b324afb1d643fa9c2fc187873"}, + {file = "tree_sitter_rust-0.23.2-cp39-abi3-win_amd64.whl", hash = "sha256:d8e0bea4fd76fc8b325247f3d1bb3dc2707db7dd3818b02c251efdea1b47909c"}, + {file = "tree_sitter_rust-0.23.2-cp39-abi3-win_arm64.whl", hash = "sha256:3ea49daa887ad59230758e7a96432193af4a2c7183781e1e85c35d4f8cb30b6b"}, + {file = "tree_sitter_rust-0.23.2.tar.gz", hash = "sha256:9088a0e0342d3de2749088811f5561994423cb10dab5ad3251003dffaa0a1bd1"}, +] + +[package.extras] +core = ["tree-sitter (>=0.22,<1.0)"] + +[[package]] +name = "tree-sitter-sql" +version = "0.3.7" +description = "Tree-sitter Grammar for SQL" +optional = false +python-versions = ">=3.9" +files = [ + {file = "tree_sitter_sql-0.3.7-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:f3f8427328bd8b4ee02ab50d71bfc515937c037b8a03dcf54b8c98403d804269"}, + {file = "tree_sitter_sql-0.3.7-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:97ad55611f7d777b08a30d60150e1c44100ac2759a341b1cf1ffa2f97f20259e"}, + {file = "tree_sitter_sql-0.3.7-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa7beae1c2e841edc6de5a80d3ee6d401b579d9d27ce9553f2c569bef4b95b8f"}, + {file = "tree_sitter_sql-0.3.7-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5fc2daa8fc8d49265327ddaff0b5bbda5a6a0e37d05b157fdbcb2530f1e96e8"}, + {file = "tree_sitter_sql-0.3.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5f3c34121b625ee8f43e6ccffaca5205b206afa31592aedd1f078b4f6f4cb321"}, + {file = "tree_sitter_sql-0.3.7-cp38-abi3-win_amd64.whl", hash = "sha256:09e4af2b4c32b09e602c83f1e584e5f390901fce929fa7c3da2ddf416e30681e"}, + {file = "tree_sitter_sql-0.3.7-cp38-abi3-win_arm64.whl", hash = "sha256:e7b09235e5492ac8f71abaeb078e28dbfa94881998a5fbf22c659da49165054c"}, + {file = "tree_sitter_sql-0.3.7.tar.gz", hash = "sha256:5eb671ad597e6245d96aa44fd584c990d3eaffe80faddf941bfe8ebee6a8e2dd"}, +] + +[package.extras] +core = ["tree-sitter (>=0.22,<1.0)"] + +[[package]] +name = "tree-sitter-toml" +version = "0.7.0" +description = "TOML grammar for tree-sitter" +optional = false +python-versions = ">=3.9" +files = [ + {file = "tree_sitter_toml-0.7.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b9ae5c3e7c5b6bb05299dd73452ceafa7fa0687d5af3012332afa7757653b676"}, + {file = "tree_sitter_toml-0.7.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:18be09538e9775cddc0290392c4e2739de2201260af361473ca60b5c21f7bd22"}, + {file = "tree_sitter_toml-0.7.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a045e0acfcf91b7065066f7e51ea038ed7385c1e35e7e8fae18f252d3f8adb8c"}, + {file = "tree_sitter_toml-0.7.0-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a2f8cf9d73f07b6628093b35e5c5fbac039247e32cb075eaa5289a5914e73af"}, + {file = "tree_sitter_toml-0.7.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:860ffa4513b2dc3083d8e412bd815a350b0a9490624b37e7c8f6ed5c6f9ce63c"}, + {file = "tree_sitter_toml-0.7.0-cp39-abi3-win_amd64.whl", hash = "sha256:2760a04f06937b01b1562a2135cd7e8207e399e73ef75bbebc77e37b1ad3b15d"}, + {file = "tree_sitter_toml-0.7.0-cp39-abi3-win_arm64.whl", hash = "sha256:fd00fd8a51c65aa19c40539431cb1773d87c30af5757b4041fa6c229058420b4"}, + {file = "tree_sitter_toml-0.7.0.tar.gz", hash = "sha256:29e257612fa8f0c1fcbc4e7e08ddc561169f1725265302e64d81086354144a70"}, +] + +[package.extras] +core = ["tree-sitter (>=0.22,<1.0)"] + +[[package]] +name = "tree-sitter-xml" +version = "0.7.0" +description = "XML & DTD grammars for tree-sitter" +optional = false +python-versions = ">=3.9" +files = [ + {file = "tree_sitter_xml-0.7.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cc3e516d4c1e0860fb22172c172148debb825ba638971bc48bad15b22e5b0bae"}, + {file = "tree_sitter_xml-0.7.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0674fdf4cc386e4d323cb287d3b072663de0f20a9e9af5d5e09821aae56a9e5c"}, + {file = "tree_sitter_xml-0.7.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c0fe5f2d6cc09974c8375c8ea9b24909f493b5bf04aacdc4c694b5d2ae6b040"}, + {file = "tree_sitter_xml-0.7.0-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd3209516a4d84dff90bc91d2ad2ce246de8504cede4358849687fa8e71536e7"}, + {file = "tree_sitter_xml-0.7.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:87578e15fa55f44ecd9f331233b6f8a2cbde3546b354c830ecb862a632379455"}, + {file = "tree_sitter_xml-0.7.0-cp39-abi3-win_amd64.whl", hash = "sha256:9ba2dafc6ce9feaf4ccc617d3aeea57f8e0ca05edad34953e788001ebff79133"}, + {file = "tree_sitter_xml-0.7.0-cp39-abi3-win_arm64.whl", hash = "sha256:fc759f710a8fd7a01c23e2d7cb013679199045bea3dc0e5151650a11322aaf40"}, + {file = "tree_sitter_xml-0.7.0.tar.gz", hash = "sha256:ab0ff396f20230ad8483d968151ce0c35abe193eb023b20fbd8b8ce4cf9e9f61"}, +] + +[package.extras] +core = ["tree-sitter (>=0.22,<1.0)"] + +[[package]] +name = "tree-sitter-yaml" +version = "0.7.0" +description = "YAML grammar for tree-sitter" +optional = false +python-versions = ">=3.9" +files = [ + {file = "tree_sitter_yaml-0.7.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:e21553ac190ae05bf82796df8beb4d9158ba195b5846018cb36fbc3a35bd0679"}, + {file = "tree_sitter_yaml-0.7.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:c022054f1f9b54201082ea83073a6c24c42d0436ad8ee99ff2574cba8f928c28"}, + {file = "tree_sitter_yaml-0.7.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cd1725142f19e41c51d27c99cfc60780f596e069eb181cfa6433d993a19aa3d"}, + {file = "tree_sitter_yaml-0.7.0-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d1b268378254f75bb27396d83c96d886ccbfcda6bd8c2778e94e3e1d2459085"}, + {file = "tree_sitter_yaml-0.7.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:27c2e7f4f49ddf410003abbb82a7b00ec77ea263d8ef08dbce1a15d293eed2fd"}, + {file = "tree_sitter_yaml-0.7.0-cp39-abi3-win_amd64.whl", hash = "sha256:98dce0d6bc376f842cfb1d3c32512eea95b37e61cd2c87074bb4b05c999917c8"}, + {file = "tree_sitter_yaml-0.7.0-cp39-abi3-win_arm64.whl", hash = "sha256:f0f8d8e05fa8e70f08d0f18a209d6026e171844f4ea7090e7c779b9c375b3a31"}, + {file = "tree_sitter_yaml-0.7.0.tar.gz", hash = "sha256:9c8bb17d9755c3b0e757260917240c0d19883cd3b59a5d74f205baa8bf8435a4"}, +] + +[package.extras] +core = ["tree-sitter (>=0.22,<1.0)"] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "uc-micro-py" +version = "1.0.3" +description = "Micro subset of unicode data files for linkify-it-py projects." +optional = false +python-versions = ">=3.7" +files = [ + {file = "uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a"}, + {file = "uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5"}, +] + +[package.extras] +test = ["coverage", "pytest", "pytest-cov"] + +[[package]] +name = "virtualenv" +version = "20.28.0" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +files = [ + {file = "virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0"}, + {file = "virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "yarl" +version = "1.18.3" +description = "Yet another URL library" +optional = false +python-versions = ">=3.9" +files = [ + {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34"}, + {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7"}, + {file = "yarl-1.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690"}, + {file = "yarl-1.18.3-cp310-cp310-win32.whl", hash = "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6"}, + {file = "yarl-1.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a"}, + {file = "yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1"}, + {file = "yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285"}, + {file = "yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2"}, + {file = "yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8"}, + {file = "yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d"}, + {file = "yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c"}, + {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:61e5e68cb65ac8f547f6b5ef933f510134a6bf31bb178be428994b0cb46c2a04"}, + {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe57328fbc1bfd0bd0514470ac692630f3901c0ee39052ae47acd1d90a436719"}, + {file = "yarl-1.18.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a440a2a624683108a1b454705ecd7afc1c3438a08e890a1513d468671d90a04e"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c7907c8548bcd6ab860e5f513e727c53b4a714f459b084f6580b49fa1b9cee"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4f6450109834af88cb4cc5ecddfc5380ebb9c228695afc11915a0bf82116789"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9ca04806f3be0ac6d558fffc2fdf8fcef767e0489d2684a21912cc4ed0cd1b8"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77a6e85b90a7641d2e07184df5557132a337f136250caafc9ccaa4a2a998ca2c"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6333c5a377c8e2f5fae35e7b8f145c617b02c939d04110c76f29ee3676b5f9a5"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0b3c92fa08759dbf12b3a59579a4096ba9af8dd344d9a813fc7f5070d86bbab1"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4ac515b860c36becb81bb84b667466885096b5fc85596948548b667da3bf9f24"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:045b8482ce9483ada4f3f23b3774f4e1bf4f23a2d5c912ed5170f68efb053318"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:a4bb030cf46a434ec0225bddbebd4b89e6471814ca851abb8696170adb163985"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:54d6921f07555713b9300bee9c50fb46e57e2e639027089b1d795ecd9f7fa910"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1d407181cfa6e70077df3377938c08012d18893f9f20e92f7d2f314a437c30b1"}, + {file = "yarl-1.18.3-cp39-cp39-win32.whl", hash = "sha256:ac36703a585e0929b032fbaab0707b75dc12703766d0b53486eabd5139ebadd5"}, + {file = "yarl-1.18.3-cp39-cp39-win_amd64.whl", hash = "sha256:ba87babd629f8af77f557b61e49e7c7cac36f22f871156b91e10a6e9d4f829e9"}, + {file = "yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b"}, + {file = "yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +propcache = ">=0.2.0" + +[metadata] +lock-version = "2.0" +python-versions = ">=3.9,<3.14" +content-hash = "7e3f85218393ee605e36a620bb089ff426faaf3d3044d94573c0f6cb7c98e7e6" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f7ec345 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,67 @@ +[tool.poetry] +name = "textual-textarea" +version = "0.15.0" +description = "A text area (multi-line input) with syntax highlighting for Textual" +authors = ["Ted Conbeer "] +license = "MIT" +homepage = "https://github.com/tconbeer/textual-textarea" +repository = "https://github.com/tconbeer/textual-textarea" +readme = "README.md" +packages = [{ include = "textual_textarea", from = "src" }] + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry.dependencies] +python = ">=3.9,<3.14" +textual = { version = ">=0.89.1,<2.0", extras = ["syntax"] } +pyperclip = "^1.9.0" + +[tool.poetry.group.dev.dependencies] +pre-commit = "^3.3.1" +textual = "0.89.1" +textual-dev = "^1.2.1" +pyinstrument = "^5" + +[tool.poetry.group.static.dependencies] +ruff = "^0.5" +mypy = "^1.10.0" + +[tool.poetry.group.test.dependencies] +pytest = ">=7.3.1,<9.0.0" +pytest-asyncio = "^0.21" + +[tool.ruff.lint] +select = ["A", "B", "E", "F", "I"] + +[tool.mypy] +python_version = "3.9" +files = [ + "src/textual_textarea/**/*.py", + "tests/**/*.py", +] +mypy_path = "src:stubs" + +show_column_numbers = true + +# show error messages from unrelated files +follow_imports = "normal" + +# be strict +disallow_untyped_calls = true +disallow_untyped_defs = true +check_untyped_defs = true +disallow_untyped_decorators = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +strict_optional = true + +warn_return_any = true +warn_no_return = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_unused_configs = true + +no_implicit_reexport = true +strict_equality = true \ No newline at end of file diff --git a/src/scripts/__init__.py b/src/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/scripts/profile_startup.py b/src/scripts/profile_startup.py new file mode 100644 index 0000000..f35ec32 --- /dev/null +++ b/src/scripts/profile_startup.py @@ -0,0 +1,23 @@ +from textual.app import App, ComposeResult +from textual_textarea import TextEditor + + +class TextApp(App, inherit_bindings=False): + def compose(self) -> ComposeResult: + self.ta = TextEditor( + text="class TextApp(App):", + language="python", + theme="monokai", + use_system_clipboard=True, + id="ta", + ) + yield self.ta + + def on_mount(self) -> None: + self.ta.focus() + self.exit() + + +if __name__ == "__main__": + app = TextApp() + app.run() diff --git a/src/scripts/sample_code.py b/src/scripts/sample_code.py new file mode 100644 index 0000000..22c5d5a --- /dev/null +++ b/src/scripts/sample_code.py @@ -0,0 +1,20 @@ +from textual.app import App, ComposeResult +from textual_textarea import TextEditor + + +class TextApp(App, inherit_bindings=False): + def compose(self) -> ComposeResult: + self.editor = TextEditor( + language="python", + theme="monokai", + use_system_clipboard=True, + ) + yield self.editor + + def on_mount(self) -> None: + self.editor.focus() + + +if __name__ == "__main__": + app = TextApp() + app.run() diff --git a/src/scripts/screenshot.py b/src/scripts/screenshot.py new file mode 100644 index 0000000..feb30ce --- /dev/null +++ b/src/scripts/screenshot.py @@ -0,0 +1,32 @@ +import asyncio +from pathlib import Path + +from textual.app import App, ComposeResult +from textual.widgets.text_area import Selection +from textual_textarea import TextEditor + +contents = (Path(__file__).parent / "sample_code.py").open("r").read() + + +class TextApp(App, inherit_bindings=False): + def compose(self) -> ComposeResult: + self.editor = TextEditor( + language="python", + theme="monokai", + use_system_clipboard=True, + ) + yield self.editor + + def on_mount(self) -> None: + self.editor.focus() + + +async def take_screenshot() -> None: + app = TextApp() + async with app.run_test(size=(80, 24)): + app.editor.text = contents + app.editor.selection = Selection((7, 12), (8, 12)) + app.save_screenshot("textarea.svg") + + +asyncio.run(take_screenshot()) diff --git a/src/textual_textarea/__init__.py b/src/textual_textarea/__init__.py new file mode 100644 index 0000000..14582bb --- /dev/null +++ b/src/textual_textarea/__init__.py @@ -0,0 +1,15 @@ +from textual_textarea.messages import ( + TextAreaClipboardError, + TextAreaSaved, + TextAreaThemeError, +) +from textual_textarea.path_input import PathInput +from textual_textarea.text_editor import TextEditor + +__all__ = [ + "TextEditor", + "PathInput", + "TextAreaClipboardError", + "TextAreaThemeError", + "TextAreaSaved", +] diff --git a/src/textual_textarea/__main__.py b/src/textual_textarea/__main__.py new file mode 100644 index 0000000..2b67f7e --- /dev/null +++ b/src/textual_textarea/__main__.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import sys + +from textual.app import App, ComposeResult +from textual.widgets import Footer, Placeholder + +from textual_textarea import TextEditor + + +class FocusablePlaceholder(Placeholder, can_focus=True): + pass + + +class TextApp(App, inherit_bindings=False): + BINDINGS = [("ctrl+q", "quit")] + CSS = """ + TextEditor { + height: 1fr; + } + Placeholder { + height: 0fr; + } + """ + + def compose(self) -> ComposeResult: + try: + language = sys.argv[1] + except IndexError: + language = "sql" + yield FocusablePlaceholder() + self.editor = TextEditor( + language=language, + use_system_clipboard=True, + id="ta", + ) + yield self.editor + yield Footer() + + def watch_theme(self, theme: str) -> None: + self.editor.theme = theme + + def on_mount(self) -> None: + self.theme = "gruvbox" + self.editor.focus() + + def _completer(prefix: str) -> list[tuple[tuple[str, str], str]]: + words = [ + "satisfy", + "season", + "second", + "seldom", + "select", + "self", + "separate", + "set", + "space", + "super", + "supercalifragilisticexpialadocioussupercalifragilisticexpialadocious", + ] + return [((w, "word"), w) for w in words if w.startswith(prefix)] + + self.editor.word_completer = _completer + + +app = TextApp() +app.run() diff --git a/src/textual_textarea/autocomplete.py b/src/textual_textarea/autocomplete.py new file mode 100644 index 0000000..51fa81d --- /dev/null +++ b/src/textual_textarea/autocomplete.py @@ -0,0 +1,289 @@ +from __future__ import annotations + +from typing import Callable + +from rich.console import RenderableType +from rich.style import Style +from rich.text import Text +from textual import on, work +from textual.css.scalar import Scalar, ScalarOffset, Unit +from textual.events import Key, Resize +from textual.geometry import Size +from textual.message import Message +from textual.reactive import Reactive, reactive +from textual.widget import Widget +from textual.widgets import OptionList +from textual.widgets._option_list import NewOptionListContent +from textual.widgets.option_list import Option + +from textual_textarea.messages import TextAreaHideCompletionList + + +class Completion(Option): + def __init__( + self, + prompt: RenderableType, + id: str | None = None, # noqa: A002 + disabled: bool = False, + value: str | None = None, + ) -> None: + super().__init__(prompt, id, disabled) + self.value = value + + +class CompletionList(OptionList, can_focus=False, inherit_bindings=False): + COMPONENT_CLASSES = { + "completion-list--type-label", + "completion-list--type-label-highlighted", + } + DEFAULT_CSS = """ + CompletionList { + layer: overlay; + padding: 0; + border: none; + width: 40; + max-height: 8; + display: none; + } + CompletionList.open { + display: block; + } + CompletionList .completion-list--type-label { + color: $foreground-muted; + background: transparent; + } + """ + + class CompletionsReady(Message, bubble=False): + def __init__( + self, + prefix: str, + items: list[tuple[str, str]] | list[tuple[tuple[str, str], str]], + ) -> None: + super().__init__() + self.items = items + self.prefix = prefix + + INNER_CONTENT_WIDTH = 37 # should be 3 less than width for scroll bar. + is_open: Reactive[bool] = reactive(False) + cursor_offset: tuple[int, int] = (0, 0) + additional_x_offset: int = 0 + + def __init__( + self, + *content: NewOptionListContent, + name: str | None = None, + id: str | None = None, # noqa: A002 + classes: str | None = None, + disabled: bool = False, + ): + super().__init__( + *content, name=name, id=id, classes=classes, disabled=disabled, wrap=False + ) + + def set_offset(self, x_offset: int, y_offset: int) -> None: + """The CSS Offset of this widget from its parent.""" + self.styles.offset = ScalarOffset.from_offset( + ( + x_offset, + y_offset, + ) + ) + + @property + def x_offset(self) -> int: + """The x-coord of the CSS Offset of this widget from its parent.""" + return int(self.styles.offset.x.value) + + @property + def y_offset(self) -> int: + """The y-coord of the CSS Offset of this widget from its parent.""" + return int(self.styles.offset.y.value) + + @property + def parent_height(self) -> int: + """ + The content size height of the parent widget + """ + return self.parent_size.height + + @property + def parent_width(self) -> int: + """ + The content size height of the parent widget + """ + return self.parent_size.width + + @property + def parent_size(self) -> Size: + """ + The content size of the parent widget + """ + parent = self.parent + if isinstance(parent, Widget): + return parent.content_size + else: + return self.screen.content_size + + @on(CompletionsReady) + def populate_and_position_list(self, event: CompletionsReady) -> None: + event.stop() + self.clear_options() + type_label_style_full = self.get_component_rich_style( + "completion-list--type-label" + ) + type_label_fg_style = Style(color=type_label_style_full.color) + prompts = [ + Text.assemble(item[0][0], " ", (item[0][1], type_label_fg_style)) + if isinstance(item[0], tuple) + else Text.from_markup(item[0]) + for item in event.items + ] + + # if the completions' prompts are wider than the widget, + # we have to trunctate them + max_length = max(map(lambda x: x.cell_len, prompts)) + truncate_amount = max( + 0, + min( + max_length - self.INNER_CONTENT_WIDTH, + len(event.prefix) - 2, + ), + ) + if truncate_amount > 0: + additional_x_offset = truncate_amount - 1 + items = [ + Completion(prompt=f"…{prompt[truncate_amount:]}", value=item[1]) + for prompt, item in zip(prompts, event.items) + ] + else: + additional_x_offset = 0 + items = [ + Completion(prompt=prompt, value=item[1]) + for prompt, item in zip(prompts, event.items) + ] + + # set x offset if not already open. + if not self.is_open: + try: + x_offset = self._get_x_offset( + prefix_length=len(event.prefix), + additional_x_offset=additional_x_offset, + cursor_x=self.cursor_offset[0], + container_width=self.parent_width, + width=self._width, + ) + except ValueError: + x_offset = 0 + self.styles.width = self._parent_container_size.width + self.set_offset(x_offset, self.y_offset) + # adjust x offset if we have to due to truncation + elif additional_x_offset != self.additional_x_offset: + self.set_offset( + min( + self.x_offset + (additional_x_offset - self.additional_x_offset), + self.parent_width - self._width, + ), + self.y_offset, + ) + + self.add_options(items=items) + self.action_first() + self.additional_x_offset = additional_x_offset + self.is_open = True + + def watch_is_open(self, is_open: bool) -> None: + if not is_open: + self.remove_class("open") + self.additional_x_offset = 0 + return + + self.add_class("open") + self.styles.max_height = Scalar( + value=8.0, unit=Unit.CELLS, percent_unit=Unit.PERCENT + ) + + def on_resize(self, event: Resize) -> None: + try: + y_offset = self._get_y_offset( + cursor_y=self.cursor_offset[1], + height=event.size.height, + container_height=self.parent_height, + ) + except ValueError: + if self.styles.max_height is not None and self.styles.max_height.value > 1: + self.styles.max_height = Scalar( + value=self.styles.max_height.value - 1, + unit=self.styles.max_height.unit, + percent_unit=self.styles.max_height.percent_unit, + ) + else: + self.post_message(TextAreaHideCompletionList()) + else: + self.set_offset(self.x_offset, y_offset) + + @work(thread=True, exclusive=True, group="completers") + def show_completions( + self, + prefix: str, + completer: Callable[ + [str], list[tuple[str, str]] | list[tuple[tuple[str, str], str]] + ] + | None, + ) -> None: + matches = completer(prefix) if completer is not None else [] + if matches: + self.post_message(self.CompletionsReady(prefix=prefix, items=matches)) + else: + self.post_message(TextAreaHideCompletionList()) + + def process_keypress(self, event: Key) -> None: + if event.key in ("tab", "enter", "shift+tab"): + self.action_select() + elif event.key == "up": + self.action_cursor_up() + elif event.key == "down": + self.action_cursor_down() + elif event.key == "pageup": + self.action_page_up() + elif event.key == "pagedown": + self.action_page_down() + + @property + def _parent_container_size(self) -> Size: + return getattr(self.parent, "container_size", self.screen.container_size) + + @property + def _width(self) -> int: + if self.styles.width and self.styles.width.unit == Unit.CELLS: + return int(self.styles.width.value) + else: + return self.outer_size.width + + @staticmethod + def _get_x_offset( + prefix_length: int, + additional_x_offset: int, + cursor_x: int, + container_width: int, + width: int, + ) -> int: + x = cursor_x - prefix_length + additional_x_offset + max_x = container_width - width + if max_x < 0: + raise ValueError("doesn't fit") + + return min(x, max_x) + + @staticmethod + def _get_y_offset(cursor_y: int, height: int, container_height: int) -> int: + fits_above = height < cursor_y + 1 + fits_below = height < container_height - cursor_y + if fits_below: + y = cursor_y + 1 + elif fits_above: + y = cursor_y - height + else: + raise ValueError("Doesn't fit.") + + return y diff --git a/src/textual_textarea/cancellable_input.py b/src/textual_textarea/cancellable_input.py new file mode 100644 index 0000000..ab23b74 --- /dev/null +++ b/src/textual_textarea/cancellable_input.py @@ -0,0 +1,19 @@ +from textual.binding import Binding +from textual.message import Message +from textual.widgets import Input + + +class CancellableInput(Input): + BINDINGS = [ + Binding("escape", "cancel", "Cancel", show=False), + ] + + class Cancelled(Message): + """ + Posted when the user presses Esc to cancel the input. + """ + + pass + + def action_cancel(self) -> None: + self.post_message(self.Cancelled()) diff --git a/src/textual_textarea/colors.py b/src/textual_textarea/colors.py new file mode 100644 index 0000000..b477492 --- /dev/null +++ b/src/textual_textarea/colors.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +from rich.style import Style +from textual.color import Color +from textual.theme import Theme +from textual.widgets.text_area import TextAreaTheme + + +def text_area_theme_from_app_theme( + theme_name: str, theme: Theme, css_vars: dict[str, str] +) -> TextAreaTheme: + builtin = TextAreaTheme.get_builtin_theme(theme_name) + if builtin is not None: + return builtin + + if "background" in css_vars: + background_color = Color.parse( + css_vars.get("background", "#000000" if theme.dark else "#FFFFFF") + ) + foreground_color = Color.parse( + css_vars.get("foreground", background_color.inverse) + ) + else: + foreground_color = Color.parse( + css_vars.get("foreground", "#FFFFFF" if theme.dark else "#000000") + ) + background_color = foreground_color.inverse + + muted = background_color.blend(foreground_color, factor=0.5) + + computed_theme = TextAreaTheme( + name=theme_name, + base_style=Style( + color=foreground_color.rich_color, bgcolor=background_color.rich_color + ), + syntax_styles={ + "comment": muted.hex, # type: ignore + "string": theme.accent, # type: ignore + "string.documentation": muted.hex, # type: ignore + "string.special": theme.accent, # type: ignore + "number": theme.accent, # type: ignore + "float": theme.accent, # type: ignore + "function": theme.secondary, # type: ignore + "function.call": theme.secondary, # type: ignore + "method": theme.secondary, # type: ignore + "method.call": theme.secondary, # type: ignore + "constant": foreground_color.hex, # type: ignore + "constant.builtin": foreground_color.hex, # type: ignore + "boolean": theme.accent, # type: ignore + "class": f"{foreground_color.hex} bold", # type: ignore + "type": f"{foreground_color.hex} bold", # type: ignore + "variable": foreground_color.hex, # type: ignore + "parameter": f"{theme.accent} bold", # type: ignore + "operator": theme.secondary, # type: ignore + "punctuation.bracket": foreground_color.hex, # type: ignore + "punctuation.delimeter": foreground_color.hex, # type: ignore + "keyword": f"{theme.primary} bold", # type: ignore + "keyword.function": theme.secondary, # type: ignore + "keyword.return": theme.primary, # type: ignore + "keyword.operator": f"{theme.primary} bold", # type: ignore + "exception": theme.error, # type: ignore + "heading": theme.primary, # type: ignore + "bold": "bold", # type: ignore + "italic": "italic", # type: ignore + }, + ) + return computed_theme diff --git a/src/textual_textarea/comments.py b/src/textual_textarea/comments.py new file mode 100644 index 0000000..9a227ae --- /dev/null +++ b/src/textual_textarea/comments.py @@ -0,0 +1,185 @@ +INLINE_MARKERS = { + "abap": '"', + "actionscript": "//", + "as": "//", + "actionscript3": "//", + "as3": "//", + "ada": "--", + "ada95": "--", + "ada2005": "--", + "antlr-objc": "//", + "apl": "⍝", + "applescript": "--", + "autohotkey": ";", + "ahk": ";", + "autoit": ";", + "basemake": "#", + "bash": "#", + "sh": "#", + "ksh": "#", + "zsh": "#", + "shell": "#", + "batch": "::", + "bat": "::", + "dosbatch": "::", + "winbatch": "::", + "bbcbasic": "REM", + "blitzbasic": "REM", + "b3d": "REM", + "bplus": "REM", + "boo": "#", + "c": "//", + "csharp": "//", + "c#": "//", + "cs": "//", + "cpp": "//", + "c++": "//", + "cbmbas": "REM", + "clojure": ";", + "clj": ";", + "clojurescript": ";", + "cljs": ";", + "cmake": "#", + "cobol": "*>", + "cobolfree": "*>", + "common-lisp": ";", + "cl": ";", + "lisp": ";", + "d": "//", + "delphi": "//", + "pas": "//", + "pascal": "//", + "objectpascal": "//", + "eiffel": "--", + "elixir": "#", + "ex": "#", + "exs": "#", + "iex": "#", + "elm": "--", + "emacs-lisp": ";", + "elisp": ";", + "emacs": ";", + "erlang": "%", + "erl": "%", + "fsharp": "//", + "f#": "//", + "factor": "!", + "fish": "#", + "fishshell": "#", + "forth": "\\", + "fortran": "!", + "f90": "!", + "fortranfixed": "!", + "go": "//", + "golang": "//", + "haskell": "--", + "hs": "--", + "inform6": "!", + "i6": "!", + "i6t": "!", + "inform7": "!", + "i7": "!", + "j": "NB.", + "java": "//", + "jsp": "//", + "javascript": "//", + "js": "//", + "julia": "#", + "jl": "#", + "jlcon": "#", + "julia-repl": "#", + "kotlin": "//", + "lua": "--", + "make": "#", + "makefile": "#", + "mf": "#", + "bsdmake": "#", + "matlab": "%", + "matlabsession": "%", + "monkey": "'", + "mysql": "#", + "newlisp": ";", + "nimrod": "#", + "nim": "#", + "objective-c": "//", + "objectivec": "//", + "obj-c": "//", + "objc": "//", + "objective-c++": "//", + "objectivec++": "//", + "obj-c++": "//", + "objc++": "//", + "perl": "#", + "pl": "#", + "perl6": "#", + "pl6": "#", + "raku": "#", + "php": "#", + "php3": "#", + "php4": "#", + "php5": "#", + "plpgsql": "--", + "psql": "--", + "postgresql-console": "--", + "postgres-console": "--", + "postgres-explain": "--", + "postgresql": "--", + "postgres": "--", + "postscript": "%", + "postscr": "%", + "powershell": "#", + "pwsh": "#", + "posh": "#", + "ps1": "#", + "psm1": "#", + "pwsh-session": "#", + "ps1con": "#", + "prolog": "%", + "python": "#", + "py": "#", + "sage": "#", + "python3": "#", + "py3": "#", + "python2": "#", + "py2": "#", + "py2tb": "#", + "pycon": "#", + "pytb": "#", + "py3tb": "#", + "py+ul4": "#", + "qbasic": "REM", + "basic": "REM", + "ragel-ruby": "#", + "ragel-rb": "#", + "rebol": ";", + "red": ";", + "red/system": ";", + "ruby": "#", + "rb": "#", + "duby": "#", + "rbcon": "#", + "irb": "#", + "rust": "//", + "rs": "//", + "sass": "//", + "scala": "//", + "scheme": ";", + "scm": ";", + "sql": "--", + "sql+jinja": "--", + "sqlite3": "--", + "swift": "//", + "tex": "%", + "latex": "%", + "tsql": "--", + "t-sql": "--", + "vbscript": "'", + "vhdl": "--", + "wast": ";;", + "wat": ";;", + "yaml": "#", + "yaml+jinja": "#", + "salt": "#", + "sls": "#", + "zig": "//", +} diff --git a/src/textual_textarea/containers.py b/src/textual_textarea/containers.py new file mode 100644 index 0000000..d8f42c5 --- /dev/null +++ b/src/textual_textarea/containers.py @@ -0,0 +1,54 @@ +from typing import Any, Union + +from textual.containers import Container, ScrollableContainer +from textual.widget import Widget + + +class TextContainer( + ScrollableContainer, + inherit_bindings=False, + can_focus=False, + can_focus_children=True, +): + DEFAULT_CSS = """ + TextContainer { + height: 1fr; + width: 100%; + layers: main overlay; + } + """ + + def scroll_to( + self, x: Union[float, None] = None, y: Union[float, None] = None, **_: Any + ) -> None: + return super().scroll_to(x, y, animate=True, duration=0.01) + + +class FooterContainer( + Container, + inherit_bindings=False, + can_focus=False, + can_focus_children=True, +): + DEFAULT_CSS = """ + FooterContainer { + dock: bottom; + height: auto; + width: 100% + } + FooterContainer.hide { + height: 0; + } + """ + + def __init__( + self, + *children: Widget, + name: Union[str, None] = None, + id: Union[str, None] = None, # noqa: A002 + classes: Union[str, None] = None, + disabled: bool = False, + ) -> None: + super().__init__( + *children, name=name, id=id, classes=classes, disabled=disabled + ) diff --git a/src/textual_textarea/error_modal.py b/src/textual_textarea/error_modal.py new file mode 100644 index 0000000..8fa4a8f --- /dev/null +++ b/src/textual_textarea/error_modal.py @@ -0,0 +1,75 @@ +from typing import Union + +from textual.app import ComposeResult +from textual.containers import Vertical, VerticalScroll +from textual.screen import ModalScreen +from textual.widgets import Static + + +class ErrorModal(ModalScreen): + DEFAULT_CSS = """ + ErrorModal { + align: center middle; + padding: 0; + } + #error_modal__outer { + border: round $error; + background: $background; + margin: 5 10; + padding: 1 2; + max-width: 88; + } + + #error_modal__header { + dock: top; + color: $text-muted; + margin: 0 0 1 0; + padding: 0 1; + } + + #error_modal__inner { + border: round $background; + padding: 1 1 1 2; + } + + #error_modal__info { + padding: 0 3 0 0; + } + + #error_modal__footer { + dock: bottom; + color: $text-muted; + margin: 1 0 0 0; + padding: 0 1; + } + """ + + def __init__( + self, + title: str, + header: str, + error: BaseException, + name: Union[str, None] = None, + id: Union[str, None] = None, # noqa: A002 + classes: Union[str, None] = None, + ) -> None: + super().__init__(name, id, classes) + self.title = title + self.header = header + self.error = error + + def compose(self) -> ComposeResult: + with Vertical(id="error_modal__outer"): + yield Static(self.header, id="error_modal__header") + with Vertical(id="error_modal__inner"): + with VerticalScroll(): + yield Static(str(self.error), id="error_modal__info") + yield Static("Press any key to continue.", id="error_modal__footer") + + def on_mount(self) -> None: + container = self.query_one("#error_modal__outer") + container.border_title = self.title + + def on_key(self) -> None: + self.app.pop_screen() + self.app.action_focus_next() diff --git a/src/textual_textarea/find_input.py b/src/textual_textarea/find_input.py new file mode 100644 index 0000000..a5eb306 --- /dev/null +++ b/src/textual_textarea/find_input.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +from textual import on +from textual.events import Blur, Key +from textual.widgets import Input + +from textual_textarea.cancellable_input import CancellableInput + + +class FindInput(CancellableInput): + def __init__( + self, + value: str = "", + history: list[str] | None = None, + classes: str | None = None, + ) -> None: + super().__init__( + value=value, + placeholder="Find; enter for next; ESC to close; ↑↓ for history", + password=False, + type="text", + id="textarea__find_input", + classes=classes, + ) + self.history: list[str] = [] if history is None else history + self.history_index: int | None = None + + @on(Key) + def handle_special_keys(self, event: Key) -> None: + if event.key not in ("up", "down", "f3"): + self.history_index = None + return + event.stop() + event.prevent_default() + if event.key == "down": + self._handle_down() + elif event.key == "up": + self._handle_up() + elif event.key == "f3": + self.post_message(Input.Submitted(self, self.value)) + + @on(Blur) + def handle_blur(self) -> None: + if self.value and (not self.history or self.value != self.history[-1]): + self.history.append(self.value) + + def _handle_down(self) -> None: + if self.history_index is None: + self.checkpoint() + self.value = "" + elif self.history_index == -1: + self.history_index = None + self.value = "" + else: + self.history_index += 1 + self.value = self.history[self.history_index] + self.action_end() + + def checkpoint(self) -> bool: + if self.value and (not self.history or self.value != self.history[-1]): + self.history.append(self.value) + return True + return False + + def _handle_up(self) -> None: + if not self.history: + if self.value: + self.history.append(self.value) + self.value = "" + return + + if self.history_index is None: + self.history_index = -1 if self.checkpoint() else 0 + + self.history_index = max(-1 * len(self.history), self.history_index - 1) + self.value = self.history[self.history_index] + self.action_end() diff --git a/src/textual_textarea/goto_input.py b/src/textual_textarea/goto_input.py new file mode 100644 index 0000000..42df5cf --- /dev/null +++ b/src/textual_textarea/goto_input.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from textual.validation import ValidationResult, Validator + +from textual_textarea.cancellable_input import CancellableInput + + +class GotoLineValidator(Validator): + def __init__( + self, + max_line_number: int, + min_line_number: int = 1, + failure_description: str = "Not a valid line number.", + ) -> None: + super().__init__(failure_description) + self.max_line_number = max_line_number + self.min_line_number = min_line_number + + def validate(self, value: str) -> ValidationResult: + try: + lno = int(value) + except (ValueError, TypeError): + return self.failure("Not a valid line number.") + + if lno < self.min_line_number: + return self.failure(f"Line number must be >= {self.min_line_number}") + elif lno > self.max_line_number: + return self.failure(f"Line number must be <= {self.max_line_number}") + + return self.success() + + +class GotoLineInput(CancellableInput): + def __init__( + self, + *, + max_line_number: int, + id: str | None = None, # noqa: A002 + classes: str | None = None, + current_line: int | None = None, + min_line_number: int = 1, + ) -> None: + current_line_text = ( + f"Current line: {current_line}. " if current_line is not None else "" + ) + range_text = ( + f"Enter a line number between {min_line_number} and " f"{max_line_number}." + ) + placeholder = f"{current_line_text}{range_text} ESC to cancel." + super().__init__( + "", + placeholder=placeholder, + type="integer", + validators=GotoLineValidator( + max_line_number=max_line_number, min_line_number=min_line_number + ), + validate_on={"changed"}, + id=id, + classes=classes, + ) diff --git a/src/textual_textarea/messages.py b/src/textual_textarea/messages.py new file mode 100644 index 0000000..f6a4826 --- /dev/null +++ b/src/textual_textarea/messages.py @@ -0,0 +1,38 @@ +from pathlib import Path +from typing import Union + +from textual.message import Message + + +class TextAreaClipboardError(Message, bubble=True): + """ + Posted when textarea cannot access the system clipboard + """ + + def __init__(self, action: str) -> None: + super().__init__() + self.action = action + + +class TextAreaThemeError(Message, bubble=True): + """ + Posted when textarea cannot instantiate a theme + """ + + def __init__(self, theme: str) -> None: + super().__init__() + self.theme = theme + + +class TextAreaSaved(Message, bubble=True): + """ + Posted when the textarea saved a file successfully. + """ + + def __init__(self, path: Union[Path, str]) -> None: + self.path = str(path) + super().__init__() + + +class TextAreaHideCompletionList(Message): + pass diff --git a/src/textual_textarea/path_input.py b/src/textual_textarea/path_input.py new file mode 100644 index 0000000..4958af3 --- /dev/null +++ b/src/textual_textarea/path_input.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +import stat +from pathlib import Path + +from rich.highlighter import Highlighter +from textual.binding import Binding +from textual.suggester import Suggester +from textual.validation import ValidationResult, Validator + +from textual_textarea.cancellable_input import CancellableInput + + +def path_completer(prefix: str) -> list[tuple[str, str]]: + try: + original = Path(prefix) + p = original.expanduser() + if p.is_dir(): + matches = list(p.iterdir()) + else: + matches = list(p.parent.glob(f"{p.name}*")) + if original != p and original.parts and original.parts[0] == "~": + prompts = [str(Path("~") / m.relative_to(Path.home())) for m in matches] + elif not original.is_absolute() and prefix.startswith("./"): + prompts = [f"./{m}" for m in matches] + else: + prompts = [str(m) for m in matches] + return [(p, p) for p in prompts] + except Exception: + return [] + + +class PathSuggester(Suggester): + def __init__(self) -> None: + super().__init__(use_cache=True, case_sensitive=True) + + async def get_suggestion(self, value: str) -> str | None: + matches = path_completer(value) + if len(matches) == 1: + return str(matches[0][0]) + else: + return None + + +class PathValidator(Validator): + def __init__( + self, + dir_okay: bool, + file_okay: bool, + must_exist: bool, + failure_description: str = "Not a valid path.", + ) -> None: + self.dir_okay = dir_okay + self.file_okay = file_okay + self.must_exist = must_exist + super().__init__(failure_description) + + def validate(self, value: str) -> ValidationResult: + if self.dir_okay and self.file_okay and not self.must_exist: + return self.success() + try: + p = Path(value).expanduser().resolve() + except Exception: + return self.failure("Not a valid path.") + + try: + st = p.stat() + except FileNotFoundError: + if self.must_exist: + return self.failure("File or directory does not exist.") + return self.success() + + if not self.dir_okay and stat.S_ISDIR(st.st_mode): + return self.failure("Path cannot be a directory.") + elif not self.file_okay and stat.S_ISREG(st.st_mode): + return self.failure("Path cannot be a regular file.") + + return self.success() + + +class PathInput(CancellableInput): + BINDINGS = [ + Binding("tab", "complete", "Accept Completion", show=False), + ] + + def __init__( + self, + value: str | None = None, + placeholder: str = "", + highlighter: Highlighter | None = None, + password: bool = False, + *, + name: str | None = None, + id: str | None = None, # noqa: A002 + classes: str | None = None, + disabled: bool = False, + dir_okay: bool = True, + file_okay: bool = True, + must_exist: bool = False, + tab_advances_focus: bool = False, + ) -> None: + self.tab_advances_focus = tab_advances_focus + super().__init__( + value, + placeholder, + highlighter, + password, + suggester=PathSuggester(), + validators=PathValidator(dir_okay, file_okay, must_exist), + name=name, + id=id, + classes=classes, + disabled=disabled, + ) + + def action_complete(self) -> None: + if self._suggestion and self._suggestion != self.value: + self.action_cursor_right() + elif self.tab_advances_focus: + self.app.action_focus_next() + + def _toggle_cursor(self) -> None: + """Toggle visibility of cursor.""" + if self.app.is_headless: + self._cursor_visible = True + else: + self._cursor_visible = not self._cursor_visible diff --git a/src/textual_textarea/py.typed b/src/textual_textarea/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/textual_textarea/text_editor.py b/src/textual_textarea/text_editor.py new file mode 100644 index 0000000..335a6e9 --- /dev/null +++ b/src/textual_textarea/text_editor.py @@ -0,0 +1,1465 @@ +from __future__ import annotations + +import re +from contextlib import suppress +from math import ceil, floor +from pathlib import Path +from typing import TYPE_CHECKING, Any, Callable, Literal, Sequence + +import pyperclip +from rich.console import RenderableType +from textual import events, on, work +from textual._cells import cell_len +from textual._node_list import DuplicateIds +from textual.app import ComposeResult +from textual.binding import Binding +from textual.events import Paste +from textual.message import Message +from textual.reactive import reactive +from textual.timer import Timer +from textual.widget import Widget +from textual.widgets import Input, Label, OptionList, TextArea +from textual.widgets.text_area import Location, Selection, SyntaxAwareDocument + +from textual_textarea.autocomplete import CompletionList +from textual_textarea.cancellable_input import CancellableInput +from textual_textarea.colors import text_area_theme_from_app_theme +from textual_textarea.comments import INLINE_MARKERS +from textual_textarea.containers import FooterContainer, TextContainer +from textual_textarea.error_modal import ErrorModal +from textual_textarea.find_input import FindInput +from textual_textarea.goto_input import GotoLineInput +from textual_textarea.messages import ( + TextAreaClipboardError, + TextAreaHideCompletionList, + TextAreaSaved, + TextAreaThemeError, +) +from textual_textarea.path_input import PathInput, path_completer + +if TYPE_CHECKING: + from tree_sitter import Node, Parser, Query, Tree + +BRACKETS = { + "(": ")", + "[": "]", + "{": "}", +} +CLOSERS = {'"': '"', "'": "'", **BRACKETS} + +# these patterns need to match a reversed string! +DOUBLE_QUOTED_EXPR = r'"([^"\\]*(\\.[^"\\]*|""[^"\\]*)*)"(b?r|f|b|rb|&?u|@)?' +SINGLE_QUOTED_EXPR = r"'([^'\\]*(\\.[^'\\]*|''[^'\\]*)*)'(b?r|f|b|rb|&?u|x)?" +BACKTICK_EXPR = r"`([^`\\]*(\\.[^`\\]*)*)`" +PATH_PROG = re.compile(r"[^\"\'\s]+") +MEMBER_PROG = re.compile( + rf"\w*(`|'|\")?(\.|::?)(\w+|{SINGLE_QUOTED_EXPR}|{DOUBLE_QUOTED_EXPR}|{BACKTICK_EXPR})", + flags=re.IGNORECASE, +) +WORD_PROG = re.compile(r"\w+") +NON_WORD_CHAR_PROG = re.compile(r"\W") + + +class TextAreaPlus(TextArea, inherit_bindings=False): + DEFAULT_CSS = """ + TextAreaPlus { + width: 1fr; + height: 1fr; + border: none; + layer: main; + + &:focus { + border: none; + } + } + """ + BINDINGS = [ + # Cursor movement + Binding("up", "cursor_up", "cursor up", show=False), + Binding("down", "cursor_down", "cursor down", show=False), + Binding("left", "cursor_left", "cursor left", show=False), + Binding("right", "cursor_right", "cursor right", show=False), + Binding("ctrl+left", "cursor_word_left", "cursor word left", show=False), + Binding("ctrl+right", "cursor_word_right", "cursor word right", show=False), + Binding("home", "cursor_line_start", "cursor line start", show=False), + Binding("end", "cursor_line_end", "cursor line end", show=False), + Binding("ctrl+home", "cursor_doc_start", "cursor doc start", show=False), + Binding("ctrl+end", "cursor_doc_end", "cursor doc end", show=False), + Binding("pageup", "cursor_page_up", "cursor page up", show=False), + Binding("pagedown", "cursor_page_down", "cursor page down", show=False), + # scrolling + Binding("ctrl+up", "scroll_one('up')", "scroll one up", show=False), + Binding("ctrl+down", "scroll_one('down')", "scroll one down", show=False), + # Making selections (generally holding the shift key and moving cursor) + Binding( + "ctrl+shift+left", + "cursor_word_left(True)", + "cursor left word select", + show=False, + ), + Binding( + "ctrl+shift+right", + "cursor_word_right(True)", + "cursor right word select", + show=False, + ), + Binding( + "shift+home", + "cursor_line_start(True)", + "cursor line start select", + show=False, + ), + Binding( + "shift+end", "cursor_line_end(True)", "cursor line end select", show=False + ), + Binding( + "ctrl+shift+home", + "cursor_doc_start(True)", + "select to cursor doc start", + show=False, + ), + Binding( + "ctrl+shift+end", + "cursor_doc_end(True)", + "select to cursor doc end", + show=False, + ), + Binding("shift+up", "cursor_up(True)", "cursor up select", show=False), + Binding("shift+down", "cursor_down(True)", "cursor down select", show=False), + Binding("shift+left", "cursor_left(True)", "cursor left select", show=False), + Binding("shift+right", "cursor_right(True)", "cursor right select", show=False), + # Binding("f5", "select_word", "select word", show=False), + # Binding("f6", "select_line", "select line", show=False), + Binding("ctrl+a", "select_all", "select all", show=False), + # Editing + Binding("ctrl+underscore", "toggle_comment", "toggle comment", show=False), + Binding("ctrl+x", "cut", "copy", show=False), + Binding("ctrl+c", "copy", "copy", show=False), + Binding("ctrl+u,ctrl+v,shift+insert", "paste", "paste", show=False), + Binding("ctrl+z", "undo", "undo", show=False), + Binding("ctrl+y", "redo", "redo", show=False), + # Deletion + Binding("backspace", "delete_left", "delete left", show=False), + Binding("delete", "delete_right", "delete right", show=False), + Binding("shift+delete", "delete_line", "delete line", show=False), + # Binding( + # "ctrl+w", "delete_word_left", "delete left to start of word", show=False + # ), + # Binding( + # "ctrl+f", "delete_word_right", "delete right to start of word", show=False + # ), + # Binding( + # "ctrl+u", "delete_to_start_of_line", "delete to line start", show=False + # ), + # Binding("ctrl+k", "delete_to_end_of_line", "delete to line end", show=False), + ] + + clipboard: str = "" + completer_active: Literal["path", "member", "word"] | None = None + + class ShowCompletionList(Message): + def __init__(self, prefix: str) -> None: + super().__init__() + self.prefix = prefix + + def __repr__(self) -> str: + return f"ShowCompletionList({self.prefix=})" + + def __str__(self) -> str: + return f"ShowCompletionList({self.prefix=})" + + class CompletionListKey(Message): + def __init__(self, key: events.Key) -> None: + super().__init__() + self.key = key + + class ClipboardReady(Message): + def __init__( + self, copy: Callable[[Any], None], paste: Callable[[], str] + ) -> None: + super().__init__() + self.copy = copy + self.paste = paste + + def __init__( + self, + text: str = "", + *, + language: str | None = None, + theme: str = "css", + use_system_clipboard: bool = True, + read_only: bool = False, + name: str | None = None, + id: str | None = None, # noqa: A002 + classes: str | None = None, + disabled: bool = False, + ) -> None: + super().__init__( + text, + language=language, + theme=theme, + name=name, + id=id, + classes=classes, + disabled=disabled, + soft_wrap=False, + tab_behavior="indent", + show_line_numbers=True, + read_only=read_only, + ) + self.cursor_blink = False if self.app.is_headless else True + self.use_system_clipboard = use_system_clipboard + self.double_click_location: Location | None = None + self.double_click_timer: Timer | None = None + self.consecutive_clicks: int = 0 + self.system_copy: Callable[[Any], None] | None = None + self.system_paste: Callable[[], str] | None = None + + def on_mount(self) -> None: + self._determine_clipboard() + self.history.checkpoint() + + def on_blur(self, event: events.Blur) -> None: + self.post_message(TextAreaHideCompletionList()) + + def on_key(self, event: events.Key) -> None: + # Naked shift or ctrl keys on Windows get sent as NUL chars; Textual + # interprets these as `ctrl+@` presses, which is inconsistent with + # other platforms. We ignore these presses. + # https://github.com/Textualize/textual/issues/872 + if event.key == "ctrl+@": + event.stop() + event.prevent_default() + return + + if event.key in ( + "apostrophe", + "quotation_mark", + "left_parenthesis", + "left_square_bracket", + "left_curly_bracket", + "right_parenthesis", + "right_square_bracket", + "right_curly_bracket", + ): + self._handle_quote_or_bracket(event) + elif event.key == "enter": + self._handle_enter(event) + elif event.key == "tab": + self._handle_tab(event) + elif event.key == "shift+tab": + self._handle_shift_tab(event) + elif event.key in ("up", "down", "pageup", "pagedown"): + self._handle_up_down(event) + elif event.key == "backspace": + self._handle_backspace(event) + elif event.key in ("slash", "backslash"): + self._handle_slash(event) + elif event.key in ("full_stop", "colon"): + self._handle_separator(event) + elif event.key == "escape": + self._handle_escape(event) + elif event.character and event.is_printable: + self._handle_printable_character(event) + else: + self.post_message(TextAreaHideCompletionList()) + + def on_mouse_down(self, event: events.MouseDown) -> None: + self.post_message(TextAreaHideCompletionList()) + target = self.get_target_document_location(event) + if ( + self.double_click_location is not None + and self.double_click_location == target + ): + event.prevent_default() + self._selecting = True + self.capture_mouse() + self._pause_blink(visible=True) + + def on_mouse_up(self, event: events.MouseUp) -> None: + target = self.get_target_document_location(event) + if ( + self.consecutive_clicks > 0 + and self.double_click_location is not None + and self.double_click_location == target + ): + if self.consecutive_clicks == 1: + self.action_select_word() + elif self.consecutive_clicks == 2: + self.action_select_line() + self.action_cursor_right(select=True) + else: + self.action_select_all() + self.consecutive_clicks += 1 + else: + self.history.checkpoint() + self.double_click_location = target + self.consecutive_clicks += 1 + + if self.double_click_timer is not None: + self.double_click_timer.reset() + else: + self.double_click_timer = self.set_timer( + delay=0.5, callback=self._clear_double_click, name="double_click_timer" + ) + + def on_paste(self, event: Paste) -> None: + event.prevent_default() + event.stop() + self.post_message(TextAreaHideCompletionList()) + self.history.checkpoint() + self.replace(event.text, *self.selection, maintain_selection_offset=False) + + @on(ClipboardReady) + def _set_clipboard(self, message: ClipboardReady) -> None: + self.system_copy = message.copy + self.system_paste = message.paste + + def watch_language(self, language: str) -> None: + self.inline_comment_marker = INLINE_MARKERS.get(language) + + def replace_current_word(self, new_word: str) -> None: + current_word = self._get_word_before_cursor() + offset = len(current_word) + self.replace( + new_word, + start=(self.cursor_location[0], self.cursor_location[1] - offset), + end=self.cursor_location, + maintain_selection_offset=False, + ) + + @work(thread=True) + def _determine_clipboard(self) -> None: + if self.use_system_clipboard: + copy, paste = pyperclip.determine_clipboard() + self.post_message(self.ClipboardReady(copy=copy, paste=paste)) + + def action_copy(self) -> None: + self._copy_selection() + + def action_cut(self) -> None: + self.post_message(TextAreaHideCompletionList()) + self.history.checkpoint() + self._copy_selection() + if not self.selected_text: + self.action_delete_line() + self.delete(*self.selection) + + def action_cursor_doc_start(self, select: bool = False) -> None: + self.post_message(TextAreaHideCompletionList()) + if select: + self.selection = Selection(start=self.selection.start, end=(0, 0)) + else: + self.selection = Selection(start=(0, 0), end=(0, 0)) + + def action_cursor_doc_end(self, select: bool = False) -> None: + self.post_message(TextAreaHideCompletionList()) + if select: + self.selection = Selection( + start=self.selection.start, end=self.document.end + ) + else: + self.selection = Selection(start=self.document.end, end=self.document.end) + + def action_delete_line(self) -> None: + self.post_message(TextAreaHideCompletionList()) + self.history.checkpoint() + if self.selection.start != self.cursor_location: # selection active + self.delete(*self.selection, maintain_selection_offset=False) + else: + line, col = self.cursor_location + if self.document.line_count == 1: + super().action_delete_line() + elif self.cursor_at_last_line: + eol = len(self.document[line - 1]) + self.replace( + "", start=(line - 1, eol), end=self.get_cursor_line_end_location() + ) + self.cursor_location = (line - 1, eol) + else: + self.delete(start=(line, 0), end=(line + 1, 0)) + self.cursor_location = (line, 0) + + def action_paste(self) -> None: + self.post_message(TextAreaHideCompletionList()) + if self.use_system_clipboard and self.system_paste is not None: + try: + self.clipboard = self.system_paste() + except Exception: + # no system clipboard; common in CI runners. Use internal + # clipboard state of self.clipboard + self.post_message(TextAreaClipboardError(action="paste")) + if self.clipboard: + self.post_message(Paste(self.clipboard)) + + def action_select_word(self) -> None: + self.post_message(TextAreaHideCompletionList()) + prev = self._get_character_before_cursor() + next_char = self._get_character_at_cursor() + at_start_of_word = self._word_pattern.match(prev) is None + at_end_of_word = self._word_pattern.match(next_char) is None + if at_start_of_word and not at_end_of_word: + self.action_cursor_word_right(select=True) + elif at_end_of_word and not at_start_of_word: + self.action_cursor_word_left(select=True) + self.section = Selection(start=self.selection.end, end=self.selection.start) + else: + self.action_cursor_word_left(select=False) + self.action_cursor_word_right(select=True) + + def action_scroll_one(self, direction: str = "down") -> None: + self.post_message(TextAreaHideCompletionList()) + if direction == "down": + self.scroll_relative(y=1, animate=False) + elif direction == "up": + self.scroll_relative(y=-1, animate=False) + + def action_toggle_comment(self) -> None: + self.post_message(TextAreaHideCompletionList()) + if self.inline_comment_marker: + self.history.checkpoint() + lines, first, last = self._get_selected_lines() + stripped_lines = [line.lstrip() for line in lines] + indents = [len(line) - len(line.lstrip()) for line in lines] + # if lines are already commented, remove them + if lines and all( + [ + not line or line.startswith(self.inline_comment_marker) + for line in stripped_lines + ] + ): + marker_offset = len(self.inline_comment_marker) + offsets = [ + ( + 0 + if not line + else ( + marker_offset + 1 + if line[marker_offset].isspace() + else marker_offset + ) + ) + for line in stripped_lines + ] + for lno, indent, offset in zip( + range(first[0], last[0] + 1), indents, offsets + ): + self.delete( + start=(lno, indent), + end=(lno, indent + offset), + maintain_selection_offset=True, + ) + # add comment tokens to all lines + else: + comment_indent = min( + [indent for indent, line in zip(indents, stripped_lines) if line] + ) + insertion = f"{self.inline_comment_marker} " + for lno, stripped_line in enumerate(stripped_lines, start=first[0]): + if stripped_line: + # insert one character at a time, to create a single undo-able + # batch of edits. + # See https://github.com/Textualize/textual/issues/4428 + for i, char in enumerate(insertion): + self.insert( + char, + location=(lno, comment_indent + i), + maintain_selection_offset=True, + ) + + def action_undo(self) -> None: + self.post_message(TextAreaHideCompletionList()) + super().action_undo() + + def action_redo(self) -> None: + self.post_message(TextAreaHideCompletionList()) + super().action_redo() + + def _clear_double_click(self) -> None: + self.consecutive_clicks = 0 + self.double_click_location = None + self.double_click_timer = None + + def _copy_selection(self) -> None: + if self.selected_text: + self.clipboard = self.selected_text + else: + whole_line = self.get_text_range( + self.get_cursor_line_start_location(), + self.get_cursor_line_end_location(), + ) + self.clipboard = f"{whole_line}{self.document.newline}" + if self.use_system_clipboard and self.system_copy is not None: + try: + self.system_copy(self.clipboard) + except Exception: + # no system clipboard; common in CI runners + self.post_message(TextAreaClipboardError(action="copy")) + + def _get_character_at_cursor(self) -> str: + if self.cursor_at_end_of_line: + return "" + return self.get_text_range( + start=self.cursor_location, end=self.get_cursor_right_location() + ) + + def _get_character_before_cursor(self) -> str: + if self.cursor_at_start_of_line: + return "" + return self.get_text_range( + start=self.get_cursor_left_location(), end=self.cursor_location + ) + + def _get_word_before_cursor(self, event: events.Key | None = None) -> str: + lno = self.cursor_location[0] + line = self.get_text_range(start=(lno, 0), end=self.cursor_location) + + if event is not None and event.key == "backspace": + if len(line) > 1: + search_string = line[:-1] + else: + search_string = "" + elif event is not None and event.character is not None: + search_string = f"{line}{event.character}" + else: + search_string = line + + if self.completer_active == "path": + pattern = PATH_PROG + elif self.completer_active == "member": + pattern = MEMBER_PROG + else: + pattern = WORD_PROG + + match = pattern.match(search_string[::-1]) + if match: + return match.group(0)[::-1] + else: + return "" + + def _handle_backspace(self, event: events.Key) -> None: + if self.completer_active is not None: + current_word = self._get_word_before_cursor(event) + if current_word: + self.post_message(self.ShowCompletionList(prefix=current_word)) + else: + self.post_message(TextAreaHideCompletionList()) + + def _handle_enter(self, event: events.Key) -> None: + event.stop() + event.prevent_default() + if self.completer_active is not None: + self.post_message(self.CompletionListKey(event)) + return + if self.read_only: + return + nl = self.document.newline + first, last = sorted([*self.selection]) + indent = self._get_indent_level_of_line(index=first[0]) + self.selection = Selection(start=first, end=first) + char_before = self._get_character_before_cursor() + if char_before in BRACKETS: + if self.indent_type == "tabs": + new_indent = indent + 1 + indent_char = "\t" + else: + new_indent = indent + self.indent_width - (indent % self.indent_width) + indent_char = " " + self.replace(f"{nl}{indent_char*new_indent}", first, last) + char_at = self._get_character_at_cursor() + if char_at == BRACKETS[char_before]: + loc = self.selection + self.insert(f"{nl}{indent * indent_char}") + self.selection = loc + else: + indent_char = "\t" if self.indent_type == "tabs" else " " + self.insert(f"{nl}{indent * indent_char}", location=self.cursor_location) + + def _handle_quote_or_bracket(self, event: events.Key) -> None: + event.stop() + event.prevent_default() + if self.read_only: + return + if self.completer_active != "member": + self.post_message(TextAreaHideCompletionList()) + else: + prefix = self._get_word_before_cursor(event=event) + self.post_message(self.ShowCompletionList(prefix=prefix)) + assert event.character is not None + if self.selection.start == self.selection.end: + self._insert_closed_character_at_cursor(event.character) + elif event.key in ( + "right_parenthesis", + "right_square_bracket", + "right_curly_bracket", + ): + self.replace(event.character, *self.selection) + else: + self._insert_characters_around_selection(event.character) + + def _handle_shift_tab(self, event: events.Key) -> None: + event.stop() + event.prevent_default() + if self.read_only: + self.app.action_focus_previous() + return + if self.completer_active is not None: + self.post_message(self.CompletionListKey(event)) + return + self._indent_selection(kind="dedent") + + def _handle_separator(self, event: events.Key) -> None: + event.stop() + if self.completer_active != "path": + self.completer_active = "member" + prefix = self._get_word_before_cursor(event) + self.post_message(self.ShowCompletionList(prefix=prefix)) + + def _handle_escape(self, event: events.Key) -> None: + """ + starting in textual 0.49, escape is handled by on_key instead of + a binding, so we inherited behavior we don't want. Trap this event + and hide the completion list. + """ + event.stop() + event.prevent_default() + self.selection = Selection(self.selection.end, self.selection.end) + self.post_message(TextAreaHideCompletionList()) + + def _handle_slash(self, event: events.Key) -> None: + event.stop() + self.completer_active = "path" + prefix = self._get_word_before_cursor(event) + self.post_message(self.ShowCompletionList(prefix=prefix)) + + def _handle_tab(self, event: events.Key) -> None: + event.stop() + event.prevent_default() + if self.completer_active is not None: + self.post_message(self.CompletionListKey(event)) + return + if self.read_only: + self.app.action_focus_next() + return + first, last = sorted([*self.selection]) + # in some cases, selections are replaced with indent + if first[0] == last[0] and ( + first[1] == last[1] + or first[1] != 0 + or last[1] != len(self.document.get_line(last[0])) - 1 + ): + indent_char = "\t" if self.indent_type == "tabs" else " " + indent_width = 1 if self.indent_type == "tabs" else self.indent_width + self.replace( + f"{indent_char*(indent_width - first[1] % indent_width)}", + first, + last, + maintain_selection_offset=False, + ) + # usually, selected lines are prepended with four-ish spaces + else: + self._indent_selection(kind="indent") + + def _handle_up_down(self, event: events.Key) -> None: + if self.completer_active is not None: + event.stop() + event.prevent_default() + self.post_message(self.CompletionListKey(event)) + + def _handle_printable_character(self, event: events.Key) -> None: + assert event.character is not None, "Error! Printable key with no character." + if self.completer_active is None: + if WORD_PROG.match(event.character) is not None: + self.completer_active = "word" + else: + return + current_word = self._get_word_before_cursor(event) + if current_word: + self.post_message(self.ShowCompletionList(prefix=current_word)) + else: + self.post_message(TextAreaHideCompletionList()) + + def _indent_selection(self, kind: Literal["indent", "dedent"]) -> None: + rounder, offset = (ceil, -1) if kind == "dedent" else (floor, 1) + + original_selection = self.selection + lines, first, last = self._get_selected_lines() + if kind == "dedent" and not lines: + return + + indent_width = 1 if self.indent_type == "tabs" else self.indent_width + indent_char = "\t" if self.indent_type == "tabs" else " " * self.indent_width + raw_indents = [ + self._get_indent_level_of_line(lno) for lno in range(first[0], last[0] + 1) + ] + tab_stops = [rounder(space / indent_width) for space in raw_indents] + + new_lines = [ + f"{indent_char * max(0, indent+offset)}{line.lstrip()}" + for line, indent in zip(lines, tab_stops) + ] + self.replace( + self.document.newline.join(new_lines), + start=(first[0], 0), + end=(last[0], len(self.document.get_line(last[0]))), + ) + + change_at_start = ( + 0 + if original_selection.start[1] == 0 + else len(new_lines[original_selection.start[0] - first[0]]) + - len(lines[original_selection.start[0] - first[0]]) + ) + change_at_cursor = ( + 0 + if original_selection.end[1] == 0 + else len(new_lines[original_selection.end[0] - first[0]]) + - len(lines[original_selection.end[0] - first[0]]) + ) + self.selection = Selection( + start=( + original_selection.start[0], + original_selection.start[1] + change_at_start, + ), + end=( + original_selection.end[0], + original_selection.end[1] + change_at_cursor, + ), + ) + + def _insert_characters_around_selection(self, character: str) -> None: + first = min(*self.selection) + self.insert(character, location=first, maintain_selection_offset=True) + first, last = sorted([*self.selection]) + self.insert(CLOSERS[character], location=last, maintain_selection_offset=False) + self.selection = Selection(start=first, end=last) + + def _insert_closed_character_at_cursor(self, character: str) -> None: + if self._get_character_at_cursor() == character: + self.action_cursor_right() + else: + if (character in BRACKETS and self._should_complete_brackets()) or ( + character in CLOSERS and self._should_complete_quotes() + ): + self.insert(character, self.cursor_location) + loc = self.selection + self.insert(CLOSERS[character], self.cursor_location) + self.selection = loc + else: + self.insert(character, self.cursor_location) + + def _should_complete_brackets(self) -> bool: + if self.cursor_at_end_of_line: + return True + + next_char = self._get_character_at_cursor() + if not next_char or next_char.isspace(): + return True + elif next_char in """>:,.="'""": + return True + + return False + + def _should_complete_quotes(self) -> bool: + next_char = self._get_character_at_cursor() + prev_char = self._get_character_before_cursor() + if ( + self.cursor_at_end_of_line or next_char.isspace() or next_char in ")>:,.=" + ) and ( + self.cursor_at_start_of_line + or prev_char.isspace() + or NON_WORD_CHAR_PROG.match(prev_char) is not None + ): + return True + return False + + def _get_indent_level_of_line(self, index: int | None = None) -> int: + if index is None: + index = self.cursor_location[0] + line = self.document.get_line(index) + while line.isspace() and index > 0: + index -= 1 + line = self.document.get_line(index) + if line.isspace(): + return 0 + indent_char = "\t" if self.indent_type == "tabs" else " " + indent_level = len(line) - len(line.lstrip(indent_char)) + return indent_level + + def _get_selected_lines(self) -> tuple[list[str], Location, Location]: + [first, last] = sorted([self.selection.start, self.selection.end]) + lines = [self.document.get_line(i) for i in range(first[0], last[0] + 1)] + return lines, first, last + + +class TextEditor(Widget, can_focus=True, can_focus_children=False): + """ + A Widget that presents a feature-rich, multiline text editor interface. + + Attributes: + text (str): The contents of the TextEditor + language (str): Must be the short name of a Pygments lexer + (https://pygments.org/docs/lexers/), e.g., "python", "sql", "as3". + theme (str): Must be name of a Pygments style (https://pygments.org/styles/), + e.g., "bw", "github-dark", "solarized-light". + """ + + DEFAULT_CSS = """ + #textarea__save_open_input_label { + margin: 0 0 0 3; + } + .validation-error { + color: $error; + text-style: italic; + } + Input.textarea--footer-input { + border: round $foreground; + color: $foreground; + background: $background; + &.-invalid { + border: round $error 60%; + } + &.-invalid:focus { + border: round $error; + } + } + """ + + BINDINGS = [ + Binding("ctrl+s", "save", "Save Query"), + Binding("ctrl+o", "load", "Open Query"), + Binding("ctrl+f", "find", "Find"), + Binding("f3", "find(True)", "Find Next"), + Binding("ctrl+g", "goto_line", "Go To Line"), + Binding("ctrl+q", "quit", "Quit"), + ] + + theme: reactive[str] = reactive("monokai") + + def __init__( + self, + *children: Widget, + name: str | None = None, + id: str | None = None, # noqa: A002 + classes: str | None = None, + disabled: bool = False, + read_only: bool = False, + language: str | None = None, + theme: str = "css", + text: str = "", + use_system_clipboard: bool = True, + path_completer: ( + Callable[ + [str], + Sequence[tuple[RenderableType, str]] + | Sequence[tuple[tuple[str, str], str]], + ] + | None + ) = path_completer, + member_completer: ( + Callable[ + [str], + Sequence[tuple[RenderableType, str]] + | Sequence[tuple[tuple[str, str], str]], + ] + | None + ) = None, + word_completer: ( + Callable[ + [str], + Sequence[tuple[RenderableType, str]] + | Sequence[tuple[tuple[str, str], str]], + ] + | None + ) = None, + ) -> None: + """ + Initializes an instance of a TextArea. + + Args: + (see also textual.widget.Widget) + language (str): Must be the short name of a tree-sitter language, + e.g., "python", "sql" + theme (str): Must be name of a Textual Theme. + """ + super().__init__( + *children, + name=name, + id=id, + classes=classes, + disabled=disabled, + ) + self._language = language + self._theme = theme + self._initial_text = text + self._find_history: list[str] = [] + self.use_system_clipboard = use_system_clipboard + self.text_input: TextAreaPlus | None = None + self.read_only = read_only + self.path_completer = path_completer + self.member_completer = member_completer + self.word_completer = word_completer + + @property + def text(self) -> str: + """ + Returns: + (str) The contents of the TextEditor. + """ + if self.text_input is None: + return "" + return self.text_input.text + + @text.setter + def text(self, contents: str) -> None: + """ + Args: + contents (str): A string (optionally containing newlines) to + set the contents of the TextEditor equal to. + """ + if self.text_input is None: + return + self.text_input.history.checkpoint() + self.text_input.replace( + contents, + start=(0, 0), + end=self.text_input.document.end, + maintain_selection_offset=False, + ) + self.text_input.move_cursor((0, 0)) + + @property + def selected_text(self) -> str: + """ + Returns: + str: The contents of the TextEditor between the selection + anchor and the cursor. Returns an empty string if the + selection anchor is not set. + """ + if self.text_input is None: + return "" + return self.text_input.selected_text + + @property + def selection(self) -> Selection: + """ + Returns + Selection: The location of the cursor in the TextEditor + """ + if self.text_input is None: + return Selection((0, 0), (0, 0)) + return self.text_input.selection + + @selection.setter + def selection(self, selection: Selection) -> None: + """ + Args: + selection (Selection): The position (line number, pos) + to move the cursor and selection anchor to + """ + if self.text_input is None: + return + self.text_input.selection = selection + + @property + def language(self) -> str | None: + """ + Returns + str | None: The tree-sitter short name of the active language + """ + if self.text_input is None: + return None + return self.text_input.language + + @language.setter + def language(self, language: str) -> None: + """ + Args: + langage (str | None): The Pygments short name for the new language + """ + if self.text_input is None: + return None + self.text_input.language = language + + @property + def line_count(self) -> int: + """ + Returns the number of lines in the document. + """ + if self.text_input is None: + return 0 + return self.text_input.document.line_count + + def get_line(self, index: int) -> str: + """ + Returns the line with the given index from the document. + + Args: + index: The index of the line in the document. + + Returns: + The str instance representing the line. + """ + if self.text_input is None: + return "" + return self.text_input.document.get_line(index=index) + + def get_text_range(self, selection: Selection) -> str: + """ + Get the text between a start and end location. + + Args: + selection: The start and end locations + + Returns: + The text between start and end. + """ + if self.text_input is None: + return "" + return self.text_input.get_text_range(*selection) + + def insert_text_at_selection(self, text: str) -> None: + """ + Inserts text at the current cursor position; if there is a selection anchor, + first deletes the current selection. + + Args: + text (str): The text to be inserted. + """ + if self.text_input is None: + return + self.text_input.replace( + text, + *self.text_input.selection, + maintain_selection_offset=False, + ) + + def copy_to_clipboard(self, text: str) -> None: + """ + Sets the editor's internal clipboard, and the system clipboard if enabled, to + the value of text + + Args: + text (str): The text to place on the clipboard. + """ + if self.text_input is None: + self.post_message(TextAreaClipboardError(action="copy")) + return + self.text_input.clipboard = text + if self.use_system_clipboard and self.text_input.system_copy is not None: + try: + self.text_input.system_copy(text) + except Exception: + self.post_message(TextAreaClipboardError(action="copy")) + + def pause_blink(self, visible: bool = True) -> None: + """ + Pauses the blink of the cursor + """ + if self.text_input is None: + return + self.text_input._pause_blink(visible=visible) + + def restart_blink(self) -> None: + """ + Restarts the blink of the cursor + """ + if self.text_input is None: + return + self.text_input._restart_blink() + + def prepare_query(self, source: str) -> "Query" | None: + """ + Build a Query from source. The Query can be used with self.query_syntax_tree + + Args: + source (str): A tree-sitter query. See + https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax + """ + if self.text_input is None: + return None + return self.text_input.document.prepare_query(query=source) + + def query_syntax_tree( + self, + query: "Query", + start_point: tuple[int, int] | None = None, + end_point: tuple[int, int] | None = None, + ) -> dict[str, list["Node"]]: + """ + Query the tree-sitter syntax tree. + + Args: + query (Query): The tree-sitter Query to perform. + start_point (tuple[int, int] | None): The (row, column byte) to start the + query at. + end_point (tuple[int, int] | None): The (row, column byte) to end the + query at. + + Returns: + A dict mapping captured node names to lists of Nodes with that name + """ + if self.text_input is None: + return {} + return self.text_input.document.query_syntax_tree( + query=query, start_point=start_point, end_point=end_point + ) + + @property + def syntax_tree(self) -> "Tree" | None: + """ + Returns the document's syntax tree. + """ + if self.text_input is None: + return None + if isinstance(self.text_input.document, SyntaxAwareDocument): + return self.text_input.document._syntax_tree + else: + return None + + @property + def parser(self) -> "Parser" | None: + if self.text_input is None: + return None + if isinstance(self.text_input.document, SyntaxAwareDocument): + return self.text_input.document._parser + else: + return None + + def compose(self) -> ComposeResult: + self.text_container = TextContainer() + self.text_input = TextAreaPlus( + language=self._language, text=self._initial_text, read_only=self.read_only + ) + self.completion_list = CompletionList() + self.footer = FooterContainer(classes="hide") + self.footer_label = Label("", id="textarea__save_open_input_label") + with self.text_container: + yield self.text_input + yield self.completion_list + with self.footer: + yield self.footer_label + + def on_mount(self) -> None: + # delay setting the reactive until the widget mounts so we can be sure that + # self.text_input exists so watch_theme can do its thing. + self.theme = self._theme + + def on_focus(self) -> None: + if self.text_input is not None: + self.text_input.focus() + + def on_click(self) -> None: + if self.text_input is not None: + self.text_input.focus() + + @on(TextAreaHideCompletionList) + def hide_completion_list(self, event: TextAreaHideCompletionList) -> None: + event.stop() + assert self.text_input is not None + self.completion_list.is_open = False + self.text_input.completer_active = None + + @on(TextAreaPlus.SelectionChanged) + def update_completion_list_offset( + self, event: TextAreaPlus.SelectionChanged + ) -> None: + event.stop() + assert self.text_input is not None + region_x, region_y, _, _ = self.text_input.region + self.completion_list.cursor_offset = self.text_input.cursor_screen_offset - ( + region_x, + region_y, + ) + + @on(TextAreaPlus.Changed) + def check_for_find_updates(self, event: TextAreaPlus.Changed) -> None: + event.stop() + try: + find_input = self.footer.query_one(FindInput) + except Exception: + return + self._update_find_label(value=find_input.value) + + @on(TextAreaPlus.ShowCompletionList) + def update_completers_and_completion_list_offset( + self, event: TextAreaPlus.ShowCompletionList + ) -> None: + event.stop() + assert self.text_input is not None + region_x, region_y, _, _ = self.text_input.region + self.completion_list.cursor_offset = self.text_input.cursor_screen_offset - ( + region_x, + region_y, + ) + if self.text_input.completer_active == "path": + self.completion_list.show_completions(event.prefix, self.path_completer) + elif self.text_input.completer_active == "member": + self.completion_list.show_completions(event.prefix, self.member_completer) + elif self.text_input.completer_active == "word": + self.completion_list.show_completions(event.prefix, self.word_completer) + + @on(TextAreaPlus.CompletionListKey) + def forward_keypress_to_completion_list( + self, event: TextAreaPlus.CompletionListKey + ) -> None: + event.stop() + self.completion_list.process_keypress(event.key) + + @on(OptionList.OptionSelected) + def insert_completion(self, event: OptionList.OptionSelected) -> None: + event.stop() + assert self.text_input is not None + value = getattr(event.option, "value", None) or str(event.option.prompt) + self.text_input.replace_current_word(value) + self.completion_list.is_open = False + self.text_input.completer_active = None + + @on(CancellableInput.Cancelled) + def clear_footer(self) -> None: + self._clear_footer_input() + if self.text_input is not None: + self.text_input.focus() + + @on(Input.Changed) + def update_validation_label(self, message: Input.Changed) -> None: + if message.input.id is None: + return + label = self.footer_label + if message.input.id in ( + "textarea__save_input", + "textarea__open_input", + "textarea__gotoline_input", + ): + message.stop() + if message.validation_result and not message.validation_result.is_valid: + label.add_class("validation-error") + label.update(";".join(message.validation_result.failure_descriptions)) + elif ( + message.validation_result + and message.validation_result.is_valid + and message.input.id in ("textarea__save_input", "textarea__open_input") + ): + action = "Saving to" if "save" in message.input.id else "Opening" + p = Path(message.input.value).expanduser().resolve() + with suppress(ValueError): + p = Path("~") / p.relative_to(Path.home()) + label.remove_class("validation-error") + label.update(f"{action} {p}") + else: + label.remove_class("validation-error") + label.update("") + elif message.input.id in ("textarea__find_input"): + message.stop() + self._find_next_after_cursor(value=message.value) + + @on(Input.Submitted, "#textarea__save_input") + def save_file(self, message: Input.Submitted) -> None: + """ + Handle the submit event for the Save and Open modals. + """ + message.stop() + expanded_path = Path(message.input.value).expanduser() + try: + expanded_path.parent.mkdir(parents=True, exist_ok=True) + with open(expanded_path, "w") as f: + f.write(self.text) + except OSError as e: + self.app.push_screen( + ErrorModal( + title="Save File Error", + header=("There was an error when attempting to save your file:"), + error=e, + ) + ) + else: + self.post_message(TextAreaSaved(path=expanded_path)) + self._clear_footer_input() + if self.text_input is not None: + self.text_input.focus() + + @on(Input.Submitted, "#textarea__open_input") + def open_file(self, message: Input.Submitted) -> None: + message.stop() + expanded_path = Path(message.input.value).expanduser() + try: + with open(expanded_path, "r") as f: + contents = f.read() + except OSError as e: + self.app.push_screen( + ErrorModal( + title="Open File Error", + header=("There was an error when attempting to open your file:"), + error=e, + ) + ) + else: + self.text = contents + self._clear_footer_input() + if self.text_input is not None: + self.text_input.focus() + + @on(Input.Submitted, "#textarea__gotoline_input") + def goto_line(self, message: Input.Submitted) -> None: + message.stop() + assert self.text_input is not None + try: + new_line = int(message.value) - 1 + except (ValueError, TypeError): + return + self.text_input.move_cursor((new_line, 0), select=False) + self._clear_footer_input() + self.text_input.focus() + + @on(Input.Submitted, "#textarea__find_input") + def find_next(self, message: Input.Submitted) -> None: + message.stop() + message.input.checkpoint() # type: ignore + self.selection = Selection(start=self.selection.end, end=self.selection.end) + self._find_next_after_cursor(value=message.value) + + def watch_theme(self, theme: str) -> None: + if self.text_input is None: + self.app.notify( + message=( + "Could not load the selected theme in the TextArea, because " + "it has not yet loaded. Please try again." + ), + severity="warning", + ) + return + + if theme in self.text_input.available_themes: + self.text_input.theme = theme + else: + css_vars = self.app.get_css_variables() + theme_obj = self.app.get_theme(theme_name=theme) + if theme_obj is None: + self.post_message(TextAreaThemeError(theme=theme)) + return + textarea_theme = text_area_theme_from_app_theme(theme, theme_obj, css_vars) + self.text_input.register_theme(textarea_theme) + self.text_input.theme = theme + + def action_save(self) -> None: + self._clear_footer_input() + self._mount_footer_path_input("save") + + def action_load(self) -> None: + self._clear_footer_input() + self._mount_footer_path_input("open") + + def action_find(self, prepopulate_from_history: bool = False) -> None: + try: + find_input = self.footer.query_one(FindInput) + except Exception: + pass + else: + find_input.focus() + return + self._clear_footer_input() + if prepopulate_from_history and self._find_history: + value = self._find_history[-1] + else: + value = "" + find_input = FindInput( + value=value, + history=self._find_history, + classes="textarea--footer-input", + ) + self._mount_footer_input(input_widget=find_input) + + def action_goto_line(self) -> None: + try: + goto_input = self.footer.query_one(GotoLineInput) + except Exception: + pass + else: + goto_input.focus() + return + self._clear_footer_input() + goto_input = GotoLineInput( + max_line_number=self.text_input.document.line_count + if self.text_input is not None + else 10000, + current_line=self.selection.end[0] + 1, + min_line_number=1, + id="textarea__gotoline_input", + classes="textarea--footer-input", + ) + self._mount_footer_input(input_widget=goto_input) + + def _clear_footer_input(self) -> None: + try: + self.footer.query_one(Input).remove() + except Exception: + pass + try: + self.footer_label.update("") + except Exception: + pass + self.footer.add_class("hide") + + def _mount_footer_input(self, input_widget: Input) -> None: + self.footer.remove_class("hide") + try: + self.footer.mount(input_widget) + except DuplicateIds: + return + else: + input_widget.focus() + + def _mount_footer_path_input(self, name: str) -> None: + if name == "open": + file_okay, dir_okay, must_exist = True, False, True + else: + file_okay, dir_okay, must_exist = True, False, False + + path_input = PathInput( + id=f"textarea__{name}_input", + placeholder=f"{name.capitalize()}: Enter file path OR press ESC to cancel", + file_okay=file_okay, + dir_okay=dir_okay, + must_exist=must_exist, + classes="textarea--footer-input", + ) + self._mount_footer_input(input_widget=path_input) + + def _find_next_after_cursor(self, value: str) -> None: + assert self.text_input is not None + label = self.footer_label + if not value: + label.update("") + return + cursor = self.selection.start + lines = self.text_input.document.lines + # first search text after the cursor + for i, line in enumerate(lines[cursor[0] :]): + pos = line.find(value, cursor[1] if i == 0 else None) + if pos >= 0: + self.selection = Selection( + start=(cursor[0] + i, pos), + end=(cursor[0] + i, pos + cell_len(value)), + ) + break + # search text from beginning, including line with cursor + else: + for i, line in enumerate(lines[: cursor[0] + 1]): + pos = line.find(value) + if pos >= 0: + self.selection = Selection( + start=(i, pos), end=(i, pos + cell_len(value)) + ) + break + self.text_input.scroll_cursor_visible(animate=True) + self._update_find_label(value=value) + + def _update_find_label(self, value: str) -> None: + label = self.footer_label + n_matches = self.text.count(value) + if n_matches > 1: + label.update(f"{n_matches} found; Enter for next; ESC to close") + elif n_matches > 0: + label.update(f"{n_matches} found") + else: + label.update("No results.") diff --git a/stubs/pyperclip/__init__.pyi b/stubs/pyperclip/__init__.pyi new file mode 100644 index 0000000..93a3f09 --- /dev/null +++ b/stubs/pyperclip/__init__.pyi @@ -0,0 +1,7 @@ +from typing import Callable, Tuple + +def copy(s: str) -> None: ... +def paste() -> str: ... +def determine_clipboard() -> Tuple[Callable[[str], None], Callable[[], str]]: ... + +class PyperclipException(Exception): ... diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..d7bb3c4 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,9 @@ +from pathlib import Path + +import pytest + + +@pytest.fixture +def data_dir() -> Path: + here = Path(__file__) + return here.parent / "data" diff --git a/tests/data/test_open/empty.py b/tests/data/test_open/empty.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/data/test_open/foo.py b/tests/data/test_open/foo.py new file mode 100644 index 0000000..c13cc94 --- /dev/null +++ b/tests/data/test_open/foo.py @@ -0,0 +1,2 @@ +def foo(bar: str, baz: int) -> None: + return diff --git a/tests/data/test_validator/bar/.gitkeep b/tests/data/test_validator/bar/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/data/test_validator/foo/baz.txt b/tests/data/test_validator/foo/baz.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/functional_tests/__init__.py b/tests/functional_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/functional_tests/conftest.py b/tests/functional_tests/conftest.py new file mode 100644 index 0000000..a3be044 --- /dev/null +++ b/tests/functional_tests/conftest.py @@ -0,0 +1,62 @@ +from typing import Type, Union +from unittest.mock import MagicMock + +import pytest +from textual.app import App, ComposeResult +from textual.driver import Driver +from textual.types import CSSPathType +from textual_textarea.text_editor import TextEditor + + +class TextEditorApp(App, inherit_bindings=False): + def __init__( + self, + driver_class: Union[Type[Driver], None] = None, + css_path: Union[CSSPathType, None] = None, + watch_css: bool = False, + language: Union[str, None] = None, + use_system_clipboard: bool = True, + ): + self.language = language + self.use_system_clipboard = use_system_clipboard + super().__init__(driver_class, css_path, watch_css) + + def compose(self) -> ComposeResult: + self.editor = TextEditor( + language=self.language, + use_system_clipboard=self.use_system_clipboard, + id="ta", + ) + yield self.editor + + def on_mount(self) -> None: + self.editor.focus() + + +@pytest.fixture +def app() -> App: + app = TextEditorApp(language="python") + return app + + +@pytest.fixture( + params=[False, True], + ids=["no_sys_clipboard", "default"], +) +def app_all_clipboards(request: pytest.FixtureRequest) -> App: + app = TextEditorApp(use_system_clipboard=request.param) + return app + + +@pytest.fixture(autouse=True) +def mock_pyperclip(monkeypatch: pytest.MonkeyPatch) -> MagicMock: + mock = MagicMock() + mock.determine_clipboard.return_value = mock.copy, mock.paste + + def set_paste(x: str) -> None: + mock.paste.return_value = x + + mock.copy.side_effect = set_paste + monkeypatch.setattr("textual_textarea.text_editor.pyperclip", mock) + + return mock diff --git a/tests/functional_tests/test_autocomplete.py b/tests/functional_tests/test_autocomplete.py new file mode 100644 index 0000000..1065de8 --- /dev/null +++ b/tests/functional_tests/test_autocomplete.py @@ -0,0 +1,279 @@ +from __future__ import annotations + +from pathlib import Path +from time import monotonic +from typing import Callable +from unittest.mock import MagicMock + +import pytest +from textual.app import App +from textual.message import Message +from textual.widgets.text_area import Selection +from textual_textarea import TextEditor + + +@pytest.fixture +def word_completer() -> Callable[[str], list[tuple[str, str]]]: + def _completer(prefix: str) -> list[tuple[str, str]]: + words = [ + "satisfy", + "season", + "second", + "seldom", + "select", + "self", + "separate", + "set", + "space", + "super", + ] + return [(w, w) for w in words if w.startswith(prefix)] + + return _completer + + +@pytest.fixture +def word_completer_with_types() -> Callable[[str], list[tuple[tuple[str, str], str]]]: + def _completer(prefix: str) -> list[tuple[tuple[str, str], str]]: + words = [ + "satisfy", + "season", + "second", + "seldom", + "select", + "self", + "separate", + "set", + "space", + "super", + ] + return [((w, "word"), w) for w in words if w.startswith(prefix)] + + return _completer + + +@pytest.fixture +def member_completer() -> Callable[[str], list[tuple[str, str]]]: + mock = MagicMock() + mock.return_value = [("completion", "completion")] + return mock + + +@pytest.mark.asyncio +async def test_autocomplete( + app: App, word_completer: Callable[[str], list[tuple[str, str]]] +) -> None: + messages: list[Message] = [] + async with app.run_test(message_hook=messages.append) as pilot: + ta = app.query_one("#ta", expect_type=TextEditor) + ta.word_completer = word_completer + ta.focus() + while ta.word_completer is None: + await pilot.pause() + + start_time = monotonic() + await pilot.press("s") + while ta.completion_list.is_open is False: + if monotonic() - start_time > 10: + print("MESSAGES:") + print("\n".join([str(m) for m in messages])) + break + await pilot.pause() + assert ta.text_input + assert ta.text_input.completer_active == "word" + assert ta.completion_list.is_open is True + assert ta.completion_list.option_count == 10 + first_offset = ta.completion_list.styles.offset + + await pilot.press("e") + await app.workers.wait_for_complete() + await pilot.pause() + assert ta.text_input.completer_active == "word" + assert ta.completion_list.is_open is True + assert ta.completion_list.option_count == 7 + assert ta.completion_list.styles.offset == first_offset + + await pilot.press("z") # sez, no matches + await app.workers.wait_for_complete() + await pilot.pause() + assert ta.text_input.completer_active is None + assert ta.completion_list.is_open is False + + # backspace when the list is not open doesn't re-open it + await pilot.press("backspace") + await app.workers.wait_for_complete() + await pilot.pause() + assert ta.text_input.completer_active is None + assert ta.completion_list.is_open is False + + await pilot.press("l") # sel + await app.workers.wait_for_complete() + await pilot.pause() + assert ta.text_input.completer_active == "word" + assert ta.completion_list.is_open is True + assert ta.completion_list.option_count == 3 + assert ta.completion_list.styles.offset == first_offset + + await pilot.press("backspace") # se + await app.workers.wait_for_complete() + await pilot.pause() + assert ta.text_input.completer_active == "word" + assert ta.completion_list.is_open is True + assert ta.completion_list.option_count == 7 + assert ta.completion_list.styles.offset == first_offset + + await pilot.press("enter") + await app.workers.wait_for_complete() + await pilot.pause() + assert ta.text_input.completer_active is None + assert ta.completion_list.is_open is False + assert ta.text == "season" + assert ta.selection.end[1] == 6 + + +@pytest.mark.asyncio +async def test_autocomplete_with_types( + app: App, + word_completer_with_types: Callable[[str], list[tuple[tuple[str, str], str]]], +) -> None: + messages: list[Message] = [] + word_completer = word_completer_with_types + async with app.run_test(message_hook=messages.append) as pilot: + ta = app.query_one("#ta", expect_type=TextEditor) + ta.word_completer = word_completer + ta.focus() + while ta.word_completer is None: + await pilot.pause() + + start_time = monotonic() + await pilot.press("s") + while ta.completion_list.is_open is False: + if monotonic() - start_time > 10: + print("MESSAGES:") + print("\n".join([str(m) for m in messages])) + break + await pilot.pause() + assert ta.text_input + assert ta.text_input.completer_active == "word" + assert ta.completion_list.is_open is True + assert ta.completion_list.option_count == 10 + first_offset = ta.completion_list.styles.offset + + await pilot.press("e") + await app.workers.wait_for_complete() + await pilot.pause() + assert ta.text_input.completer_active == "word" + assert ta.completion_list.is_open is True + assert ta.completion_list.option_count == 7 + assert ta.completion_list.styles.offset == first_offset + + await pilot.press("z") # sez, no matches + await app.workers.wait_for_complete() + await pilot.pause() + assert ta.text_input.completer_active is None + assert ta.completion_list.is_open is False + + # backspace when the list is not open doesn't re-open it + await pilot.press("backspace") + await app.workers.wait_for_complete() + await pilot.pause() + assert ta.text_input.completer_active is None + assert ta.completion_list.is_open is False + + await pilot.press("l") # sel + await app.workers.wait_for_complete() + await pilot.pause() + assert ta.text_input.completer_active == "word" + assert ta.completion_list.is_open is True + assert ta.completion_list.option_count == 3 + assert ta.completion_list.styles.offset == first_offset + + await pilot.press("backspace") # se + await app.workers.wait_for_complete() + await pilot.pause() + assert ta.text_input.completer_active == "word" + assert ta.completion_list.is_open is True + assert ta.completion_list.option_count == 7 + assert ta.completion_list.styles.offset == first_offset + + await pilot.press("enter") + await app.workers.wait_for_complete() + await pilot.pause() + assert ta.text_input.completer_active is None + assert ta.completion_list.is_open is False + assert ta.text == "season" + assert ta.selection.end[1] == 6 + + +@pytest.mark.asyncio +async def test_autocomplete_paths(app: App, data_dir: Path) -> None: + messages: list[Message] = [] + async with app.run_test(message_hook=messages.append) as pilot: + ta = app.query_one("#ta", expect_type=TextEditor) + ta.focus() + test_path = str(data_dir / "test_validator") + ta.text = test_path + await pilot.pause() + ta.selection = Selection((0, len(test_path)), (0, len(test_path))) + + start_time = monotonic() + await pilot.press("slash") + while ta.completion_list.is_open is False: + if monotonic() - start_time > 10: + print("MESSAGES:") + print("\n".join([str(m) for m in messages])) + break + await pilot.pause() + assert ta.text_input + assert ta.text_input.completer_active == "path" + assert ta.completion_list.is_open is True + assert ta.completion_list.option_count == 2 + + +@pytest.mark.parametrize( + "text,keys,expected_prefix", + [ + ("foo bar", ["full_stop"], "bar."), + ("foo 'bar'", ["full_stop"], "'bar'."), + ("foo `bar`", ["full_stop"], "`bar`."), + ('foo "bar"', ["full_stop"], '"bar".'), + ("foo bar", ["colon"], "bar:"), + ("foo bar", ["colon", "colon"], "bar::"), + ('foo "bar"', ["colon", "colon"], '"bar"::'), + ("foo bar", ["full_stop", "quotation_mark"], 'bar."'), + ('foo "bar"', ["full_stop", "quotation_mark"], '"bar"."'), + ], +) +@pytest.mark.asyncio +async def test_autocomplete_members( + app: App, + member_completer: MagicMock, + text: str, + keys: list[str], + expected_prefix: str, +) -> None: + messages: list[Message] = [] + async with app.run_test(message_hook=messages.append) as pilot: + ta = app.query_one("#ta", expect_type=TextEditor) + ta.member_completer = member_completer + ta.focus() + while ta.member_completer is None: + await pilot.pause() + ta.text = text + ta.selection = Selection((0, len(text)), (0, len(text))) + await pilot.pause() + for key in keys: + await pilot.press(key) + + start_time = monotonic() + while ta.completion_list.is_open is False: + if monotonic() - start_time > 10: + print("MESSAGES:") + print("\n".join([str(m) for m in messages])) + break + await pilot.pause() + + member_completer.assert_called_with(expected_prefix) + assert ta.text_input is not None + assert ta.text_input.completer_active == "member" + assert ta.completion_list.is_open is True diff --git a/tests/functional_tests/test_comments.py b/tests/functional_tests/test_comments.py new file mode 100644 index 0000000..75d96f3 --- /dev/null +++ b/tests/functional_tests/test_comments.py @@ -0,0 +1,29 @@ +import pytest +from textual.app import App +from textual.widgets.text_area import Selection +from textual_textarea import TextEditor + + +@pytest.mark.parametrize( + "language,expected_marker", + [ + ("python", "# "), + ("sql", "-- "), + # ("mysql", "# "), + # ("c", "// "), + ], +) +@pytest.mark.asyncio +async def test_comments(app: App, language: str, expected_marker: str) -> None: + async with app.run_test() as pilot: + ta = app.query_one("#ta", expect_type=TextEditor) + ta.language = language + original_text = "foo bar baz" + ta.text = original_text + ta.selection = Selection((0, 0), (0, 0)) + + await pilot.press("ctrl+underscore") # alias for ctrl+/ + assert ta.text == f"{expected_marker}{original_text}" + + await pilot.press("ctrl+underscore") # alias for ctrl+/ + assert ta.text == f"{original_text}" diff --git a/tests/functional_tests/test_find.py b/tests/functional_tests/test_find.py new file mode 100644 index 0000000..10454a3 --- /dev/null +++ b/tests/functional_tests/test_find.py @@ -0,0 +1,143 @@ +import pytest +from textual.app import App +from textual_textarea import TextEditor +from textual_textarea.find_input import FindInput + + +@pytest.mark.asyncio +async def test_find(app: App) -> None: + async with app.run_test() as pilot: + ta = app.query_one("#ta", expect_type=TextEditor) + ta.text = "foo bar\n" * 50 + await pilot.pause() + assert ta.selection.start == ta.selection.end == (0, 0) + await pilot.press("ctrl+f") + + find_input = app.query_one(FindInput) + assert find_input + assert find_input.has_focus + assert "Find" in find_input.placeholder + + await pilot.press("b") + assert find_input.has_focus + assert ta.selection.start == (0, 4) + assert ta.selection.end == (0, 5) + + await pilot.press("a") + assert find_input.has_focus + assert ta.selection.start == (0, 4) + assert ta.selection.end == (0, 6) + + await pilot.press("enter") + assert find_input.has_focus + assert ta.selection.start == (1, 4) + assert ta.selection.end == (1, 6) + + await pilot.press("escape") + assert ta.text_input + assert ta.text_input.has_focus + assert ta.selection.start == (1, 4) + assert ta.selection.end == (1, 6) + + await pilot.press("ctrl+f") + + find_input = app.query_one(FindInput) + await pilot.press("f") + assert find_input.has_focus + assert ta.selection.start == (2, 0) + assert ta.selection.end == (2, 1) + + +@pytest.mark.asyncio +async def test_find_history(app: App) -> None: + async with app.run_test() as pilot: + ta = app.query_one("#ta", expect_type=TextEditor) + ta.text = "foo bar\n" * 50 + await pilot.pause() + + # add an item to the history by pressing enter + await pilot.press("ctrl+f") + await pilot.press("a") + await pilot.press("enter") + await pilot.press("escape") + + # re-open the find input and navigate the one-item + # history + await pilot.press("ctrl+f") + find_input = app.query_one(FindInput) + assert find_input.value == "" + await pilot.press("up") + assert find_input.value == "a" + await pilot.press("down") + assert find_input.value == "" + await pilot.press("up") + await pilot.press("up") + assert find_input.value == "a" + await pilot.press("down") + assert find_input.value == "" + + # add an item to the history by closing the find input + await pilot.press("b") + await pilot.press("escape") + + # navigate the two-item history + await pilot.press("ctrl+f") + find_input = app.query_one(FindInput) + assert find_input.value == "" + await pilot.press("up") + assert find_input.value == "b" + await pilot.press("down") + assert find_input.value == "" + await pilot.press("up") + assert find_input.value == "b" + await pilot.press("up") + assert find_input.value == "a" + await pilot.press("up") + assert find_input.value == "a" + await pilot.press("down") + assert find_input.value == "b" + await pilot.press("down") + assert find_input.value == "" + + +@pytest.mark.asyncio +async def test_find_with_f3(app: App) -> None: + async with app.run_test() as pilot: + ta = app.query_one("#ta", expect_type=TextEditor) + ta.text = "foo bar\n" * 50 + await pilot.pause() + assert ta.selection.start == ta.selection.end == (0, 0) + + # pressing f3 with no history brings up an empty find box + await pilot.press("f3") + find_input = app.query_one(FindInput) + assert find_input + assert find_input.has_focus + assert find_input.value == "" + + await pilot.press("b") + assert find_input.has_focus + assert ta.selection.start == (0, 4) + assert ta.selection.end == (0, 5) + + # pressing f3 from the find input finds the next match + await pilot.press("f3") + assert find_input.has_focus + assert ta.selection.start == (1, 4) + assert ta.selection.end == (1, 5) + + # close the find input and navigate up one line + await pilot.press("escape") + await pilot.press("up") + + # pressing f3 with history prepopulates the find input + await pilot.press("f3") + find_input = app.query_one(FindInput) + assert find_input.value == "b" + assert ta.selection.start == (1, 4) + assert ta.selection.end == (1, 5) + + # pressing again advances to the next match + await pilot.press("f3") + assert ta.selection.start == (2, 4) + assert ta.selection.end == (2, 5) diff --git a/tests/functional_tests/test_goto.py b/tests/functional_tests/test_goto.py new file mode 100644 index 0000000..8add0bc --- /dev/null +++ b/tests/functional_tests/test_goto.py @@ -0,0 +1,36 @@ +import pytest +from textual.app import App +from textual_textarea import TextEditor +from textual_textarea.goto_input import GotoLineInput + + +@pytest.mark.asyncio +async def test_goto_line(app: App) -> None: + async with app.run_test() as pilot: + ta = app.query_one("#ta", expect_type=TextEditor) + ta.text = "\n" * 50 + await pilot.pause() + assert ta.selection.start == ta.selection.end == (0, 0) + await pilot.press("ctrl+g") + + goto_input = app.query_one(GotoLineInput) + assert goto_input + assert goto_input.has_focus + assert "51" in goto_input.placeholder + + await pilot.press("1") + await pilot.press("2") + await pilot.press("enter") + + assert ta.text_input + assert ta.text_input.has_focus + assert ta.selection.start == ta.selection.end == (11, 0) + + # ensure pressing ctrl+g twice doesn't crash + + await pilot.press("ctrl+g") + goto_input = app.query_one(GotoLineInput) + assert goto_input.has_focus + await pilot.press("2") + + await pilot.press("ctrl+g") diff --git a/tests/functional_tests/test_open.py b/tests/functional_tests/test_open.py new file mode 100644 index 0000000..2484756 --- /dev/null +++ b/tests/functional_tests/test_open.py @@ -0,0 +1,70 @@ +from pathlib import Path +from typing import List + +import pytest +from textual.app import App +from textual.message import Message +from textual.widgets import Input +from textual_textarea import TextAreaSaved, TextEditor + + +@pytest.mark.parametrize("filename", ["foo.py", "empty.py"]) +@pytest.mark.asyncio +async def test_open(data_dir: Path, app: App, filename: str) -> None: + p = data_dir / "test_open" / filename + with open(p, "r") as f: + contents = f.read() + + async with app.run_test() as pilot: + ta = app.query_one("#ta", expect_type=TextEditor) + assert ta.text == "" + starting_text = "123" + for key in starting_text: + await pilot.press(key) + assert ta.text == starting_text + + await pilot.press("ctrl+o") + open_input = ta.query_one(Input) + assert open_input.id and "open" in open_input.id + assert open_input.has_focus + + for key in str(p): + await pilot.press(key) + await pilot.press("enter") + + assert ta.text == contents + assert ta.text_input is not None + assert ta.text_input.has_focus + + # make sure the end of the buffer is formatted properly. + # these previously caused a crash. + await pilot.press("ctrl+end") + assert ta.selection.end[1] >= 0 + await pilot.press("enter") + + +@pytest.mark.asyncio +async def test_save(app: App, tmp_path: Path) -> None: + TEXT = "select\n 1 as a,\n 2 as b,\n 'c' as c" + p = tmp_path / "text.sql" + print(p) + messages: List[Message] = [] + async with app.run_test(message_hook=messages.append) as pilot: + ta = app.query_one("#ta", expect_type=TextEditor) + ta.text = TEXT + + await pilot.press("ctrl+s") + save_input = ta.query_one(Input) + assert save_input.id and "save" in save_input.id + assert save_input.has_focus + + save_input.value = str(p) + await pilot.press("enter") + await pilot.pause() + assert len(messages) > 1 + assert Input.Submitted in [msg.__class__ for msg in messages] + assert TextAreaSaved in [msg.__class__ for msg in messages] + + with open(p, "r") as f: + saved_text = f.read() + assert saved_text == TEXT diff --git a/tests/functional_tests/test_textarea.py b/tests/functional_tests/test_textarea.py new file mode 100644 index 0000000..951b0bd --- /dev/null +++ b/tests/functional_tests/test_textarea.py @@ -0,0 +1,462 @@ +from __future__ import annotations + +from typing import List + +import pytest +from textual.app import App +from textual.widgets.text_area import Selection +from textual_textarea import TextEditor + + +@pytest.mark.parametrize( + "keys,text,selection,expected_text,expected_selection", + [ + ( + ["ctrl+a"], + "select\n foo", + Selection(start=(1, 2), end=(1, 2)), + "select\n foo", + Selection(start=(0, 0), end=(1, 4)), + ), + ( + ["ctrl+shift+right"], + "select\n foo", + Selection(start=(0, 0), end=(0, 0)), + "select\n foo", + Selection(start=(0, 0), end=(0, 6)), + ), + ( + ["right"], + "select\n foo", + Selection(start=(0, 0), end=(0, 6)), + "select\n foo", + Selection(start=(1, 0), end=(1, 0)), + ), + ( + ["a"], + "select\n foo", + Selection(start=(1, 4), end=(1, 4)), + "select\n fooa", + Selection(start=(1, 5), end=(1, 5)), + ), + ( + ["a"], + "select\n foo", + Selection(start=(1, 0), end=(1, 4)), + "select\na", + Selection(start=(1, 1), end=(1, 1)), + ), + ( + ["enter"], + "a\na", + Selection(start=(1, 0), end=(1, 0)), + "a\n\na", + Selection(start=(2, 0), end=(2, 0)), + ), + ( + ["enter"], + "a\na", + Selection(start=(1, 1), end=(1, 1)), + "a\na\n", + Selection(start=(2, 0), end=(2, 0)), + ), + ( + ["enter", "b"], + "a()", + Selection(start=(0, 2), end=(0, 2)), + "a(\n b\n)", + Selection(start=(1, 5), end=(1, 5)), + ), + ( + ["enter", "b"], + " a()", + Selection(start=(0, 3), end=(0, 3)), + " a(\n b\n )", + Selection(start=(1, 5), end=(1, 5)), + ), + ( + ["delete"], + "0\n1\n2\n3", + Selection(start=(2, 1), end=(2, 1)), + "0\n1\n23", + Selection(start=(2, 1), end=(2, 1)), + ), + ( + ["shift+delete"], + "0\n1\n2\n3", + Selection(start=(2, 1), end=(2, 1)), + "0\n1\n3", + Selection(start=(2, 0), end=(2, 0)), + ), + ( + ["shift+delete"], + "0\n1\n2\n3", + Selection(start=(2, 0), end=(2, 1)), + "0\n1\n\n3", + Selection(start=(2, 0), end=(2, 0)), + ), + ( + ["shift+delete"], + "0\n1\n2\n3", + Selection(start=(3, 1), end=(3, 1)), + "0\n1\n2", + Selection(start=(2, 1), end=(2, 1)), + ), + ( + ["shift+delete"], + "foo", + Selection(start=(3, 1), end=(3, 1)), + "", + Selection(start=(0, 0), end=(0, 0)), + ), + ( + ["ctrl+home"], + "foo\nbar", + Selection(start=(1, 2), end=(1, 2)), + "foo\nbar", + Selection(start=(0, 0), end=(0, 0)), + ), + ( + ["ctrl+end"], + "foo\nbar", + Selection(start=(0, 1), end=(0, 1)), + "foo\nbar", + Selection(start=(1, 3), end=(1, 3)), + ), + ( + ["("], + "foo", + Selection(start=(0, 3), end=(0, 3)), + "foo()", + Selection(start=(0, 4), end=(0, 4)), + ), + ( + ["("], + "foo", + Selection(start=(0, 2), end=(0, 2)), + "fo(o", + Selection(start=(0, 3), end=(0, 3)), + ), + ( + ["("], + "foo.", + Selection(start=(0, 3), end=(0, 3)), + "foo().", + Selection(start=(0, 4), end=(0, 4)), + ), + ( + ["("], + "foo-", + Selection(start=(0, 3), end=(0, 3)), + "foo(-", + Selection(start=(0, 4), end=(0, 4)), + ), + ( + ["'"], + "foo", + Selection(start=(0, 3), end=(0, 3)), + "foo'", + Selection(start=(0, 4), end=(0, 4)), + ), + ( + ["'"], + "ba r", + Selection(start=(0, 3), end=(0, 3)), + "ba '' r", + Selection(start=(0, 4), end=(0, 4)), + ), + ( + ["'"], + "foo-", + Selection(start=(0, 3), end=(0, 3)), + "foo'-", + Selection(start=(0, 4), end=(0, 4)), + ), + ( + ["'"], + "fo--", + Selection(start=(0, 3), end=(0, 3)), + "fo-'-", + Selection(start=(0, 4), end=(0, 4)), + ), + ( + ["'"], + "fo-.", + Selection(start=(0, 3), end=(0, 3)), + "fo-''.", + Selection(start=(0, 4), end=(0, 4)), + ), + ( + ["'"], + "fo()", + Selection(start=(0, 3), end=(0, 3)), + "fo('')", + Selection(start=(0, 4), end=(0, 4)), + ), + ( + ["tab"], + "bar", + Selection(start=(0, 1), end=(0, 1)), + "b ar", + Selection(start=(0, 4), end=(0, 4)), + ), + ( + ["tab"], + "bar", + Selection(start=(0, 0), end=(0, 0)), + " bar", + Selection(start=(0, 4), end=(0, 4)), + ), + ( + ["shift+tab"], + "bar", + Selection(start=(0, 0), end=(0, 0)), + "bar", + Selection(start=(0, 0), end=(0, 0)), + ), + ( + ["shift+tab"], + " bar", + Selection(start=(0, 7), end=(0, 7)), + "bar", + Selection(start=(0, 3), end=(0, 3)), + ), + ( + ["tab"], + "bar\n baz", + Selection(start=(0, 2), end=(1, 1)), + " bar\n baz", + Selection(start=(0, 6), end=(1, 4)), + ), + ( + ["tab"], + "bar\n baz", + Selection(start=(0, 0), end=(1, 1)), + " bar\n baz", + Selection(start=(0, 0), end=(1, 4)), + ), + ( + ["shift+tab"], + " bar\n baz", + Selection(start=(0, 0), end=(1, 1)), + "bar\nbaz", + Selection(start=(0, 0), end=(1, 0)), + ), + ], +) +@pytest.mark.asyncio +async def test_keys( + app: App, + keys: List[str], + text: str, + selection: Selection, + expected_text: str, + expected_selection: Selection, +) -> None: + async with app.run_test() as pilot: + ta = app.query_one("#ta", expect_type=TextEditor) + ta.text = text + ta.selection = selection + + for key in keys: + await pilot.press(key) + + assert ta.text == expected_text + assert ta.selection == expected_selection + + +@pytest.mark.parametrize( + "starting_selection,expected_clipboard,expected_paste_loc", + [ + (Selection((0, 5), (1, 5)), "56789\n01234", (1, 5)), + (Selection((0, 0), (1, 0)), "0123456789\n", (1, 0)), + ], +) +@pytest.mark.asyncio +async def test_copy_paste( + app_all_clipboards: App, + starting_selection: Selection, + expected_clipboard: str, + expected_paste_loc: tuple[int, int], +) -> None: + original_text = "0123456789\n0123456789\n0123456789" + + def eq(a: str, b: str) -> bool: + return a.replace("\r\n", "\n") == b.replace("\r\n", "\n") + + async with app_all_clipboards.run_test() as pilot: + ta = app_all_clipboards.query_one("#ta", expect_type=TextEditor) + while ta.text_input is None: + await pilot.pause(0.1) + ti = ta.text_input + assert ti is not None + ta.text = original_text + ta.selection = starting_selection + + await pilot.press("ctrl+c") + await pilot.pause() + assert eq(ti.clipboard, expected_clipboard) + assert ta.selection == starting_selection + assert ta.text == original_text + + await pilot.press("ctrl+u") + await pilot.pause() + assert eq(ti.clipboard, expected_clipboard) + assert ta.selection == Selection(starting_selection.end, starting_selection.end) + assert ta.text == original_text + + await pilot.press("ctrl+a") + assert ta.selection == Selection( + (0, 0), + (len(original_text.splitlines()) - 1, len(original_text.splitlines()[-1])), + ) + assert eq(ti.clipboard, expected_clipboard) + assert ta.text == original_text + + await pilot.press("ctrl+u") + await pilot.pause() + assert ta.selection == Selection(expected_paste_loc, expected_paste_loc) + assert eq(ti.clipboard, expected_clipboard) + assert ta.text == expected_clipboard + + await pilot.press("ctrl+a") + await pilot.press("ctrl+x") + await pilot.pause() + assert ta.selection == Selection((0, 0), (0, 0)) + assert eq(ti.clipboard, expected_clipboard) + assert ta.text == "" + + await pilot.press("ctrl+v") + await pilot.pause() + assert eq(ti.clipboard, expected_clipboard) + assert ta.text == expected_clipboard + assert ta.selection == Selection(expected_paste_loc, expected_paste_loc) + + +@pytest.mark.asyncio +async def test_undo_redo(app: App) -> None: + async with app.run_test() as pilot: + ta = app.query_one("#ta", expect_type=TextEditor) + ti = ta.text_input + assert ti + assert ti.has_focus + + for char in "foo": + await pilot.press(char) + await pilot.pause(0.6) + + await pilot.press("enter") + for char in "bar": + await pilot.press(char) + await pilot.pause(0.6) + + await pilot.press("ctrl+z") + assert ta.text == "foo\n" + assert ta.selection == Selection((1, 0), (1, 0)) + + await pilot.press("ctrl+z") + assert ta.text == "foo" + assert ta.selection == Selection((0, 3), (0, 3)) + + await pilot.press("ctrl+z") + assert ta.text == "" + assert ta.selection == Selection((0, 0), (0, 0)) + + await pilot.press("ctrl+y") + assert ta.text == "foo" + assert ta.selection == Selection((0, 3), (0, 3)) + + await pilot.press("z") + assert ta.text == "fooz" + + +@pytest.mark.parametrize( + "start_text,insert_text,selection,expected_text", + [ + ( + "select ", + '"main"."drivers"."driverId"', + Selection((0, 7), (0, 7)), + 'select "main"."drivers"."driverId"', + ), + ( + "select , foo", + '"main"."drivers"."driverId"', + Selection((0, 7), (0, 7)), + 'select "main"."drivers"."driverId", foo', + ), + ( + "aaa\naaa\naaa\naaa", + "bb", + Selection((2, 2), (2, 2)), + "aaa\naaa\naabba\naaa", + ), + ( + "aaa\naaa\naaa\naaa", + "bb", + Selection((2, 2), (1, 1)), + "aaa\nabba\naaa", + ), + ( + "01234", + "\nabc\n", + Selection((0, 2), (0, 2)), + "01\nabc\n234", + ), + ], +) +@pytest.mark.asyncio +async def test_insert_text( + app: App, + start_text: str, + insert_text: str, + selection: Selection, + expected_text: str, +) -> None: + async with app.run_test() as pilot: + ta = app.query_one("#ta", expect_type=TextEditor) + ta.text = start_text + ta.selection = selection + await pilot.pause() + + ta.insert_text_at_selection(insert_text) + await pilot.pause() + + assert ta.text == expected_text + + +@pytest.mark.asyncio +async def test_toggle_comment(app: App) -> None: + async with app.run_test() as pilot: + ta = app.query_one("#ta", expect_type=TextEditor) + ta.text = "one\ntwo\n\nthree" + ta.selection = Selection((0, 0), (0, 0)) + await pilot.pause() + + await pilot.press("ctrl+underscore") + assert ta.text == "# one\ntwo\n\nthree" + + await pilot.press("down") + await pilot.press("ctrl+underscore") + assert ta.text == "# one\n# two\n\nthree" + + await pilot.press("ctrl+a") + await pilot.press("ctrl+underscore") + assert ta.text == "# # one\n# # two\n\n# three" + + await pilot.press("ctrl+underscore") + assert ta.text == "# one\n# two\n\nthree" + + await pilot.press("up") + await pilot.press("up") + await pilot.press("ctrl+underscore") + assert ta.text == "# one\ntwo\n\nthree" + + await pilot.press("shift+down") + await pilot.press("shift+down") + await pilot.press("ctrl+underscore") + assert ta.text == "# one\n# two\n\n# three" + + await pilot.press("ctrl+a") + await pilot.press("ctrl+underscore") + assert ta.text == "one\ntwo\n\nthree" diff --git a/tests/unit_tests/test_path_validator.py b/tests/unit_tests/test_path_validator.py new file mode 100644 index 0000000..1f652de --- /dev/null +++ b/tests/unit_tests/test_path_validator.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +from pathlib import Path + +import pytest +from textual_textarea.path_input import PathValidator, path_completer + + +@pytest.mark.parametrize( + "relpath,expected_matches", + [ + ("", ["foo", "bar"]), + ("f", ["foo"]), + ("fo", ["foo"]), + ("foo", ["baz.txt"]), + ("foo/", ["baz.txt"]), + ("b", ["bar"]), + ("c", []), + ], +) +def test_path_completer( + data_dir: Path, + relpath: str, + expected_matches: list[str], +) -> None: + test_path = data_dir / "test_validator" / relpath + test_dir = test_path if test_path.is_dir() else test_path.parent + prefix = str(test_path) + print(prefix) + matches = path_completer(prefix) + assert sorted(matches) == sorted( + [(str(test_dir / m), str(test_dir / m)) for m in expected_matches] + ) + + +@pytest.mark.parametrize( + "relpath,dir_okay,file_okay,must_exist,expected_result", + [ + ("foo", True, True, True, True), + ("foo", True, True, False, True), + ("foo", True, False, True, True), + ("foo", True, False, False, True), + ("foo", False, True, True, False), + ("foo", False, True, False, False), + ("foo", False, False, True, False), + ("foo", False, False, False, False), + ("bar", True, True, True, True), + ("bar", True, True, False, True), + ("bar", True, False, True, True), + ("bar", True, False, False, True), + ("bar", False, True, True, False), + ("bar", False, True, False, False), + ("bar", False, False, True, False), + ("bar", False, False, False, False), + ("baz", True, True, True, False), + ("baz", True, True, False, True), + ("baz", True, False, True, False), + ("baz", True, False, False, True), + ("baz", False, True, True, False), + ("baz", False, True, False, True), + ("baz", False, False, True, False), + ("baz", False, False, False, True), + ("foo/baz.txt", True, True, True, True), + ("foo/baz.txt", True, True, False, True), + ("foo/baz.txt", True, False, True, False), + ("foo/baz.txt", True, False, False, False), + ("foo/baz.txt", False, True, True, True), + ("foo/baz.txt", False, True, False, True), + ("foo/baz.txt", False, False, True, False), + ("foo/baz.txt", False, False, False, False), + ], +) +def test_path_validator( + data_dir: Path, + relpath: str, + dir_okay: bool, + file_okay: bool, + must_exist: bool, + expected_result: bool, +) -> None: + p = data_dir / "test_validator" / relpath + validator = PathValidator(dir_okay, file_okay, must_exist) + result = validator.validate(str(p)) + assert result.is_valid == expected_result diff --git a/textarea.svg b/textarea.svg new file mode 100644 index 0000000..2d79719 --- /dev/null +++ b/textarea.svg @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextApp + + + + + + + + + +  1  from textual.app import App, ComposeResult  + 2  from textual_textarea import TextEditor  + 3   + 4   + 5  classTextApp(App, inherit_bindings=False):  + 6  defcompose(self) -> ComposeResult:  + 7          self.editor =TextEditor + 8  language="python", + 9  theme="monokai",                                               +10              use_system_clipboard=True +11          )  +12  yield self.editor  +13   +14  defon_mount(self) -> None:  +15          self.editor.focus()  +16   +17   +18  if __name__ =="__main__" +19      app =TextApp()  +20      app.run()  +21   + + + + + +