diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..448b653
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,19 @@
+# Check http://editorconfig.org for more information
+# This is the main config file for this project:
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 2
+trim_trailing_whitespace = true
+
+[*.{py, pyi}]
+indent_size = 4
+
+[*.md]
+indent_size = 4
+trim_trailing_whitespace = false
+max_line_length = 120
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..1f5ed02
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,35 @@
+name: Bug report
+description: Found a bug? Let us know so we can fix it!
+labels: ["bug"]
+
+body:
+ - type: textarea
+ id: bug-description
+ attributes:
+ label: Bug description
+ description: Describe the bug. What's wrong?
+ validations:
+ required: true
+
+ - type: textarea
+ id: reproduction-steps
+ attributes:
+ label: Reproduction
+ description: Steps to reproduce the bug. You can also include code snippets. Try to keep things as minimal as you can.
+
+ - type: textarea
+ id: further-info
+ attributes:
+ label: Further info
+ description: Any further info such as images, exception tracebacks, ...
+
+ - type: checkboxes
+ id: checklist
+ attributes:
+ label: Checklist
+ description: Make sure to tick all the following boxes.
+ options:
+ - label: I have searched the issue tracker and have made sure it's not a duplicate. If it is a follow up of another issue, I have specified it.
+ required: true
+ - label: I have made sure to remove ANY sensitive information (passwords, credentials, personal details, etc.).
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000..4fec4bc
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,34 @@
+name: Feature request
+description: Got a cool idea you would like implemented? Let us know!
+labels: ["feature"]
+
+body:
+ - type: textarea
+ id: summary
+ attributes:
+ label: Summary
+ description: Quick summary of the feature.
+ validations:
+ required: true
+
+ - type: textarea
+ id: problem
+ attributes:
+ label: Why is this needed?
+ description: Why should this feature be implemented? What problem(s) would it solve?
+
+ - type: textarea
+ id: ideal-implementation
+ attributes:
+ label: Ideal implementation
+ description: How should this feature be implemented?
+ value: To be decided.
+
+ - type: checkboxes
+ id: checklist
+ attributes:
+ label: Checklist
+ description: Make sure to tick all the following boxes.
+ options:
+ - label: I have searched the issue tracker and have made sure it's not a duplicate. If it is a follow up of another issue, I have specified it.
+ required: true
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index aeda8d6..1ff942a 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -1,39 +1,21 @@
+---
name: CI
-on: [push]
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ workflow_dispatch:
+
+# Cancel already running workflows if new ones are scheduled
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
jobs:
- build-os-python:
- runs-on: ubuntu-latest
- strategy:
- max-parallel: 5
- fail-fast: true
- matrix:
- os: [ubuntu-latest, macos-latest, windows-latest]
- python-version: ["3.8", "3.10", "3.12"]
- steps:
- - uses: actions/checkout@v3
- - uses: goanpeca/setup-miniconda@v2.2.0
- with:
- miniforge-version: latest
- conda-version: ">=23.7.4"
- conda-build-version: ">=3.26"
- environment-file: environment.yml
- activate-environment: mkxref-dev
- python-version: ${{ matrix.python-version }}
- condarc-file: github-condarc.yml
- auto-activate-base: true
- use-mamba: true
- - name: Dev install package
- run: |
- conda run -n mkxref-dev pip install -e . --no-deps --no-build-isolation
- - name: ruff
- run: |
- make ruff
- - name: mypy
- if: success() || failure()
- run: |
- make mypy
- - name: Test with pytest
- if: success() || failure()
- run: |
- make coverage-test
+ validation:
+ uses: ./.github/workflows/validation.yml
+
+ unit-tests:
+ uses: ./.github/workflows/unit-tests.yml
diff --git a/.github/workflows/mkdocs.yml b/.github/workflows/mkdocs.yml
new file mode 100644
index 0000000..6d20bbc
--- /dev/null
+++ b/.github/workflows/mkdocs.yml
@@ -0,0 +1,71 @@
+---
+name: MkDocs
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ types: # Add closed type
+ - opened
+ - reopened
+ - synchronize
+ - closed # to delete the PR preview
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: write
+ pull-requests: write
+
+env:
+ PYTHON_VERSION: "3.14"
+
+jobs:
+ deploy-docs:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ token: "${{ github.token }}"
+ # Fetch the entire git history (all branches + tags)
+ # We do this because the docs use git describe, which requires having all
+ # the commits up to the latest version tag.
+ # We also need the gh-pages branch to push the docs to.
+ fetch-depth: 0
+
+ - name: Setup uv
+ uses: astral-sh/setup-uv@v5
+ with:
+ version: "latest"
+ python-version: ${{ env.PYTHON_VERSION }}
+ enable-cache: true
+ cache-suffix: "docs-py${{ env.python-version }}"
+
+ - name: Install dependencies
+ run: |
+ uv sync --no-default-groups --group docs
+
+ - name: Build the documentation (mkdocs - PR preview)
+ if: ${{ github.event_name == 'pull_request' }}
+ run: mkdocs build
+
+ - name: Deploy docs - PR preview
+ if: ${{ github.event_name == 'pull_request' }}
+ uses: rossjrw/pr-preview-action@v1
+ with:
+ source-dir: ./site
+ preview-branch: gh-pages
+ umbrella-dir: pr-preview
+ token: ${{ github.token }}
+
+ - name: Build the documentation (mike)
+ if: ${{ github.event_name == 'push' }}
+ run: mike deploy latest
+
+ - name: Deploy docs - latest
+ if: ${{ github.event_name == 'push' }}
+ run: git push origin gh-pages
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..7f2d3da
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,83 @@
+name: Publish to PyPI / GitHub
+
+on:
+ push:
+ tags:
+ - "v*"
+
+permissions:
+ contents: read
+
+env:
+ PYTHON_VERSION: "3.14"
+
+jobs:
+ build:
+ name: "Build the project"
+ runs-on: ubuntu-latest
+
+ outputs:
+ prerelease: ${{ steps.check-version.outputs.prerelease }}
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup uv
+ uses: astral-sh/setup-uv@v5
+ with:
+ version: "latest"
+ python-version: ${{ env.PYTHON_VERSION }}
+ enable-cache: true
+ cache-suffix: "publish-py${{ env.python-version }}"
+
+ - name: Install dependencies
+ run: |
+ # We only need the project dependencies, no development deps
+ uv sync --no-default-groups
+
+ - name: Upload build files
+ uses: actions/upload-artifact@v4
+ with:
+ name: "dist"
+ path: "dist/"
+ if-no-files-found: error
+ retention-days: 5
+
+ publish-github:
+ name: "Publish a GitHub release"
+ needs: build
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Download the distribution files from PR artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: "dist"
+ path: "dist/"
+
+ - name: Create Release
+ uses: ncipollo/release-action@v1
+ with:
+ artifacts: "dist/*"
+ bodyFile: changelog.txt
+ draft: false
+
+ publish-pypi:
+ name: "Publish to PyPI"
+ needs: build
+ runs-on: ubuntu-latest
+ permissions:
+ # Used to authenticate to PyPI via OIDC.
+ id-token: write
+
+ steps:
+ - name: Download the distribution files from PR artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: "dist"
+ path: "dist/"
+
+ # This uses PyPI's trusted publishing, so no token is required
+ - name: Release to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml
new file mode 100644
index 0000000..0019ee2
--- /dev/null
+++ b/.github/workflows/unit-tests.yml
@@ -0,0 +1,54 @@
+---
+name: Unit-Tests
+
+on: workflow_call
+
+jobs:
+ unit-tests:
+ strategy:
+ fail-fast: false # Allows for matrix sub-jobs to fail without cancelling the rest
+ matrix:
+ platform: [ubuntu-latest, windows-latest]
+ python-version: ["3.9", "3.14"]
+
+ runs-on: ${{ matrix.platform }}
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup uv
+ uses: astral-sh/setup-uv@v5
+ with:
+ version: "latest"
+ python-version: ${{ matrix.python-version }}
+ enable-cache: true
+ cache-suffix: "test-py${{ matrix.python-version }}"
+
+ - name: Install dependencies
+ run: |
+ uv sync --no-default-groups --group test
+
+ - name: Run pytest
+ shell: bash
+ run: pytest -vv
+
+ # This job is used purely to provide a workflow status, which we can mark as a
+ # required action in branch protection rules. This is a better option than marking
+ # the unit-tests jobs manually, since their names change as the supported python
+ # versions change. This job provides an easy single action that can be marked required.
+ tests-done:
+ needs: [unit-tests]
+ if: always() && !cancelled()
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Set status based on required jobs
+ env:
+ RESULTS: ${{ join(needs.*.result, ' ') }}
+ run: |
+ for result in $RESULTS; do
+ if [ "$result" != "success" ]; then
+ exit 1
+ fi
+ done
diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml
new file mode 100644
index 0000000..47afc47
--- /dev/null
+++ b/.github/workflows/validation.yml
@@ -0,0 +1,61 @@
+name: Validation
+
+on: workflow_call
+
+env:
+ PYTHON_VERSION: "3.14"
+ PRE_COMMIT_HOME: "/home/runner/.cache/pre-commit"
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup uv
+ uses: astral-sh/setup-uv@v5
+ with:
+ version: "latest"
+ python-version: ${{ env.PYTHON_VERSION }}
+ enable-cache: true
+ cache-suffix: "validation-py${{ env.python-version }}"
+
+ - name: Install dependencies
+ run: |
+ # We need the test group here to allow pyright to type-check code in tests
+ uv sync --no-default-groups --group lint --group test
+
+ - name: Get precommit version
+ id: precommit_version
+ run: |
+ PACKAGE_VERSION=$(pip show pre-commit | grep -i "version:" | awk '{print $2}')
+ echo "version=$PACKAGE_VERSION" >> $GITHUB_ENV
+
+ - name: Pre-commit Environment Caching
+ uses: actions/cache@v4
+ with:
+ path: ${{ env.PRE_COMMIT_HOME }}
+ key:
+ "precommit-${{ runner.os }}-${{ env.PYTHON_VERSION }}-${{ steps.precommit_version.outputs.version }}-\
+ ${{ hashFiles('./.pre-commit-config.yaml') }}"
+ # Restore keys allows us to perform a cache restore even if the full cache key wasn't matched.
+ # That way we still end up saving new cache, but we can still make use of the cache from previous
+ # version.
+ restore-keys: "precommit-${{ runner.os }}-${{ steps.poetry_setup.outputs-python-version}}-"
+
+ - name: Run pre-commit hooks
+ run: SKIP=ruff-linter,ruff-formatter,slotscheck,basedpyright pre-commit run --all-files
+
+ - name: Run ruff linter
+ run: ruff check --output-format=github --show-fixes --exit-non-zero-on-fix .
+
+ - name: Run ruff formatter
+ run: ruff format --diff .
+
+ - name: Run basedpyright type checker
+ run: basedpyright --warnings .
+
+ - name: Check UV Lockfile
+ run: uv lock --check
diff --git a/.gitignore b/.gitignore
index 7b59b3a..d5a2037 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,39 +1,45 @@
-# MacOS
-.DS_Store
+# Python byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+.pytest_cache/
+.mypy_cache/
+
+# Virtual environments
+.venv/
+.tox/
+venv/
+env/
+ENV/
+
+# Python packaging files
+dist/
-# Make
-custom.mk
+# Pytest coverage reports
+htmlcov/
+.coverage*
+coverage.xml
-# Python
-__pycache__
-*.pyc
+# Mkdocs documentation
+site/
-# Generated files
-*.egg-info
-build/
-dist/
-# ignore .eggs directory created by setup.py
-.eggs/
-.conda-built
-.coverage
-/htmlcov/
-/site/
-/pages-tmp/
-/public/
-/tests/.aws/
-conda-meta-data.json
-# stray build artifacts
-*.whl
-*.tar.bz2
-*.conda
-
-# tox
-/.tox/
-/tox-env.yml
-/tox-requirements.txt
-
-# git
-*.orig
+# Pyenv local version information
+.python-version
+
+# Editor generated files
+.idea/
+.vscode/
+.spyproject/
+.spyderproject/
+.replit
+# Auto-generated folder attributes for MacOS
+.DS_STORE
+# Local gitignore (symlinked to .git/info/exclude)
+.gitignore_local
+# Environmental, backup and personal files
+*.env
+*.bak
+TODO
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 787c8e3..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-*.xml
-
-!modules.xml
-!vcs.xml
diff --git a/.idea/garpy.mkdocstrings.iml b/.idea/garpy.mkdocstrings.iml
deleted file mode 100644
index 288b948..0000000
--- a/.idea/garpy.mkdocstrings.iml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/icon.svg b/.idea/icon.svg
deleted file mode 120000
index 61f0c4e..0000000
--- a/.idea/icon.svg
+++ /dev/null
@@ -1 +0,0 @@
-../docs/logo.svg
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
deleted file mode 100644
index e2a5a6f..0000000
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index 9b275bc..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..6e9a5bc
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,57 @@
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.3.0
+ hooks:
+ - id: check-merge-conflict
+ - id: check-toml # For pyproject.toml
+ - id: check-yaml # For workflows
+ # Only parse the files for syntax, don't do full load.
+ # We need this because of mkdocs.yml, which uses some custom tags to perform dynamic imports from python.
+ args: ["--unsafe"]
+ - id: end-of-file-fixer
+ - id: trailing-whitespace
+ args: [--markdown-linebreak-ext=md]
+ - id: mixed-line-ending
+ args: [--fix=lf]
+ - id: sort-simple-yaml
+
+ - repo: local
+ hooks:
+ - id: ruff-linter
+ name: Ruff Linter
+ description: Run ruff checks on the code
+ entry: ruff check --force-exclude
+ language: system
+ types: [python]
+ require_serial: true
+ args: [--fix, --exit-non-zero-on-fix]
+
+ - repo: local
+ hooks:
+ - id: ruff-formatter
+ name: Ruff Formatter
+ description: Ruf ruff auto-formatter
+ entry: ruff format
+ language: system
+ types: [python]
+ require_serial: true
+
+ - repo: local
+ hooks:
+ - id: basedpyright
+ name: Based Pyright
+ description: Run basedpyright type checker
+ entry: basedpyright --warnings
+ language: system
+ types: [python]
+ pass_filenames: false # pyright runs for the entire project, it can't run for single files
+
+ - repo: local
+ hooks:
+ - id: uv-lockfile
+ name: UV Lockfile
+ description: Check if the UV lockfile is up to date with pyproject.toml
+ entry: uv lock --check
+ language: system
+ files: '^pyproject\.toml$|^uv\.lock$'
+ pass_filenames: false
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3591c88..6584d34 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,20 +1,17 @@
-# mkdocstring-python-xref changes
+## [UNRELEASED] Version 0.1.0 (2025-02-09)
-## 1.6.2
+This is the initial release following a **rewrite of the project (fork)**.
-* Use griffe 1.0 or later
+### Breaking changes
-## 1.6.1
+- Handler name was renamed to `python_betterrefs` (from `python_xrefs`)
+- Config option `relative_crossrefs` was renamed to `better_crossrefs`
+- Config option `better_crossrefs` (previously `relative_crossrefs`) is now enabled by default
-* Available on conda-forge
-
-## 1.6.0
-
-* Added explicit option to disable cross-reference checking.
-* When enabled, check all cross-references, not just relative ones
-* If reference begins with '?', don't check cross-reference.
-
-## 1.5.3
-
-First public release
+### Other changes
+- Rewrite the project documentation
+- Move to `basedpyright` type-checker (from mypy)
+- Move to `uv` package manager (from condadev)
+- Improve CI workflows
+- Improve testing
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
deleted file mode 100644
index 5c9afba..0000000
--- a/CONTRIBUTING.md
+++ /dev/null
@@ -1,34 +0,0 @@
-# Contributing to mkdocstrings-python-xref
-
-## Prerequisites
-
-* conda must be installed on your machine
-* make should be installed in your base conda environment
-
-## Development install
-
-To (re)create a conda development environment for this project run:
-
-```
-make createdev
-conda activate mkxref-dev
-```
-
-After you have created the environment for the first time, you can configure your IDE
-to use that for this project.
-
-To update the environment after pulling or modifying project dependencies, you can use
-
-```
-make updatedev
-```
-
-This is just an optimization. If it does not work (e.g. can happen when switching to an old branch), just use `createdev`.
-
-## Versioning
-
-The versions will generally track the version of [mkdocstrings_python][] on which it depends.
-
-[mkdocstrings_python]: https://github.com/mkdocstrings/python
-
-
diff --git a/LICENSE.md b/LICENSE.txt
similarity index 100%
rename from LICENSE.md
rename to LICENSE.txt
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index 56ce4fc..0000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,5 +0,0 @@
-# Including requirements via manifest.in allows:
-# - tox to find the same requirements file.
-# TODO - removes this if TOX support is dropped or other way
-# to tell tox to include this
-include runtime-env.yml
diff --git a/Makefile b/Makefile
deleted file mode 100644
index be20f2c..0000000
--- a/Makefile
+++ /dev/null
@@ -1,244 +0,0 @@
-CONDA := conda
-ECHO := echo
-RM := rm
-RMDIR := $(RM) -rf
-CAT := cat
-TOUCH := touch
-
-# OS specific values
-ifndef OS
- # maxOs or linux
- USERNAME := $(shell whoami)
-else
- # windows
-endif
-
-COVERAGE_HTML := $(abspath htmlcov)
-
-# If you want to override default variables in your own source tree,
-# define a custom.mk file, for instance to use the name 'garconf-dev' for
-# the conda development environment, you could use the entry:
-#
-# override DEV_ENV := garconf-dev
--include custom.mk
-
-# General env/build/deploy args
-PACKAGE := mkdocstrings-python-xref
-PACKAGE_VERSION_PATH := src/mkdocstrings_handlers/python_xref/VERSION
-VERSION := $(strip $(file < $(PACKAGE_VERSION_PATH)))
-
-SRC_FILES := $(wildcard src/mkdocstrings_handlers/python_xref/*.py) $(PYTHON_VERSION_PATH)
-
-# Env names
-DEV_ENV := mkxref-dev
-
-# Whether to run targets in current env or explicitly in $(DEV_ENV)
-CURR_ENV_BASENAME := $(shell basename $(CONDA_PREFIX))
-ifeq ($(CURR_ENV_BASENAME), $(DEV_ENV))
- CONDA_RUN :=
-else
- CONDA_RUN := conda run -n $(DEV_ENV) --no-capture-output
-endif
-
-# Testing args
-PYTEST_ARGS :=
-TOX_ARGS :=
-PYLINT_ARGS :=
-MYPY_ARGS :=
-RUFF_ARGS :=
-
-DOC_DIR := docs
-MKDOC_CONFIG := mkdocs.yml
-MKDOC_FILES := $(MKDOC_CONFIG) $(wildcard $(DOC_DIR)/user/*.md)
-
-# 'make help' colors
-TOP_COLOR :=\033[0;34m
-SECTION_COLOR :=\033[0;32m
-COLORLESS :=\033[0m
-
-help:
- @$(ECHO)
- @$(ECHO) "$(TOP_COLOR)=== Make targets ===$(COLORLESS)"
- @$(ECHO) "$(SECTION_COLOR)--- conda environment ---$(COLORLESS)"
- @$(ECHO) "create-dev - Create conda development environment named '$(DEV_ENV)'."
- @$(ECHO) "update-dev - Update conda development environment '$(DEV_ENV)'."
- @$(ECHO) "clean-dev - Remove $(DEV_ENV) conda environment"
- @$(ECHO)
- @$(ECHO) "$(SECTION_COLOR)--- test ---$(COLORLESS)"
- @$(ECHO) "pytest - Run pytest in '$(DEV_ENV)' environment."
- @$(ECHO) "test - Run tests and linting in '$(DEV_ENV)' environment."
- @$(ECHO) "coverage-test - Runs pytest instrumented for coverage and generates html report"
- @$(ECHO) "coverage-show - Open html coverage report in a web browser."
- @$(ECHO)
- @$(ECHO) "$(SECTION_COLOR)--- lint ---$(COLORLESS)"
- @$(ECHO) "lint - Run linting commands in '$(DEV_ENV)' environment."
- @$(ECHO) "ruff - Run ruff in '$(DEV_ENV)' environment."
- @$(ECHO) "mypy - Run mypy in '$(DEV_ENV)' environment."
- @$(ECHO)
- @$(ECHO) "$(SECTION_COLOR)--- build ---$(COLORLESS)"
- @$(ECHO) "build - Build wheel"
- @$(ECHO) "build-wheel - Build wheel."
- @$(ECHO) "build-conda - Build conda package (requires whl2conda)"
- @$(ECHO)
- @$(ECHO) "$(SECTION_COLOR)--- upload ---$(COLORLESS)"
- @$(ECHO) "upload - Upload wheel to pypi (requires authorization)"
- @$(ECHO) "check-upload - Verify file for upload"
- @$(ECHO)
- @$(ECHO) "$(SECTION_COLOR)--- documentation ---$(COLORLESS)"
- @$(ECHO) "doc - Build HTML documentation in site/ directory."
- @$(ECHO) "doc-strict - Build documentation but fail if any warnings"
- @$(ECHO) "showdoc - Show HTML documentation using local HTML server."
- @$(ECHO)
- @$(ECHO) "$(SECTION_COLOR)--- cleanup ---$(COLORLESS)"
- @$(ECHO) "clean - Remove generated files."
- @$(ECHO) "clean-test - Remove generated test files including tox environments."
- @$(ECHO) "clean-doc - Remove generated documentation files."
- @$(ECHO)
- @$(ECHO) "$(TOP_COLOR)====================$(COLORLESS)"
- @$(ECHO)
-
-dev-install:
- $(CONDA_RUN) pip install -e . --no-deps --no-build-isolation
-
-create-dev:
- $(CONDA) env create -f environment.yml --yes
- $(MAKE) dev-install
-
-createdev: create-dev
-
-update-dev:
- $(CONDA) env update -f environment.yml
- $(MAKE) dev-install
-
-updatedev: update-dev
-
-create-ci-env: create-dev
-
-clean-dev:
- -$(CONDA) env remove -n $(DEV_ENV)
-
-pytest:
- $(CONDA_RUN) pytest -sv -ra $(PYTEST_ARGS) tests
-
-test: lint pytest
-
-test-all: test tox
-
-.coverage:
- @$(CONDA_RUN) pytest -ra $(PYTEST_ARGS) --cov --cov-report=html --cov-report=term -- tests
-
-coverage-test:
- @$(MAKE) -B .coverage
-
-coverage: coverage-test
-
-coverage-show:
- @$(CONDA_RUN) python -m webbrowser file://$(COVERAGE_HTML)/index.html
-
-pylint:
- $(CONDA_RUN) pylint src/mkdocstrings_handlers tests $(PYLINT_ARGS)
-
-ruff:
- $(CONDA_RUN) ruff check src/mkdocstrings_handlers tests $(RUFF_ARGS)
-
-mypy:
- $(CONDA_RUN) mypy $(MYPY_ARGS)
-
-lint: ruff mypy
-
-WHEEL_FILE := dist/$(subst -,_,$(PACKAGE))-$(VERSION)-py3-none-any.whl
-CONDA_FILE := dist/$(PACKAGE)-$(VERSION)-py_0.conda
-
-build-sdist:
- $(CONDA_RUN) python -m build --sdist --no-isolation --outdir dist
-
-$(WHEEL_FILE):
- $(CONDA_RUN) pip wheel . --no-deps --no-build-isolation -w dist
-
-build-wheel: $(WHEEL_FILE)
-
-$(CONDA_FILE): $(WHEEL_FILE)
- $(CONDA_RUN) whl2conda convert $(WHEEL_FILE)
-
-build-conda: $(CONDA_FILE)
-
-build: build-wheel build-sdist build-conda
-
-site/index.html: $(MKDOC_FILES) $(SRC_FILES)
- $(CONDA_RUN) mkdocs build -f $(MKDOC_CONFIG)
-
-site/.doc-strict: $(MKDOC_FILES) $(SRC_FILES)
- $(CONDA_RUN) mkdocs build -f $(MKDOC_CONFIG) --strict
- $(CONDA_RUN) linkchecker -f linkcheckerrc.ini site
- $(TOUCH) site/.doc-strict
-
-doc: site/index.html
-
-docs: doc
-
-doc-strict: site/.doc-strict
-
-showdoc: site/index.html
- $(CONDA_RUN) mkdocs serve -f $(MKDOC_CONFIG)
-
-showdocs: showdoc
-
-doc-deploy:
- $(CONDA_RUN) mike deploy -F $(MKDOC_CONFIG) -u $(VERSION) latest
- $(CONDA_RUN) mike set-default -F $(MKDOC_CONFIG) latest
-
-mike-deploy: doc-deploy
-mike-build: doc-deploy
-
-doc-push:
- git push origin gh-pages
-
-doc-upload: doc-push
-
-doc-serve-all:
- $(CONDA_RUN) mike serve -F $(MKDOC_CONFIG)
-
-mike-serve: doc-serve-all
-
-check-upload-wheel:
- $(CONDA_RUN) twine check dist/*.whl
-
-check-upload-sdist:
- $(CONDA_RUN) twine check dist/*.tar.gz
-
-check-upload: check-upload-sdist check-upload-wheel
-
-upload-wheel: check-upload-wheel
- # NOTE: --skip-existing doesn't seem to actually work
- $(CONDA_RUN) twine upload --skip-existing $(lastword $(sort $(wildcard dist/*.whl)))
-
-
-upload-sdist: check-upload-sdist
- # NOTE: --skip-existing doesn't seem to actually work
- $(CONDA_RUN) twine upload --skip-existing $(lastword $(sort $(wildcard dist/*.tar.gz)))
-
-upload: upload-sdist upload-wheel
-
-clean-build:
- -@$(RMDIR) build
- -@$(RMDIR) dist
-
-clean-doc:
- -@$(RMDIR) site
-
-clean-tox:
- -@$(RMDIR) .tox
- -@$(RM) tox-env.yml tox-requirements.txt
-
-clean-test: clean-coverage clean-tox
- -@$(RMDIR) .pytest_cache
- -@$(RMDIR) .mypy_cache
-
-clean-coverage:
- -@$(RMDIR) $(COVERAGE_HTML) .coverage .coverage.*
-
-clean-python:
- -@python -Bc "import pathlib; [p.unlink() for p in pathlib.Path('.').rglob('*.py[co]')]"
- -@python -Bc "import pathlib; [p.rmdir() for p in pathlib.Path('.').rglob('__pycache__')]"
-
-clean: clean-doc clean-python clean-test clean-build
diff --git a/NOTICE.md b/NOTICE.md
new file mode 100644
index 0000000..8387ee8
--- /dev/null
+++ b/NOTICE.md
@@ -0,0 +1,20 @@
+This project forks off of the original project:
+[mkdocstrings-python-xref](https://github.com/analog-garage/mkdocstrings-python-xref). This project was created by
+Christopher Barber and copyrighted by Analog Devices, Inc. (2022-2023)
+
+The original project was licensed under the Apache 2.0 License, which this fork also uses. According to the terms of
+the Apache 2.0 License, significant modifications made to the project are required to be listed here. Below are the key
+changes introduced in this fork:
+
+- **Documentation:** The documentation has been extensively modified and relicensed under a Creative Commons license
+ (see the `LICENSE.txt` file in the `docs/` directory for more information). The usage page retains some original
+ content but has been reorganized for better readability.
+- **User Experience:** This fork aims to make migration from the original project as seamless as possible. While the
+ user interface remains largely the same, the handler has been renamed from mkdocstrings-python-xref to
+ mkdocstrings-python-betterrefs, some config options were also renamed.
+- **Source Code:** Various changes have been made to the source code, with more ongoing. For a detailed list of changes,
+ please refer to the project's `CHANGELOG.md` file.
+
+The reason for this fork was mainly personal curiosity and the desire to heavily modify the packaging practices and the
+code base, but also to address some issues which the original maintainer took a while to get to. This project is
+essentially a direct continuation of the original project, bringing it up-to-date with latest mkdocs-python.
diff --git a/README.md b/README.md
index 9b89e93..dbf8f18 100644
--- a/README.md
+++ b/README.md
@@ -1,24 +1,36 @@
-# mkdocstrings-python-xref
+# mkdocstrings-python-betterrefs
-Python handler for [mkdocstrings] supporting relative cross-references.
+
+[](https://pypi.org/project/mkdocstrings-python-betterrefs/)
-[](https://pypi.org/project/mkdocstrings-python-xref/)
-[](https://anaconda.org/conda-forge/whl2conda)
-[](https://analog-garage.github.io/mkdocstrings-python-xref/)
-
-
-[](https://github.com/analog-garage/mkdocstrings-python-xref/actions/workflows/main.yml)
-
+[](https://github.com/ItsDrike/mkdocstrings-python-betterrefs/actions/workflows/validation.yml)
+[](https://github.com/ItsDrike/mkdocstrings-python-betterrefs/actions/workflows/unit-tests.yml)
-[mkdocstrings] is an awesome plugin for [MkDocs] that can generate Markdown API documentation
-from comments in code. The standard [python handler][mkdocstrings-python] allows you to
-create cross-reference links using the syntax `[
][]` where the path must
-either be the fully qualified name of the referent or is empty, in which case the path
-is taken from the title. This works well when the names are short, but can be burdensome
-in larger codebases with deeply nested package structures.
+
-This package extends [mkdocstrings-python] to support a relative cross-reference syntax,
-that allows you to write doc-strings with cross-references like:
+[](https://itsdrike.github.io/mkdocstrings-python-betterrefs)
+
+Python handler for [mkdocstrings] with improved handling for cross-references, including relative ones.
+
+[mkdocstrings] is an awesome plugin for [MkDocs] that can generate Markdown API documentation from comments in code. The
+standard [python handler][mkdocstrings-python] allows you to create cross-reference links using the syntax
+`[][]` where the path must either be the fully qualified name of the referent or is empty, in which case
+the path is taken from the title.
+
+[mkdocstrings-python] does already have support for cross-references, however, it is currently only available in the
+insiders edition, which is limited to their sponsors. Additionally, this implementation is fairly limited in comparison
+to what this project offers.
+
+> [!TIP]
+> For more information on the [mkdocstrings-python] official support of relative cross-references, check out the feature
+> request proposing them: [here][official-xrefs-issue], and the docs detailing the configuration option:
+> [here][official-xrefs-docs].
+>
+> It is expected that relative cross-references will make it into the open-source version once a funding goal of $2,000
+> is reached. You can see the current progress towards this goal [here][official-xrefs-funding-goal].
+
+This package extends [mkdocstrings-python] to support an improved cross-reference syntax, that allows you to write
+doc-strings with relative cross-references like:
```python
class MyClass:
@@ -27,19 +39,23 @@ class MyClass:
See [other_method][..] from [MyClass][(c)]
"""
```
+
rather than:
```python
class MyClass:
def this_method(self):
"""
- See [other_method][mypkg.mymod.MyClass.other_method]
+ See [other_method][mypkg.mymod.MyClass.other_method]
from [MyClass][mypkg.mymod.Myclass]
"""
```
-Another benefit of this extension is that it will report source locations for bad references
-so that errors are easier to find and fix. For example:
+Relative references are especially useful for larger codebases with deeply nested package structure, where writing out
+the absolute paths each time gets very burdensome.
+
+Another benefit of this extension is that it will report source locations for bad references so that errors are easier
+to find and fix. For example:
```bash
$ mkdocs build
@@ -49,8 +65,11 @@ WARNING - mkdocstrings_handlers: file:///home/jdoe/my-project/src/myproj/bar.py
Cannot load reference 'myproj.bar.bad'
```
-For further details, please see the [Documentation](https://analog-garage.github.io/mkdocstrings-python-xref/)
+For further details, please see the [Documentation](https://itsdrike.github.io/mkdocstrings-python-betterrefs)
[MkDocs]: https://mkdocs.readthedocs.io/
[mkdocstrings]: https://github.com/mkdocstrings/mkdocstrings
[mkdocstrings-python]: https://github.com/mkdocstrings/python
+[official-xrefs-issue]: https://github.com/mkdocstrings/python/issues/27
+[official-xrefs-docs]: https://mkdocstrings.github.io/python/usage/configuration/docstrings/?h=relative#relative_crossrefs
+[official-xrefs-funding-goal]: https://mkdocstrings.github.io/python/insiders/#funding
diff --git a/docs/LICENSE.md b/docs/LICENSE.md
new file mode 100644
index 0000000..4269472
--- /dev/null
+++ b/docs/LICENSE.md
@@ -0,0 +1,54 @@
+# Documentation License
+
+This documentation itself does NOT follow the primary project license!
+
+Instead, it follows a Creative Commons license: CC BY-NC-SA 4.0
+
+## Attribution
+
+If you need a copyright header for proper attribution, you can use:
+
+mkdocstrings-python-betterrefs documentation © 2025 by ItsDrike
+
+In HTML:
+
+```html
+mkdocstrings-python-betterrefs documentation
+© 2025 by ItsDrike
+```
+
+If you also need the license identifier, use the following:
+
+```html
+CC BY-NC-SA 4.0
+
+```
+
+## Notice
+
+This documentation is a modified version of the documentation from the original project ([mkdocstrings-python-xref]).
+The original documentation was not licensed separately from the project, which means it was following the Apache 2.0
+license.
+
+This documentation has been heavily modified in various ways, however, the usage page remains fairly similar to the
+original.
+
+See the `NOTICE.txt` file in the root directory of this project for further details.
+
+[mkdocstrings-python-xref]: https://github.com/analog-garage/mkdocstrings-python-xref
diff --git a/docs/changelog.md b/docs/changelog.md
deleted file mode 100644
index 786b75d..0000000
--- a/docs/changelog.md
+++ /dev/null
@@ -1 +0,0 @@
---8<-- "CHANGELOG.md"
diff --git a/docs/config.md b/docs/config.md
index ab5d5e2..6947b67 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -1,40 +1,41 @@
-Configuration is the same as with [mkdocstrings-python][] except
-that the handler name should be `python_xref` instead of `python`. Because
-this handler extends the standard [mkdocstrings-python][] handler, the same options are
+# Configuration
+
+Configuration is the same as with [mkdocstrings-python] except that the handler name should be `python_betterrefs`
+instead of `python`. Because this handler extends the standard mkdocstrings-python handler, the same options are
available.
Additional options are added by this extension. Currently, there are two:
-* **relative_crossrefs** - if set to true enables use of relative path syntax in
- cross-references.
-
-* **check_crossrefs** - enables early checking of all cross-references. Note that
- this option only takes affect if **relative_crossrefs** is also true. This option is
- true by default, so this option is used to disable checking. Checking can
- also be disabled on a per-case basis by prefixing the reference with '?', e.g.
- `[something][?dontcheckme]`.
+- **better_crossrefs** - If set to true enables use of better cross-reference syntax provided by this handler
+ extension (setting this to false would essentially mimic the `python` handler). This is enabled by default, so you
+ shouldn't need to specify it unless you want to disable this behavior.
+
+- **check_crossrefs** - Enables early checking of all cross-references. Note that this option only takes affect if
+ **better_crossrefs** is also true. This option is true by default, so you only need to specify it if you wish to
+ disable this checking. Checking can also be disabled on a per-case basis by prefixing a reference with '?', e.g.
+ `[something][?dontcheckme]`.
!!! Example "mkdocs.yml plugins specification using this handler"
-```yaml
-plugins:
-- search
-- mkdocstrings:
- default_handler: python_xref
- handlers:
- python_xref:
- import:
- - https://docs.python.org/3/objects.inv
- options:
- docstring_style: google
- docstring_options:
- ignore_init_summary: yes
- merge_init_into_class: yes
- relative_crossrefs: yes
- check_crossrefs: no
- separate_signature: yes
- show_source: no
- show_root_full_path: no
-```
+ ```yaml
+ plugins:
+ - search
+ - mkdocstrings:
+ default_handler: python_betterrefs
+ handlers:
+ python_betterrefs:
+ options:
+ docstring_style: google
+ docstring_options:
+ ignore_init_summary: true
+ merge_init_into_class: true
+ better_crossrefs: true
+ check_crossrefs: false
+ separate_signature: true
+ show_source: true
+ show_root_full_path: true
+ inventories:
+ - https://docs.python.org/3/objects.inv
+ ```
[mkdocstrings-python]: https://mkdocstrings.github.io/python/
diff --git a/docs/contributing/bugs-and-feature-reqs.md b/docs/contributing/bugs-and-feature-reqs.md
new file mode 100644
index 0000000..aaf0362
--- /dev/null
+++ b/docs/contributing/bugs-and-feature-reqs.md
@@ -0,0 +1,185 @@
+# Bug Reports & Feature Requests
+
+`mkdocstrings-python-betterrefs` is an actively maintained project, and we welcome contributions in the form of both bug
+reports and feature requests. This guide will help you understand how to effectively submit an issue, whether it's
+reporting a bug or proposing a new feature.
+
+## Before creating an issue
+
+Before opening a new issue with your bug report, please do the following things:
+
+### Upgrade to the latest version
+
+Chances are that the bug you discovered was already fixed in a subsequent version. Thus, before reporting an issue,
+ensure that you're using the latest version of `mkdocstrings-python-betterrefs`.
+
+!!! warning "Bug fixes are not backported"
+
+ Please understand that only bugs that occur in the latest version will be addressed. Also, to reduce duplicate
+ efforts, fixes cannot be backported to earlier versions, except as a hotfix to the latest version, diverging from
+ the not yet finished features, even if already in the `main` branch.
+
+ Due to the nature of our [versioning], that might mean that if you require an older version of `mkdocstrings`, or
+ other dependencies of this project, you might be stuck with an older, buggy version of this library.
+
+### Search for existing issues
+
+It's possible that the issue you're having was already reported. Please take some time and search the [existing
+issues][issue-tracker] in the GitHub repository for your problem. If you do find an existing issue that matches the
+problem you're having, simply leave a :thumbsup: reaction instead (avoid commenting "I have this issue too" or similar,
+as that ultimately just clutters the discussion in that issue, but if you do think that you have something meaningful to
+add, please do).
+
+!!! note
+
+ Make sure to also check the closed issues. By default, github issue search will start with: `is:issue is:open`,
+ remove the `is:open` part to search all issues, not just the opened ones. It's possible that we seen this issue
+ before, but closed the issue as something that we're unable to fix.
+
+In case you found a relevant issue, however, it has already been closed as implemented (not as declined / not planned),
+but the bug / proposed feature is still somehow relevant, don't be worried to drop a comment on this older issue, we
+will get notifications for those too. That said, if you think there is sufficient new context now, it might also make
+sense to open a new issue instead, but make sure to at least mention the old issue if you choose this route.
+
+## Creating a new issue
+
+At this point, when you still haven't found a solution to your problem, we encourage you to create an issue.
+
+We have some issue-templates ready, to make sure that you include all of the necessary things we need to know:
+
+- For a **bug report**, you can click [here][open-bug-issue].
+- For a **feature request**, you can instead click [here][open-feature-req-issue].
+
+If you prefer, you can also [open a blank issue][open-blank-issue]. This will allow you to avoid having to follow the
+issue templates above. This might be useful if your issue doesn't cleanly fit into either of these two, or if you prefer
+to use your own categories and structure for the issue. That said, make please still make sure to include all of the
+relevant details when you do so.
+
+## Writing good bug reports
+
+Generally, the GitHub issue template should guide you towards telling us everything that we need to know. However, for
+the best results, keep reading through this section. In here, we'll explain how a well formatted issue should look like
+in general and what it should contain.
+
+### Issue Title
+
+A good title is short and descriptive. It should be a one-sentence executive summary of the issue, so the impact and
+severity of the bug you want to report can be inferred right from the title.
+
+| | Example |
+| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
+| :material-check:{ style="color: #4DB6AC" } **Clear** | Check-crossrefs incorrectly reports stdlib modules as unknown. |
+| :material-close:{ style="color: #EF5350" } **Wordy** | When check-crossrefs is enabled, stdlib modules get reported as unknown, causing warnings. |
+| :material-close:{ style="color: #EF5350" } **Unclear** | Incorrect warnings |
+| :material-close:{ style="color: #EF5350" } **Non-english** | Check-crossrefs informa incorrectamente que los módulos stdlib son desconocidos |
+| :material-close:{ style="color: #EF5350" } **Useless** | Help |
+
+### Bug description
+
+Now, to the bug you want to report. Provide a clear, focused, specific and concise summary of the bug you encountered.
+Explain why you think this is a bug that should be reported to us. Adhere to the following principles:
+
+1. **Explain the what , not the how ** – don't explain [how to reproduce the bug](#reproduction) here,
+ we're getting there. Focus on articulating the problem and its impact as clearly as possible.
+2. **Keep it short and concise** - if the bug can be precisely explained in one or two sentences, perfect. Don't
+ inflate it - maintainers and future users will be grateful for having to read less.
+3. **Don't under-explain** - don't leave out important details just to keep things short. While keeping things short is
+ important, if something is relevant, mention it. It is more important for us to have enough information to be able
+ to understand the bug, even if it means slightly longer bug report.
+4. **One bug at a time** - if you encounter several unrelated bugs, please create separate issues for them. Don't
+ report them in the same issue, as this makes it difficult for others when they're searching for existing issues and
+ also for us, since we can't mark such an issue as complete if only one of the bugs was fixed.
+
+:material-run-fast: **Stretch goal** – if you found a workaround or a way to fix the bug, you can add it as a comment on
+the issue, to help other users temporarily mitigate the problem before we can fix the bug in our code base.
+
+### Reproduction
+
+A minimal reproducible example is at the heart of every well-written bug report, as it allows us maintainers to
+instantly recreate the necessary conditions to inspect the bug and quickly find its root cause from there. It's a
+proven fact that issues with concise and small reproductions can be fixed much faster.
+
+Focus on keeping your example minimal, ideally, test out the bug in a brand new temporary project, with as little
+dependencies as possible (ideally just this library). Use a minimal `mkdocs.yml` configuration, with only what's
+necessary to demonstrate the bug. In most cases, it's enough to submit 3 files: `mkdocs.yml`, a single `docs.md` and a
+single `source.py`.
+
+Once you have your reproducible example ready, just give us these files as code snippets. Alternatively, you can also
+link us to a github gist, or something similar.
+
+??? tip "How to include code-snippets (markdown)"
+
+ In case you're not yet familiar with the syntax, GitHub issues use `markdown` format, which means you can use some
+ nice custom formatting to make the text appear distinct. One of these formatting options is a source-code block /
+ code snippet. To include one, you will want to use the following syntax:
+
+ ````markdown
+ ```language
+ your code
+ it can be multiline
+ ```
+ ````
+
+ Note that the symbols used here aren't single quotes (`'`), they're backticks: `` ` ``.
+ On an english keyboard, you can type these using the key right below escape (also used for tildes: `~`).
+
+ The `language` controls how the code will be highlighted. For python, you can use `python`, for yaml, `yaml`, etc.
+
+Sometimes, the bug can't be described in terms of code snippets, such as when reporting a mistake in the documentation.
+In that case, provide a link to the documentation or whatever other relevant things that will allows us to see the bug
+with minimal effort. In certain cases, it might even be fine to leave the reproduction steps section empty.
+
+## Next steps
+
+Once the issue is submitted, you have 2 options:
+
+### Wait for us to address it
+
+We will try to review your issue as soon as possible. Please be patient though, as this is an open-source project
+maintained by volunteers, who work on it simply for the fun of it. This means that we may sometimes have other
+priorities in life or we just want to work on some more interesting tasks first. It might therefore take a while for us
+to get to your issue, but we try and do our best to respond reasonably quickly, when we can. Even when things are
+slower, we kindly ask you to avoid posting comments like "Any progress on this?" as they are not helpful and only create
+unnecessary clutter in the discussion.
+
+When we do address your issue, we might need further information from you. GitHub has a notification system, so once we
+respond, you will be notified there. Note that, by default, these notifications might not be forwarded to your email or
+elsewhere, so please check GitHub periodically.
+
+Finally, when we address your issue, we will mark the issue as closed (GitHub will notify you of this too). Once that
+happens, your bug should be fixed / feature implemented, but we appreciate it if you take the time to verify that
+everything is working correctly. If something is still wrong, you can reopen the issue and let us know.
+
+!!! warning "Issues are fixed on the main branch"
+
+ Do note that when we close an issue, it means that we have fixed your issue in the `main` branch of the repository.
+ That doesn't necessarily mean the fix has been released on PyPI yet, so you might still need to wait a while unil
+ the next release is out. Alternatively, you can also try the [git installation][git-installation] to get the
+ project right from that latest `main` branch.
+
+### Attempt to solve it yourself
+
+!!! quote
+
+ The fastest way to get something done is to avoid waiting on others.
+
+If you wish to try and tackle the bug yourself, let us know by commenting on the issue with something like "I'd like to
+work on this". This helps us avoid duplicate efforts and ensures that we don't work on something you're already
+addressing.
+
+Once a maintainer sees your comment, they will assign the issue to you. Being assigned is a soft approval from us,
+giving you the green light to start working.
+
+Of course, you are welcome to start working on the issue even before being officially assigned. However, please be
+aware that sometimes we choose not to fix certain bugs for specific reasons. In such cases, your work might not end up
+being used.
+
+Before starting your work though, make sure to also read our [pull request guide][pr-guide].
+
+[versioning]: ./versioning.md
+[issue-tracker]: https://github.com/ItsDrike/mkdocstrings-python-betterrefs/issues
+[open-bug-issue]: https://github.com/ItsDrike/mkdocstrings-python-betterrefs/issues/new?labels=bug&template=bug-report.yml
+[open-feature-req-issue]: https://github.com/ItsDrike/mkdocstrings-python-betterrefs/issues/new?labels=feature&template=feature-request.yml
+[open-blank-issue]: https://github.com/ItsDrike/mkdocstrings-python-betterrefs/issues/new?template=Blank+issue
+[git-installation]: ../install.md#latest-git-version
+[pr-guide]: ./making-a-pr.md
diff --git a/docs/contributing/making-a-pr.md b/docs/contributing/making-a-pr.md
new file mode 100644
index 0000000..6d36f1d
--- /dev/null
+++ b/docs/contributing/making-a-pr.md
@@ -0,0 +1,5 @@
+# Making a PR
+
+!!! bug "Work In Progress"
+
+ This page is still being written.
diff --git a/docs/index.md b/docs/index.md
index eb03413..1704308 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,129 +1,45 @@
-## Relative cross-references
+---
+hide:
+ - navigation
+---
-By default, [mkdocstrings] only supports cross-references where the path is
-fully qualified or is empty, in which case it is taken from the title.
-If you work with long package and class names or with namespace packages, this can result in a lot
-of extra typing and harder to read doc-strings.
+# mkdocstrings-python-betterrefs
-If you enable the `relative_crossrefs` option in the `python_xref` handler options
-in your mkdocs.yml file ([see configuration](config.md) for an example), then the handler
-will support more compact relative syntax:
+Python handler for [mkdocstrings] with improved handling for cross-references, including relative ones.
-=== "Absolute"
+[mkdocstrings] is an awesome plugin for [MkDocs] that can generate Markdown API documentation from comments in code. The
+standard [python handler (mkdomkdocstrings-python)][mkdocstrings-python] allows you to create cross-reference links
+using the syntax `[][]` where the path must either be the fully qualified name of the referent or is empty,
+in which case the path is taken from the title.
- ```python
- class MyClass:
- def this_method(self):
- """
- See [other_method][mypkg.mymod.MyClass.other_method]
- from [MyClass][mypkg.mymod.Myclass]
- """
- ```
+mkdocstrings-python does already have support for cross-references, however, it is currently only available in the
+insiders edition, which is limited to their sponsors. Additionally, this implementation is fairly limited in comparison
+to what this project offers.
-=== "Relative"
+!!! tip
- ```python
- class MyClass:
- def this_method(self):
- """
- See [other_method][..] from [MyClass][(c)]
- """
- ```
+ For more information on the mkdocstrings-python official support of relative cross-references, check out the
+ feature request proposing them: [here][official-xrefs-issue], and the docs detailing the configuration option:
+ [here][official-xrefs-docs].
-The relative path specifier has the following form:
+ Even though the issue proposed the syntax similar to that used by this handler, the official relative crossrefs
+ support ended up being a very limited version of it.
-* If the path ends in `.` then the title text will be appended to the path
- (ignoring bold, italic or code markup).
+ It is expected that relative cross-references will make it into the open-source version once a funding goal of
+ $2,000 is reached. You can see the current progress towards this goal [here][official-xrefs-funding-goal].
-* If the path begins with a single `.` then it will be expanded relative to the path
- of the doc-string in which it occurs.
-
-* If the path begins with `(c)`, that will be replaced by the path of the
- class that contains the doc-string
-
-* If the path begins with `(m)`, that will be replaced by the path of the
- module that contains the doc-string
-
-* If the path begins with `(p)`, that will be replaced by the path of the
- package that contains the doc-string. If there is only one module in the
- system it will be treated as a package.
-
-* If the path begins with one or more `^` characters, then will be replaced
- by the path of the parent element. For example, when used in a doc-string
- for a method, `^` would get replaced with the class and `^^` would get
- replaced with the module.
-
-* Similarly, if the path begins with two or more `.` characters, then all but
- the last `.` will be replaced by the parent element, and if nothing follows
- the last `.`, the title text will be appended according to the first rule.
-
- *NOTE: When using either `^` or `..` we have found that going up more than one
- or two levels makes cross-references difficult to read and should be avoided*
-
-These are demonstrated here:
-
-=== "Relative"
-
- ```python
- class MyClass:
- def this_method(self):
- """
- [MyClass][^]
- Also [MyClass][(c)]
- [`that_method`][^.]
- Also [`that_method`][..]
- [init method][(c).__init__]
- [this module][(m)]
- [this package][(p)]
- [OtherClass][(m).]
- [some_func][^^.] or [some_func][...]
- """
- ```
-
-=== "Absolute"
-
- ```python
- class MyClass:
- def this_method(self):
- """
- [MyClass][mypkg.mymod.MyClass]
- Also [MyClass][mypkg.mymod.MyClass]
- [`that_method`][mypkg.mymod.MyClass.that_method]
- Also [`that_method`][mypkg.mymod.MyClass.that_method]
- [init method][mypkg.mymod.MyClass.__init__]
- [this module][mypkg.mymod]
- [this package][mypkg]
- [OtherClass][mypkg.mymod.OtherClass]
- [some_func][mypkg.mymod.some_func]
- """
- ```
-
-This has been [proposed as a feature in the standard python handler][relative-crossref-issue]
-but has not yet been accepted.
-
-## Cross-reference checking
-
-If `relative_crossrefs` and `check_crossrefs` are both enabled (the latter is true by default),
-then all cross-reference expressions will be checked to ensure that they exist and failures
-will be reported with the source location. Otherwise, missing cross-references will be reported
-by mkdocstrings without the source location, in which case it is often difficult to locate the source
-of the error. Note that the errors generatoed by this feat[.gitignore](..%2F.gitignore)
-
-
-
-ure are in addition to the errors
-from mkdocstrings.
-
-The current implementation of this feature can produce false errors for definitions from the
-python standard library. You can disable the check on a case-by-case basis by prefixing the
-reference expression with a `?`, for example:
-
-```
-This function returns a [Path][?pathlib.] instance.
-```
-
-[mkdocstrings]: https://mkdocstrings.github.io/
-[mkdocstrings_python]: https://mkdocstrings.github.io/python/
-[relative-crossref-issue]: https://github.com/mkdocstrings/python/issues/27
+This package extends [mkdocstrings-python] to support an improved cross-reference syntax, that allows you to write
+your doc-strings with these nicer cross-references. The primary goal is making cross-references shorter and less
+repetitive.
+Do note that this project is a fork of the original [mkdocstrings-python-xref]. For more info, see our [fork
+notice][fork-notice] section
+[MkDocs]: https://mkdocs.readthedocs.io/
+[mkdocstrings]: https://github.com/mkdocstrings/mkdocstrings
+[mkdocstrings-python]: https://github.com/mkdocstrings/python
+[official-xrefs-issue]: https://github.com/mkdocstrings/python/issues/27
+[official-xrefs-docs]: https://mkdocstrings.github.io/python/usage/configuration/docstrings/?h=relative#relative_crossrefs
+[official-xrefs-funding-goal]: https://mkdocstrings.github.io/python/insiders/#funding
+[mkdocstrings-python-xref]: https://github.com/analog-garage/mkdocstrings-python-xref
+[fork-notice]: ./meta/fork.md
diff --git a/docs/install.md b/docs/install.md
index fafd479..a80abc5 100644
--- a/docs/install.md
+++ b/docs/install.md
@@ -1,10 +1,149 @@
-### Using pip
+# Installation
-```
-pip install mkdocstrings-python-xref
-```
-### Using conda
+!!! note "Installing alongside `mkdocstrings-python`"
-```
-conda install -c conda-forge mkdocstrings-python-xref
-```
+ You don't need to explicitly specify [`mkdocstrings-python`][mkdocstrings-python] as a dependency, as this package
+ already lists it as it's internal dependency, which means installing `mkdocstrings-python-betterrefs` will also
+ install `mkdocstrings-python` for you.
+
+## PyPI (stable) version
+
+`mkdocstrings-python-betterrefs` is available on [PyPI] and can be installed like any other python library with:
+
+=== ":simple-python: pip"
+
+ ```bash
+ pip install mkdocstrings-python-betterrefs
+ ```
+
+
+
+ [pip](https://pip.pypa.io/en/stable/) is the main package installer for Python.
+
+
+
+=== ":simple-poetry: poetry"
+
+ ```bash
+ poetry add mkdocstrings-python-betterrefs
+ ```
+
+
+
+ [Poetry](https://python-poetry.org/) is an all-in-one solution for Python project management.
+
+
+
+=== ":simple-rye: rye"
+
+ ```bash
+ rye add mkdocstrings-python-betterrefs
+ ```
+
+
+
+ [Rye](https://rye.astral.sh/) is an all-in-one solution for Python project management, written in Rust.
+
+
+
+=== ":simple-ruff: uv"
+
+ ```bash
+ uv pip install mkdocstrings-python-betterrefs
+ ```
+
+
+
+ [uv](https://github.com/astral-sh/uv) is an ultra fast dependency resolver and package installer, written in Rust.
+
+
+
+=== ":simple-pdm: pdm"
+
+ ```bash
+ pdm add mkdocstrings-python-betterrefs
+ ```
+
+
+
+ [PDM](https://pdm-project.org/en/latest/) is an all-in-one solution for Python project management.
+
+
+
+## Latest (git) version
+
+!!! warning "We don't guarantee stability with method of installing"
+
+If you wish to install the latest available version (the one you currently see in the `main` git branch), you may
+instead choose this method of installing.
+
+This kind of installation should only be done if you wish to test some new unreleased features and it's likely that you
+will encounter bugs.
+
+That said, this library is still in development, and there may be some features that you might wish to try, even
+though they're not yet available in the latest release. This method of installation allows you to do just that.
+
+To install the latest version of `mkdocstrings-python-betterrefs` directly from the `main` git branch, use:
+
+=== ":simple-python: pip"
+
+ ```bash
+ pip install 'mkdocstrings-python-betterrefs@git+https://github.com/ItsDrike/mkdocstrings-python-betterrefs@main'
+ ```
+
+
+
+ [pip](https://pip.pypa.io/en/stable/) is the main package installer for Python.
+
+
+
+=== ":simple-poetry: poetry"
+
+ ```bash
+ poetry add 'git+https://github.com/ItsDrike/mkdocstrings-python-betterrefs#main'
+ ```
+
+
+
+ [Poetry](https://python-poetry.org/) is an all-in-one solution for Python project management.
+
+
+
+=== ":simple-rye: rye"
+
+ ```bash
+ rye add mkdocstrings-python-betterrefs --git='https://github.com/ItsDrike/mkdocstrings-python-betterrefs' --branch main
+ ```
+
+
+
+ [Rye](https://rye.astral.sh/) is an all-in-one solution for Python project management, written in Rust.
+
+
+
+=== ":simple-ruff: uv"
+
+ ```bash
+ uv pip install 'mkdocstrings-python-betterrefs@git+https://github.com/ItsDrike/mkdocstrings-python-betterrefs@main'
+ ```
+
+
+
+ [uv](https://github.com/astral-sh/uv) is an ultra fast dependency resolver and package installer, written in Rust.
+
+
+
+=== ":simple-pdm: pdm"
+
+ ```bash
+ pdm add "git+https://github.com/ItsDrike/mkdocstrings-python-betterrefs@main"
+ ```
+
+
+
+ [PDM](https://pdm-project.org/en/latest/) is an all-in-one solution for Python project management.
+
+
+
+[PyPI]: https://pypi.org/project/mkdocstrings-python-betterrefs
+[mkdocstrings-python]: https://github.com/mkdocstrings/python
diff --git a/docs/license.md b/docs/license.md
deleted file mode 100644
index af216c6..0000000
--- a/docs/license.md
+++ /dev/null
@@ -1 +0,0 @@
---8<-- "LICENSE.md"
diff --git a/docs/meta/changelog.md b/docs/meta/changelog.md
new file mode 100644
index 0000000..af66744
--- /dev/null
+++ b/docs/meta/changelog.md
@@ -0,0 +1,7 @@
+# Changelog
+
+!!! danger ""
+
+ Major and minor releases include the changes specified in prior development releases.
+
+--8<-- "CHANGELOG.md"
diff --git a/docs/meta/fork.md b/docs/meta/fork.md
new file mode 100644
index 0000000..8b81b93
--- /dev/null
+++ b/docs/meta/fork.md
@@ -0,0 +1,48 @@
+# Fork notice
+
+This project is a fork of the excellent [mkdocstrings-python-xref] project. The primary reason for forking was personal
+curiosity with how this project worked, though another reason was also that the original project was somewhat slow at
+addressing some issues and used fairly uncommon packaging practices when it comes to modern python, which IMO made it
+harder to contribute to.
+
+At its core, this fork retains the original functionality while addressing compatibility issues that arose as its
+dependencies (namely: mkdocstrings, mkdocstrings-python, mkdocstrings-autorefs, and griffe) were updated.
+
+In addition, significant improvements have been made to the codebase, including cleanup and updates to follow modern
+packaging practices. For example, this project moved away from Conda in favor of [uv].
+
+We’ve also placed a greater emphasis on properly managing project dependencies. Stricter version requirements have been
+applied to ensure stability, meaning that new versions of dependencies will only be supported once they’ve been properly
+tested. The goal is to automate this process with GitHub workflows that will periodically check for new versions, run
+tests, and publish a new PyPI release if all tests pass. This is particularly important given this library's reliance on
+internal features of [mkdocstrings-python], which means breakages are common when dependencies are updated.
+
+It's important to note that this is a "hard fork," meaning future updates to the original mkdocstrings-python-xref will
+not necessarily be merged back here. This is mainly because the code-base of this project has become sufficiently
+different to make that task pretty hard. However, if a relevant feature from the original project is introduced, we may
+consider porting it to this fork. That said, considering this project haven't released a new feature in quite a while
+now, this likely won't be a concern.
+
+!!! note
+
+ Due to technical reasons, this project is not marked as a "fork" on GitHub, even though it is one. Forked repositories
+ come with limitations, such as disappearing if the original repo is made private, or creating confusion by suggesting
+ this project is still work-in-progress aimed at contributing back to the original. Using a standalone repository helps
+ avoid these issues and makes it clear that this is an independent continuation of the original project.
+
+## Acknowledgements
+
+This project would not exist without the original mkdocstrings-python-xref project, created by Christopher Barber and
+the Analog Devices, Inc. We owe a huge thanks to them for their work, which laid the foundation for this fork and also
+suggested the original idea of the improved syntax for cross-references.
+
+## Legal notice
+
+The purpose of this page is to acknowledge the original author and clarify the reasons behind this fork, as well as the
+changes that have been made since. For the legal information required for derivative works under the Apache 2.0 license,
+please refer to the [license page][license-page] instead.
+
+[mkdocstrings-python-xref]: https://github.com/analog-garage/mkdocstrings-python-xref
+[mkdocstrings-python]: https://github.com/mkdocstrings/python
+[uv]: https://docs.astral.sh/uv
+[license-page]: ./license.md
diff --git a/docs/meta/license.md b/docs/meta/license.md
new file mode 100644
index 0000000..46f2a70
--- /dev/null
+++ b/docs/meta/license.md
@@ -0,0 +1,85 @@
+# License
+
+## Source Code
+
+This project's source code is licensed under the **Apache 2.0** license.
+
+This license allows you to use `mkdocstrings-python-betterrefs` in any project, regardless of it's license (including a
+proprietary one). You can change the code, distribute it or even use it for commercial purposes. That said, if you wish
+to do either of these, you are required to include a copyright notice, the license fulltext and state any significant
+changes made to the software; include a `NOTICE` file with the attribution notes.
+
+!!! tip
+
+ If you want to see a quick glance of what this license allows, prohibits & requires, check it out in [tl;dr
+ legal][tldr-apache2].
+
+??? example "Full LICENSE text"
+
+ ```title="LICENSE.txt"
+ --8<-- "LICENSE.txt"
+ ```
+
+!!! note
+
+ If you need a copyright header for attribution, you can use:
+
+ === "Rendered"
+
+ mkdocstrings-python-betterrefs
+
+ Copyright © 2022-2023 Analog Devices, Inc.
+ Copyright © 2025 ItsDrike <itsdrike@protonmail.com>
+
+ === "HTML"
+
+ ```html
+ mkdocstrings-python-betterrefs
+
+ Copyright © 2022-2023 Analog Devices, Inc.
+ Copyright © 2025 ItsDrike <itsdrike@protonmail.com>
+ ```
+
+## This documentation
+
+This documentation itself follows a Creative Commons license: CC BY-NC-SA 4.0
+
+!!! note
+
+ If you need a copyright header for proper attribution, you can use:
+
+ === "Rendered"
+
+ mkdocstrings-python-betterrefs documentation © 2025 by ItsDrike
+
+ If you also need the license identifier, use the following:
+
+ CC BY-NC-SA 4.0
+
+ === "HTML"
+
+ ```html
+ mkdocstrings-python-betterrefs documentation © 2025 by ItsDrike
+ ```
+
+ If you also need the license identifier, use the following:
+
+ ```html
+ CC BY-NC-SA 4.0
+ ```
+
+## Fork NOTICE
+
+Note that this project itself is a fork of [`mkdocstrings-python-xref`][mkdocstrings-python-xref] and it is distributed
+under the same (Apache 2.0) license (fulltext of this license was already shown above). As such, it is required to
+include a `NOTICE` file detailing the changes made in this fork. The full text of the `NOTICE` file, which is included
+below, lists these modifications.
+
+??? example "Full NOTICE text"
+
+ ```markdown title="NOTICE.md"
+ --8<-- "NOTICE.md"
+ ```
+
+[mkdocstrings-python-xref]: https://github.com/analog-garage/mkdocstrings-python-xref
+[tldr-apache2]: https://www.tldrlegal.com/license/apache-license-2-0-apache-2-0
diff --git a/docs/meta/support.md b/docs/meta/support.md
new file mode 100644
index 0000000..95c3047
--- /dev/null
+++ b/docs/meta/support.md
@@ -0,0 +1,10 @@
+# Support
+
+- If you found a bug, or wish to propose a new feature, please follow this [guide][bugs-and-feature-reqs-guide].
+- If you just want to ask a question, feel free to do so on the [project's discussions board][discussions].
+- In case you have a security concern, or some other problem that requires private resolution, you can [send me an
+ email][email] (`itsdrike@protonmail.com`).
+
+[bugs-and-feature-reqs-guide]: ../contributing/bugs-and-feature-reqs.md
+[discussions]: https://github.com/ItsDrike/mkdocstrings-python-betterrefs/discussions
+[email]: mailto:itsdrike@protonmail.com
diff --git a/docs/meta/versioning.md b/docs/meta/versioning.md
new file mode 100644
index 0000000..8592d41
--- /dev/null
+++ b/docs/meta/versioning.md
@@ -0,0 +1,55 @@
+# Versioning
+
+!!! danger "Pre-release phase"
+
+ `mkdocstrings-python-betterrefs` is currently in the pre-release phase (pre v1.0.0). During this phase, these
+ guarantees will NOT be followed! This means that **breaking changes can occur in minor version bumps**. That said,
+ micro version bumps are still strictly for bugfixes, and will not include any features or breaking changes.
+
+This library follows [semantic versioning model][semver], which means the major version is updated every time
+there is an incompatible (breaking) change made to the public API.
+
+In our case, the public API refers to the cross-reference syntax. A major version bump would therefore mean a
+potentially breaking update, requiring you to modify the cross references to be compatible with the new version.
+
+## Examples of Breaking level change (major bump: `vX.0.0`)
+
+We try to avoid breaking changes as much as we can, but it might sometimes be beneficial, especially if it's to resolve
+a problem, that people might technically be relying on.
+
+Here are some examples of what constitutes a breaking change:
+
+- Dropping support for the `(m)` syntax, resolving to the current module
+- Changing the behavior of a trailing `.` (previously just appending the title text) to now first clean up any markup
+ from the title (like bold/italic or code markup)
+- Using stricter validation of cross-references (breaking, as new warnings will be produced on xrefs that were
+ considered as valid before and didn't produce warnings)
+- Dropping support for `?` prefixing references to avoid cross-ref validation, as it's no longer necessary (validation
+ now always works, ignoring it should never be needed)
+- Removing or renaming a [configuration option][config-option] for the handler (like `check_crossrefs`)
+- Changing the default value for a configuration option
+
+## Examples of a Feature level Change (minor bump: `v1.X.0`)
+
+- Dropping support of an old `mkdocstrings-python` version
+- Introducing support of a new `mkdocstrings-python` version
+- Introducing new cross-reference syntax that doesn't interfere with the existing one
+- Adding a configuration option for the handler (like `check_crossrefs`), assuming it doesn't affect the default
+ behavior in a breaking way, at least not by default.
+- Adding support for validating more cross-references (not breaking, as people will have these marked as ignored, this
+ just allows to no longer ignore these xrefs from validation)
+
+## Examples of a Patch level change (patch bump: `v1.0.X`)
+
+- Adding unit-tests
+- Adding CI workflows
+- Changing the documentation
+- **Changing the internal code in any way**
+
+Relying on the handler class, or any other code components from this library is not supported. **All code for this
+library is considered a part of the private API** and is subject to any changes without backwards compatibility in mind.
+
+We usually make these updates with the goal of improving the code readability or efficiency.
+
+[semver]: https://semver.org
+[config-option]: ../config.md
diff --git a/docs/support.md b/docs/support.md
deleted file mode 100644
index 3d9f5e6..0000000
--- a/docs/support.md
+++ /dev/null
@@ -1,4 +0,0 @@
-You can file bug reports or feature requests
-[on GitHub](https://github.com/analog-garage/mkdocstrings-python-xref/issues) or if you just want to ask
-a question on the [project's discussions board](https://github.com/analog-garage/mkdocstrings-python-xref/discussions)
-
diff --git a/docs/usage.md b/docs/usage.md
new file mode 100644
index 0000000..032110c
--- /dev/null
+++ b/docs/usage.md
@@ -0,0 +1,159 @@
+---
+hide:
+ - navigation
+---
+
+# Usage
+
+## Relative cross-references
+
+As already mentioned before, `mkdocstrings-python-betterrefs` allows you to use a custom improved syntax when specifying
+your cross-references, to make your references shorter and easier to comprehend. The most important feature is the
+relative cross-references support. Check the example below:
+
+=== "Absolute"
+
+ ```python title="mypkg/mymod.py"
+ class OtherClass: ...
+
+ class MyClass:
+ def other_method(self): ...
+
+ def this_method(self):
+ """
+ See [other_method][mypkg.mymod.MyClass.other_method] and
+ the [OtherClass][mypkg.mymod.OtherClass].
+ """
+ ```
+
+=== "Relative"
+
+ ```python title="mypkg/mymod.py"
+ class OtherClass: ...
+
+ class MyClass:
+ def other_method(self): ...
+
+ def this_method(self):
+ """
+ See [other_method][..other_method] and
+ the [OtherClass][(m).OtherClass]
+ """
+ ```
+
+The relative path specifier works as follows:
+
+
+
+- `(c)`: Replaced by the path of the **class** that contains the docstring.
+- `(m)`: Replaced by the path of the **module** that contains the docstring.
+- `(p)`: Replaced by the path of the **package** that contains the docstring. (If this is a stand-alone module, this
+module will be treated as a package.)
+
+- **One or more `.` characters:** Expanded to the path of the current docstring (or its parent elements).
+
+ For example, in a method's docstring:
+
+ - `.` is replaced by the method name,
+ - `..` is replaced by the class name, and
+ - `...` is replaced by the module name.
+
+- **One or more `^` characters:** Replaced by the path of the parent element. This is a shorthand for .., which is
+ commonly used.
+
+ For instance, in a method's docstring:
+
+ - `^` is replaced by the class name, and
+ - `^^` is replaced by the module name.
+
+
+
+!!! note
+
+ When using either `^` or `..` we have found that going up more than one or two levels makes cross-references
+ difficult to read and should be avoided
+
+## Avoiding repetition
+
+In addition to relative reference support, there's special handling to reduce repetitive reference declarations in the
+title and the cross-reference target. For instance, instead of writing `[MyClass][..MyClass]`, you can simply write
+`[MyClass][..]`, resulting in a much cleaner and more compact syntax.
+
+This rule applies when the cross-reference path ends with a period (`.`); in such cases, the title text is automatically
+appended to the path (ignoring any bold, italic, or code markup).
+
+## Demonstration
+
+Quick demonstration:
+
+=== "mkdocstrings-python-betterrefs"
+
+ ```python title="this_package/this_module.py"
+ def some_func(): ...
+
+ class MyClass:
+ def that_method(self): ...
+
+ def this_method(self):
+ """
+ [MyClass][^]
+ Also [MyClass][(c)]
+ And [`MyClass`][(m).] yet again
+ [`that_method`][^.]
+ Also [`that_method`][..]
+ [init method][(c).__init__]
+ [this module][(m)]
+ [this package][(p)]
+ [that module][(p).that_module] or [that module][(p).]
+ [OtherClass][(m).]
+ [some_func][^^.] or [some_func][...]
+ """
+ ```
+
+=== "mkdocstrings-python"
+
+ ```python title="this_package/this_module.py"
+ def some_func(): ...
+
+ class MyClass:
+ def that_method(self): ...
+
+ def this_method(self):
+ """
+ [MyClass][this_package.this_module.MyClass]
+ Also [MyClass][this_package.this_module.MyClass]
+ And [`MyClass`][this_package.this_module.MyClass] yet again
+ [`that_method`][this_package.this_module.MyClass.that_method]
+ Also [`that_method`][this_package.this_module.MyClass.that_method]
+ [init method][this_package.this_module.MyClass.__init__]
+ [this module][this_package.this_module]
+ [this package][this_package]
+ [that module][this_package.that_module] or [that module][this_package.that_module]
+ [OtherClass][this_package.this_module.OtherClass]
+ [some_func][this_package.this_module.some_func] or [some_func][this_package.this_module]
+ """
+ ```
+
+## Cross-reference checking
+
+If `check_crossrefs` is enabled (default), then all cross-reference expressions will be validated to ensure that they
+exist. Failures will be reported with the source location information.
+
+If disabled, missing cross-references will still be reported by `mkdocstrings` directly, but these reports lack source
+location details, which can make it challenging to locate the problematic docstring.
+
+Note that the errors generated by this feature are in addition to the errors from `mkdocstrings`, which will mean you
+will see 2 errors for each invalid reference.
+
+!!! warning
+
+ The current implementation of this feature can produce false errors for definitions from the python standard
+ library, or external imported libraries. You can disable the check on a case-by-case basis by prefixing the
+ reference expression with a `?`, for example:
+
+ ```python
+ def foo() -> pathlib.Path:
+ """
+ This function returns a [Path][?pathlib.] instance.
+ """
+ ```
diff --git a/environment.yml b/environment.yml
deleted file mode 100644
index 437c633..0000000
--- a/environment.yml
+++ /dev/null
@@ -1,26 +0,0 @@
-name: mkxref-dev
-channels:
- - conda-forge
-
-dependencies:
- # runtime
- - python >=3.8,<3.13
- - mkdocstrings-python >=1.6.2,<2.0
- - griffe >=1.0
- # build
- - python-build >=1.0.0
- - hatchling >=1.21
- # test
- - coverage >=7.4.0
- - pytest >=8.2
- - pytest-cov >=5.0
- - pylint >=3.0.3
- - mypy >=1.10
- - ruff >=0.4.10
- - beautifulsoup4 >=4.12
- # documentation
- - black >=23.12
- - mike >=1.1,<2.0
- - mkdocs >=1.5.3,<2.0
- - mkdocs-material >=9.5.4
- - linkchecker >=10.4
diff --git a/github-condarc.yml b/github-condarc.yml
deleted file mode 100644
index 586b715..0000000
--- a/github-condarc.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-auto_stack: 1
-repodata_fns:
- - repodata.json
-default_channels:
- - conda-forge
-# solver: libmamba
-
diff --git a/mkdocs.yml b/mkdocs.yml
index e48a5a4..0cb1b2a 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -1,87 +1,128 @@
-site_name: "mkdocstrings-python-xref"
-site_description: "A an extended Python handler for mkdocstrings."
-repo_url: https://github.com/analog-garage/mkdocstrings-python-xref
-site_url: https://github.com/analog-garage/mkdocstrings-python-xref
-repo_name: GitHub
-site_dir: site
-site_author: Christopher Barber
-copyright: Copyright © 2022-2023 Analog Devices, Inc.
+site_name: "mkdocstrings-python-betterrefs"
+site_description: "Extended mkdocstrings-python handler with better cross-references support"
+site_url: https://github.com/ItsDrike/mkdocstrings-python-betterrefs
+
+repo_url: https://github.com/ItsDrike/mkdocstrings-python-betterrefs
+copyright: Copyright © 2025 by ItsDrike
watch:
- src/mkdocstrings_handlers
+ - LICENSE.txt
+ - NOTICE.md
+ - CHANGELOG.md
+
+exclude_docs: |
+ LICENSE.md
nav:
-- User Guide: index.md
-- Setup:
- - Installation: install.md
- - Configuration: config.md
-- Support:
- - Getting help: support.md
- - Changelog: changelog.md
- - License: license.md
+ - Home: index.md
+ - Installation:
+ - Installation: install.md
+ - Configuration: config.md
+ - Usage: usage.md
+ - Meta:
+ - Getting help: meta/support.md
+ - Versioning: meta/versioning.md
+ - Changelog: meta/changelog.md
+ - Fork notice: meta/fork.md
+ - License: meta/license.md
+ - Contributing:
+ - Bugs & Feature requests: contributing/bugs-and-feature-reqs.md
+ - Making a PR: contributing/making-a-pr.md
theme:
name: material
logo: logo.svg
favicon: logo.svg
- features:
- - navigation.tabs
- - navigation.tabs.sticky
- - navigation.top
palette:
- - media: "(prefers-color-scheme: light)"
- scheme: default
- primary: white
- accent: purple
- toggle:
- icon: material/weather-sunny
- name: Switch to dark mode
- - media: "(prefers-color-scheme: dark)"
- scheme: slate
- primary: black
- accent: lime
- toggle:
- icon: material/weather-night
- name: Switch to light mode
+ - media: "(prefers-color-scheme)"
+ primary: black
+ accent: black
+ toggle:
+ icon: material/brightness-auto
+ name: Switch to light mode
-extra:
- generator: false
- version:
- provider: mike
- default: stable
+ - media: "(prefers-color-scheme: light)"
+ scheme: default
+ primary: black
+ accent: black
+ toggle:
+ icon: material/brightness-7
+ name: Switch to dark mode
+
+ - media: "(prefers-color-scheme: dark)"
+ scheme: slate
+ primary: black
+ accent: black
+ toggle:
+ icon: material/brightness-4
+ name: Switch to system preference
+ icon:
+ repo: fontawesome/brands/github
+ features:
+ - content.tabs.link
+ - content.code.copy
+ - content.action.edit
+ - search.highlight
+ - search.share
+ - search.suggest
+ - navigation.footer
+ - navigation.indexes
+ - navigation.sections
+ - navigation.tabs
+ - navigation.tabs.sticky
+ - navigation.top
+ - toc.follow
markdown_extensions:
-- admonition
-- pymdownx.snippets:
- check_paths: true
-- pymdownx.superfences
-- pymdownx.tabbed:
- alternate_style: true
+ - admonition
+ - attr_list
+ - md_in_html
+ - toc:
+ permalink: true
+ - pymdownx.highlight:
+ anchor_linenums: true
+ - pymdownx.snippets:
+ check_paths: true
+ - pymdownx.inlinehilite
+ - pymdownx.superfences
+ - pymdownx.details
+ - pymdownx.emoji:
+ emoji_index: !!python/name:material.extensions.emoji.twemoji
+ emoji_generator: !!python/name:material.extensions.emoji.to_svg
+ - pymdownx.tabbed:
+ alternate_style: true
plugins:
-- search
-- mike:
- # These fields are all optional; the defaults are as below...
- canonical_version: stable
- version_selector: true
- css_dir: css
- javascript_dir: js
-- mkdocstrings:
- default_handler: python_xref
- handlers:
- python_xref:
- paths: [src]
- import:
- - https://docs.python.org/3/objects.inv
- - https://mkdocstrings.github.io/objects.inv
- - https://mkdocstrings.github.io/griffe/objects.inv
- options:
- docstring_style: google
- docstring_options:
- ignore_init_summary: yes
- merge_init_into_class: yes
- relative_crossrefs: yes
- separate_signature: yes
- show_source: no
- show_root_full_path: no
+ - search
+ - mike:
+ canonical_version: "latest"
+ version_selector: true
+ - mkdocstrings:
+ enable_inventory: true
+ default_handler: python_betterrefs
+ handlers:
+ python_betterrefs:
+ paths: [src]
+ options:
+ docstring_style: google
+ docstring_options:
+ ignore_init_summary: true
+ merge_init_into_class: true
+ better_crossrefs: true
+ check_crossrefs: true
+ separate_signature: true
+ show_source: false
+ show_root_full_path: false
+ inventories:
+ - https://docs.python.org/3/objects.inv
+ - https://www.mkdocs.org/objects.inv
+ - https://mkdocstrings.github.io/objects.inv
+ - https://mkdocstrings.github.io/griffe/objects.inv
+ - https://mkdocstrings.github.io/autorefs/objects.inv
+extra:
+ version:
+ provider: mike
+ default: latest
+ alias: true
diff --git a/pyproject.toml b/pyproject.toml
index a1d7f52..b46d470 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,183 +1,186 @@
-[build-system]
-requires = ["hatchling"]
-build-backend = "hatchling.build"
-
[project]
-name = "mkdocstrings-python-xref"
-description = "Enhanced mkdocstrings python handler"
+name = "mkdocstrings-python-betterrefs"
+version = "1.0.0"
+license = "Apache-2.0"
+description = "Extended mkdocstrings-python handler with better cross-references support"
readme = "README.md"
authors = [
- {name = "Christopher Barber", email="Christopher.Barber@analog.com" },
+ { name = "Christopher Barber", email = "Christopher.Barber@analog.com" },
+ { name = "ItsDrike", email = "itsdrike@protonmail.com" },
]
+maintainers = [{ name = "ItsDrike", email = "itsdrike@protonmail.com" }]
classifiers = [
- "Development Status :: 3 - Alpha",
- "Intended Audience :: Developers",
- "Topic :: Software Development :: Documentation",
- "Programming Language :: Python :: 3.8",
- "Programming Language :: Python :: 3.9",
- "Programming Language :: Python :: 3.10",
- "Programming Language :: Python :: 3.11",
- "Programming Language :: Python :: 3.12",
-]
-keywords = [
- "documentation-tool", "mkdocstrings", "mkdocstrings-handler", "python"
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: Apache Software License",
+ "Natural Language :: English",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
+ "Topic :: Documentation",
+ "Topic :: Software Development",
+ "Topic :: Software Development :: Documentation",
+ "Topic :: Utilities",
+ "Typing :: Typed",
]
-dynamic = ["version"]
-requires-python = ">=3.8"
+keywords = ["documentation", "mkdocstrings", "mkdocstrings-handler"]
+requires-python = ">=3.9"
dependencies = [
- "mkdocstrings-python >=1.6.2,<2.0",
- "griffe >=1.0"
+ "mkdocstrings-python>=1.14.1,<1.15.0",
+ "griffe>=1.0.0",
+ "typing-extensions>=4.0",
]
[project.urls]
-Repository = "https://github.com/analog-garage/mkdocstrings-python-xref"
-Documentation = "https://analog-garage.github.io/mkdocstrings-python-xref/"
-
-[tool.hatch.version]
-path = "src/mkdocstrings_handlers/python_xref/VERSION"
-pattern = "\\s*(?P[\\w.]*)"
-
-[tool.hatch.build]
-include = [
- "src/**/*.py",
- "src/mkdocstrings_handlers/python_xref/VERSION",
- "src/mkdocstrings_handlers/python_xref/py.typed",
+Homepage = "https://itsdrike.github.com/mkdocstrings-python-betterrefs/"
+Documentation = "https://itsdrike.github.com/mkdocstrings-python-betterrefs/"
+Repository = "https://github.com/ItsDrike/mkdocstrings-python-betterrefs"
+Issues = "https://github.com/ItsDrike/mkdocstrings-python-betterrefs/issues"
+
+[tool.uv]
+default-groups = ["lint", "test", "docs"]
+
+[dependency-groups]
+lint = [
+ "pre-commit>=3.3.3",
+ "ruff>=0.9.2",
+ "basedpyright>=1.13.3",
]
-
-[tool.hatch.build.targets.sdist]
-packages = [
- "src/mkdocstrings_handlers",
- "src/mkdocstrings_handlers/python_xref",
+test = [
+ "beautifulsoup4>=4.13.3",
+ "pytest>=8.3.4",
]
-
-[tool.hatch.build.targets.wheel]
-packages = [
- "src/mkdocstrings_handlers",
- "src/mkdocstrings_handlers/python_xref",
+docs = [
+ # Most of the docs dependencies are already a part of the project itself
+ "mike>=2.1.2",
+ "mkdocs-material>=9.5.30",
]
-[tool.mypy]
-check_untyped_defs = true
-mypy_path = "src"
-namespace_packages = true
-explicit_package_bases = true
-files = [
- "src/mkdocstrings_handlers",
- "tests"
- ]
-show_error_codes = true
-disallow_untyped_defs = true
-disallow_incomplete_defs = true
-
-[[tool.mypy.overrides]]
-module = [
- "bs4"
+[tool.pytest.ini_options]
+filterwarnings = [
+ "error",
]
-ignore_missing_imports = true
+
+[tool.basedpyright]
+pythonPlatform = "All"
+pythonVersion = "3.9"
+typeCheckingMode = "all"
+
+# Diagnostic behavior settings
+strictListInference = false
+strictDictionaryInference = false
+strictSetInference = false
+analyzeUnannotatedFunctions = true
+strictParameterNoneValue = true
+enableTypeIgnoreComments = true
+deprecateTypingAliases = true
+enableExperimentalFeatures = false
+
+# Diagnostic rules
+reportAny = false
+reportExplicitAny = false
+reportImplicitStringConcatenation = false
+reportUnreachable = "hint"
+reportUnusedParameter = "hint"
+reportUnannotatedClassAttribute = false
+reportUnknownArgumentType = false # consider enabling
+reportUnknownVariableType = false # consider enabling
+reportUnknownMemberType = false # consider enabling
+reportUnknownParameterType = false # consider enabling
+reportUnknownLambdaType = false # consider enabling
+reportMissingTypeStubs = "information" # consider bumping to warning/error
+reportUninitializedInstanceVariable = false # until https://github.com/DetachHead/basedpyright/issues/491
+reportMissingParameterType = false # ruff's flake8-annotations (ANN) already covers this + gives us more control
+
+[tool.ruff]
+target-version = "py39"
+line-length = 119
[tool.ruff.lint]
-# TODO add "I" (isort)
-# TODO add "RUF"
-# TODO add "ARG"
-# TODO add "SIM"
-# TODO add "S" (bandit)
-# TODO add "PT" (pytest style)
-# TODO add "CPY101"
-select = ["E", "F", "PL", "D", "R", "T10", "EXE"]
+select = ["ALL"]
+
ignore = [
- "D105", # missing doc string for dunder methods
- "D200", # one line doc string
- "D202", # blank lines after function docstring
- "D205", # doc on first line
- "D212", # doc on first line
- "D410", # blank line after doc section
- "D411", # blank line before doc section
- "D412", # no blank lines after section header
- "D415", # doc title punctuation
- "E501", # line too long
- "PLC0105", # covariant metatype names
- "PLR0913", # too-many-argument
- "PLR2004", # magic value
- "PT001", # allow @pytest.fixture without parens
- "RET504", # unnecessary assignment to variable before return
- "S101", # use of assert - do we care?
- # TODO: fix the ones below
- "D403", # capitalize first word of doc string
- "D102", # undocumented public method
- "D104", # missing docstring in public package
- "D107", # __init__ docstring
- "D417", # missing argument description
-]
-preview = true
-explicit-preview-rules = true # only preview explicitly selected rules (E.g. CPY001)
-
-[tool.ruff.lint.per-file-ignores]
-# Ignore some issues in tests
-"tests/**" = [
- "F401", # unused import (pytest fixture)
- "F403", # wildcard import (for fixtures)
- "F405", # defined from star imports (typicallky a pytest fixture)
- "F811", # redefinition of unused (typically a pytest fixture)
- "PLR0912", # too many branches
- "PLR0915", # too many statements
- "S", # bandit rules
+ "C90", # mccabe
+ "FBT", # flake8-boolean-trap
+ "CPY", # flake8-copyright
+ "EM", # flake8-errmsg
+ "SLF", # flake8-self
+ "ARG", # flake8-unused-arguments
+ "TD", # flake8-todos
+ "FIX", # flake8-fixme
+
+ "D100", # Missing docstring in public module
+ "D104", # Missing docstring in public package
+ "D105", # Missing docstring in magic method
+ "D107", # Missing docstring in __init__
+ "D203", # Blank line required before class docstring
+ "D213", # Multi-line summary should start at second line (incompatible with D212)
+ "D301", # Use r""" if any backslashes in a docstring
+ "D405", # Section name should be properly capitalized
+ "D406", # Section name should end with a newline
+ "D407", # Missing dashed underline after section
+ "D408", # Section underline should be in the line following the section's name
+ "D409", # Section underline should match the length of its name
+ "D410", # Missing blank line after section
+ "D411", # Missing blank line before section
+ "D412", # No blank lines allowed between a section header and its content
+ "D413", # Missing blank line after last section
+ "D414", # Section has no content
+ "D416", # Section name should end with a colon
+ "D417", # Missing argument descrition in the docstring
+
+ "ANN002", # Missing type annotation for *args
+ "ANN003", # Missing type annotation for **kwargs
+ "ANN204", # Missing return type annotation for special method
+ "ANN401", # Dynamically typed expressions (typing.Any) disallowed
+
+ "SIM102", # use a single if statement instead of nested if statements
+ "SIM108", # Use ternary operator {contents} instead of if-else-block
+
+ "TC001", # Move application imports used only for annotations into a type-checking block
+ "TC002", # Move 3rd-party imports used only for annotations into a type-checking block
+ "TC003", # Move standard library imports used only for annotations into a type-checking block
+
+ "TD002", # Missing author in TODO
+ "TD003", # Missing issue link on the line following this TODO
+
+ "TRY003", # No f-strings in raise statements
+ "EM101", # No string literals in exception init
+ "EM102", # No f-strings in exception init
+ "PLR2004", # Using unnamed numerical constants
+ "PGH003", # Using specific rule codes in type ignores
+ "E731", # Don't assign a lambda expression, use a def
]
-[tool.ruff.lint.pylint]
-#max-locals = 30
-max-branches = 15
-#max-attributes = 30
+[tool.ruff.lint.extend-per-file-ignores]
+"tests/*" = [
+ "ANN", # flake8-annotations
+ "S101", # Use of assert
+]
[tool.ruff.lint.pydocstyle]
convention = "google"
-[too.ruff.format]
-skip-magic-trailing-comma = false
+[tool.ruff.lint.pylint]
+max-args = 20
+max-branches = 20
+max-returns = 20
+max-statements = 250
+
+[tool.ruff.format]
line-ending = "lf"
-[tool.pylint.main]
-jobs = 0
-# Minimum Python version to use for version dependent checks.
-py-version = "3.8"
-
-[tool.pylint.format]
-max-line-length = 110
-max-module-lines = 2000
-
-[tool.pylint."messages control"]
-disable = [
- "bad-inline-option",
- "c-extension-no-member",
- "consider-using-from-import",
- "deprecated-pragma",
- "disallowed-name",
- "file-ignored",
- "fixme",
- "import-outside-toplevel",
- "invalid-characters-in-docstring",
- "invalid-name",
- "locally-disabled",
- "multiple-statements",
- "no-else-return",
- "raw-checker-failed",
- "superfluous-parens",
- "suppressed-message",
- "too-few-public-methods",
- "too-many-arguments",
- "too-many-branches",
- "too-many-instance-attributes",
- "too-many-locals",
- "too-many-public-methods",
- "too-many-statements",
- "trailing-newlines",
- "trailing-whitespace",
- "unspecified-encoding",
- "unused-wildcard-import",
- "use-dict-literal",
- "use-symbolic-message-instead",
- "useless-suppression",
- "wrong-import-order",
- "wrong-import-position",
- "wrong-spelling-in-comment",
- "wrong-spelling-in-docstring",
-]
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build.targets.wheel]
+only-include = ["src/mkdocstrings_handlers"]
+sources = ["src"]
diff --git a/src/mkdocstrings_handlers/python_betterrefs/__init__.py b/src/mkdocstrings_handlers/python_betterrefs/__init__.py
new file mode 100644
index 0000000..cbc8211
--- /dev/null
+++ b/src/mkdocstrings_handlers/python_betterrefs/__init__.py
@@ -0,0 +1,48 @@
+"""Expose the extended mkdocstrings python handler."""
+
+from collections.abc import MutableMapping
+from pathlib import Path
+from typing import Any
+from warnings import warn
+
+from mkdocs.config.defaults import MkDocsConfig
+
+from mkdocstrings_handlers.python.config import PythonConfig
+
+from .handler import PythonBetterRefsHandler
+
+__all__ = ["get_handler"]
+
+
+def get_handler(
+ handler_config: MutableMapping[str, Any],
+ tool_config: MkDocsConfig,
+ **kwargs: Any,
+) -> PythonBetterRefsHandler:
+ """Return an instance of PythonBetterRefsHandler handler.
+
+ This function essentially mimics the same function from mkdocstrings-python,
+ just returning our extended handler instead of the Python one.
+
+ Arguments:
+ handler_config: The handler configuration.
+ tool_config: The tool (SSG) configuration.
+
+ Returns:
+ An instance of `PythonHandler`.
+ """
+ base_dir = Path(tool_config.config_file_path or "./mkdocs.yml").parent
+ if "inventories" not in handler_config and "import" in handler_config:
+ warn("The 'import' key is renamed 'inventories' for the Python handler", FutureWarning, stacklevel=1)
+ handler_config["inventories"] = handler_config.pop("import", [])
+
+ # PythonConfig will actually store all of the options in a dict without doing any
+ # checking during this initialization. That means we can just re-use it here, our
+ # custom options will be stored and we can handle them from __init__.
+ config = PythonConfig.from_data(**handler_config)
+
+ return PythonBetterRefsHandler(
+ config=config,
+ base_dir=base_dir,
+ **kwargs,
+ )
diff --git a/src/mkdocstrings_handlers/python_betterrefs/config.py b/src/mkdocstrings_handlers/python_betterrefs/config.py
new file mode 100644
index 0000000..5b49c84
--- /dev/null
+++ b/src/mkdocstrings_handlers/python_betterrefs/config.py
@@ -0,0 +1,58 @@
+from collections.abc import Mapping
+from dataclasses import dataclass, fields
+from typing import Annotated, Any
+
+from typing_extensions import Self
+
+from mkdocstrings_handlers.python.config import (
+ Field,
+ PythonOptions,
+ _dataclass_options, # pyright: ignore[reportPrivateUsage]
+)
+
+
+@dataclass(**_dataclass_options)
+class PythonBetterRefsOptions(PythonOptions):
+ """Accepted input options."""
+
+ better_crossrefs: Annotated[
+ bool,
+ Field(group="docstrings", description="Whether to enable better crossrefs syntax."),
+ ] = True
+
+ check_crossrefs: Annotated[
+ bool,
+ Field(
+ group="docstrings",
+ description="""Whether to produce improved warnings for invalid cross-references.
+
+ This will only take effect if `better_crossrefs` is also enabled.
+ """,
+ ),
+ ] = True
+
+ @staticmethod
+ def extract_betterrefs(data: Mapping[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
+ """Extract the options specific to better-refs, leaving the rest untouched.
+
+ Returns:
+ A tuple of (better-refs specific options, remaining options).
+ """
+ better_refs_fields = {"better_crossrefs", "check_crossrefs"}
+ copy = dict(data)
+ return {name: copy.pop(name) for name in data if name in better_refs_fields}, copy
+
+ @classmethod
+ def merge_python(cls, python_opts: PythonOptions, extra_data: Mapping[str, Any]) -> Self:
+ """Create a better-refs options from an existing PythonOptions instance and the extra data.
+
+ Note that the passed extra_data is expected to already be coerced.
+ """
+ if type(python_opts) is not PythonOptions:
+ raise TypeError("This function can only work directly with PythonOptions instances, not any descendents.")
+
+ orig_fields = fields(PythonOptions) # pyright: ignore[reportArgumentType] # PythonOptions is a dataclass
+ field_names = {field.name for field in orig_fields}
+ orig_data = {name: getattr(python_opts, name) for name in field_names}
+
+ return cls(**orig_data, **extra_data)
diff --git a/src/mkdocstrings_handlers/python_xref/crossref.py b/src/mkdocstrings_handlers/python_betterrefs/crossref.py
similarity index 84%
rename from src/mkdocstrings_handlers/python_xref/crossref.py
rename to src/mkdocstrings_handlers/python_betterrefs/crossref.py
index 1035d4e..c298ed7 100644
--- a/src/mkdocstrings_handlers/python_xref/crossref.py
+++ b/src/mkdocstrings_handlers/python_betterrefs/crossref.py
@@ -1,32 +1,19 @@
-# Copyright (c) 2022-2024. Analog Devices Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
"""Support for translating compact relative crossreferences in docstrings."""
from __future__ import annotations
import re
-from typing import Callable, List, Optional, cast
+from collections.abc import Callable
+from typing import cast
from griffe import Docstring, Object
from mkdocstrings.loggers import get_logger
-__all__ = [
- "substitute_relative_crossrefs"
-]
+__all__ = ["substitute_relative_crossrefs"]
logger = get_logger(__name__)
+
def _re_or(*exps: str) -> str:
"""Construct an "or" regular expression from a sequence of regular expressions.
@@ -53,6 +40,7 @@ def _re_named(name: str, exp: str, optional: bool = False) -> str:
optchar = "?" if optional else ""
return f"(?P<{name}>{exp}){optchar}"
+
_RE_CROSSREF = re.compile(r"\[([^\[\]]+?)\]\[([^\[\]]*?)\]")
"""Regular expression that matches general cross-references."""
@@ -75,7 +63,7 @@ def _re_named(name: str, exp: str, optional: bool = False) -> str:
),
optional=True,
)
- + _re_named("relname", r"(?:[a-zA-Z_][a-zA-Z0-9_\.]*)?")
+ + _re_named("relname", r"(?:[a-zA-Z_][a-zA-Z0-9_\.]*)?"),
)
"""Regular expression that matches a relative path reference.
@@ -101,22 +89,21 @@ def _always_ok(_ref: str) -> bool:
class _RelativeCrossrefProcessor:
- """
- A callable object that can substitute relative cross-reference expressions.
+ """A callable object that can substitute relative cross-reference expressions.
This is intended to be used as a substitution function by `re.sub`
to process relative cross-references in a doc-string.
"""
_doc: Docstring
- _cur_match: re.Match | None
+ _cur_match: re.Match[str] | None
_cur_input: str
_cur_offset: int
- _cur_ref_parts: List[str]
+ _cur_ref_parts: list[str]
_ok: bool
_check_ref: Callable[[str], bool]
- def __init__(self, doc: Docstring, checkref: Optional[Callable[[str], bool]] = None):
+ def __init__(self, doc: Docstring, checkref: Callable[[str], bool] | None = None):
self._doc = doc
self._cur_match = None
self._cur_input = ""
@@ -125,9 +112,8 @@ def __init__(self, doc: Docstring, checkref: Optional[Callable[[str], bool]] = N
self._check_ref = checkref or _always_ok
self._ok = True
- def __call__(self, match: re.Match) -> str:
- """
- Process a cross-reference expression.
+ def __call__(self, match: re.Match[str]) -> str:
+ """Process a cross-reference expression.
This should be called with a match from the _RE_CROSSREF expression
which matches expression of the form [][[].
@@ -161,14 +147,17 @@ def __call__(self, match: re.Match) -> str:
self._process_append_from_title(ref_match, title)
if self._ok:
- new_ref = '.'.join(self._cur_ref_parts)
+ new_ref = ".".join(self._cur_ref_parts)
logger.debug(
"cross-reference substitution\nin %s:\n[%s][%s] -> [...][%s]",
- cast(Object, self._doc.parent).canonical_path, title, ref, new_ref
+ cast(Object, self._doc.parent).canonical_path,
+ title,
+ ref,
+ new_ref,
)
# builtin names get handled specially somehow, so don't check here
- if new_ref not in __builtins__ and not checkref(new_ref): # type: ignore[operator]
+ if new_ref not in __builtins__ and not checkref(new_ref):
self._error(f"Cannot load reference '{new_ref}'")
if new_ref:
@@ -178,19 +167,19 @@ def __call__(self, match: re.Match) -> str:
return result
- def _start_match(self, match: re.Match) -> None:
+ def _start_match(self, match: re.Match[str]) -> None:
self._cur_match = match
self._cur_offset = match.start(0)
self._cur_input = match[0]
self._ok = True
self._cur_ref_parts.clear()
- def _process_relname(self, ref_match: re.Match) -> None:
+ def _process_relname(self, ref_match: re.Match[str]) -> None:
relname = ref_match.group("relname").strip(".")
if relname:
self._cur_ref_parts.append(relname)
- def _process_append_from_title(self, ref_match: re.Match, title_text: str) -> None:
+ def _process_append_from_title(self, ref_match: re.Match[str], title_text: str) -> None:
if ref_match.group(0).endswith("."):
id_from_title = title_text.strip("`*")
if not _RE_ID.fullmatch(id_from_title):
@@ -198,7 +187,7 @@ def _process_append_from_title(self, ref_match: re.Match, title_text: str) -> No
return
self._cur_ref_parts.append(id_from_title)
- def _process_parent_specifier(self, ref_match: re.Match) -> None:
+ def _process_parent_specifier(self, ref_match: re.Match[str]) -> None:
if not ref_match.group("parent"):
return
@@ -218,19 +207,16 @@ def _process_parent_specifier(self, ref_match: re.Match) -> None:
if rel_obj is not None and self._ok:
self._cur_ref_parts.append(rel_obj.canonical_path)
- def _process_current_specifier(self, obj: Object, ref_match: re.Match) -> Optional[Object]:
+ def _process_current_specifier(self, obj: Object, ref_match: re.Match[str]) -> Object | None:
rel_obj: Object | None = None
if ref_match.group("current"):
if obj.is_function:
- self._error(
- f"Cannot use '.' in function {obj.canonical_path}",
- just_warn=False
- )
+ self._error(f"Cannot use '.' in function {obj.canonical_path}", just_warn=False)
else:
rel_obj = obj
return rel_obj
- def _process_class_specifier(self, obj: Object, ref_match: re.Match) -> Optional[Object]:
+ def _process_class_specifier(self, obj: Object, ref_match: re.Match[str]) -> Object | None:
rel_obj: Object | None = None
if ref_match.group("class"):
rel_obj = obj
@@ -241,7 +227,7 @@ def _process_class_specifier(self, obj: Object, ref_match: re.Match) -> Optional
break
return rel_obj
- def _process_module_specifier(self, obj: Object, ref_match: re.Match) -> Optional[Object]:
+ def _process_module_specifier(self, obj: Object, ref_match: re.Match[str]) -> Object | None:
rel_obj: Object | None = None
if ref_match.group("module"):
rel_obj = obj
@@ -252,7 +238,7 @@ def _process_module_specifier(self, obj: Object, ref_match: re.Match) -> Optiona
break
return rel_obj
- def _process_package_specifier(self, obj: Object, ref_match: re.Match) -> Optional[Object]:
+ def _process_package_specifier(self, obj: Object, ref_match: re.Match[str]) -> Object | None:
# griffe does not distinguish between modules and packages, so we identify a package
# as a module that contains other modules. A module that has no parent is considered to
# be a package even if it does not contain modules.
@@ -275,7 +261,7 @@ def _process_package_specifier(self, obj: Object, ref_match: re.Match) -> Option
return rel_obj
- def _process_up_specifier(self, obj: Object, ref_match: re.Match) -> Optional[Object]:
+ def _process_up_specifier(self, obj: Object, ref_match: re.Match[str]) -> Object | None:
rel_obj: Object | None = None
if ref_match.group("up"):
level = len(ref_match.group("up"))
@@ -313,12 +299,12 @@ def _error(self, msg: str, just_warn: bool = False) -> None:
# that without knowing how much the doc string was unindented.
prefix += " \n"
- logger.warning(prefix + msg)
+ logger.warning(prefix + msg) # noqa: G003
self._ok = just_warn
-def substitute_relative_crossrefs(obj: Object, checkref: Optional[Callable[[str], bool]] = None) -> None:
+def substitute_relative_crossrefs(obj: Object, checkref: Callable[[str], bool] | None = None) -> None:
"""Recursively expand relative cross-references in all docstrings in tree.
Arguments:
diff --git a/src/mkdocstrings_handlers/python_betterrefs/handler.py b/src/mkdocstrings_handlers/python_betterrefs/handler.py
new file mode 100644
index 0000000..5c14bfb
--- /dev/null
+++ b/src/mkdocstrings_handlers/python_betterrefs/handler.py
@@ -0,0 +1,85 @@
+"""Implementation of python_betterrefs handler."""
+
+from __future__ import annotations
+
+from collections.abc import Mapping
+from dataclasses import replace
+from pathlib import Path
+from typing import Any, ClassVar
+
+from mkdocstrings.handlers.base import CollectionError, CollectorItem, HandlerOptions
+from mkdocstrings.loggers import get_logger
+from typing_extensions import override
+
+from mkdocstrings_handlers.python.config import PythonConfig, PythonOptions
+from mkdocstrings_handlers.python.handler import PythonHandler
+from mkdocstrings_handlers.python_betterrefs.config import PythonBetterRefsOptions
+
+from .crossref import substitute_relative_crossrefs
+
+__all__ = ["PythonBetterRefsHandler"]
+
+logger = get_logger(__name__)
+
+
+class PythonBetterRefsHandler(PythonHandler):
+ """Extended version of mkdocstrings Python handler.
+
+ * Converts custom cross-reference syntax into full (absolute) references.
+ * Checks cross-references early in order to produce errors with source location.
+ """
+
+ name: ClassVar[str] = "python_betterrefs"
+ """The handler's name."""
+
+ @override
+ def __init__(self, config: PythonConfig, base_dir: Path, **kwargs: Any) -> None:
+ # Extract our custom options before thy're passed into PythonHandler init,
+ # preventing complains about unknown options
+ self.better_refs_opts, remaining_opts = PythonBetterRefsOptions.extract_betterrefs(config.options)
+ config = replace(config, options=remaining_opts) # pyright: ignore[reportArgumentType] # PythonConfig is a dataclass
+
+ super().__init__(config, base_dir, **kwargs)
+
+ @override
+ def get_options(self, local_options: Mapping[str, Any]) -> HandlerOptions:
+ local_better_refs_opts, local_remaining_opts = PythonBetterRefsOptions.extract_betterrefs(local_options)
+ python_opts: PythonOptions = super().get_options(local_remaining_opts)
+
+ better_refs_opts = self.better_refs_opts.copy()
+ better_refs_opts.update(local_better_refs_opts)
+
+ return PythonBetterRefsOptions.merge_python(python_opts, better_refs_opts)
+
+ @override
+ def render( # pyright: ignore[reportIncompatibleMethodOverride] # we use our type for options
+ self,
+ data: CollectorItem,
+ options: PythonBetterRefsOptions,
+ ) -> str:
+ if options.better_crossrefs:
+ checkref = self._check_ref if options.check_crossrefs else None
+ substitute_relative_crossrefs(data, checkref=checkref)
+
+ try:
+ return super().render(data, options)
+ except Exception: # pragma: no cover
+ print(f"{data.path=}") # noqa: T201
+ raise
+
+ @override
+ def get_templates_dir(self, handler: str | None = None) -> Path:
+ # Return the python handler templates dir
+ if handler == self.name:
+ handler = "python"
+ return super().get_templates_dir(handler)
+
+ def _check_ref(self, ref: str) -> bool:
+ """Check for existence of reference."""
+ # Try to collect the reference normally and see if it fails
+ try:
+ self.collect(ref, PythonOptions())
+ except CollectionError:
+ return False
+ else:
+ return True
diff --git a/src/mkdocstrings_handlers/python_xref/py.typed b/src/mkdocstrings_handlers/python_betterrefs/py.typed
similarity index 100%
rename from src/mkdocstrings_handlers/python_xref/py.typed
rename to src/mkdocstrings_handlers/python_betterrefs/py.typed
diff --git a/src/mkdocstrings_handlers/python_xref/VERSION b/src/mkdocstrings_handlers/python_xref/VERSION
deleted file mode 100644
index fdd3be6..0000000
--- a/src/mkdocstrings_handlers/python_xref/VERSION
+++ /dev/null
@@ -1 +0,0 @@
-1.6.2
diff --git a/src/mkdocstrings_handlers/python_xref/__init__.py b/src/mkdocstrings_handlers/python_xref/__init__.py
deleted file mode 100644
index 56a5170..0000000
--- a/src/mkdocstrings_handlers/python_xref/__init__.py
+++ /dev/null
@@ -1,22 +0,0 @@
-# Copyright (c) 2022-2023. Analog Devices Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""
-Extended mkdocstrings python handler
-"""
-
-from .handler import PythonRelXRefHandler
-
-__all__ = ["get_handler"]
-
-get_handler = PythonRelXRefHandler
diff --git a/src/mkdocstrings_handlers/python_xref/handler.py b/src/mkdocstrings_handlers/python_xref/handler.py
deleted file mode 100644
index 5128e2f..0000000
--- a/src/mkdocstrings_handlers/python_xref/handler.py
+++ /dev/null
@@ -1,95 +0,0 @@
-# Copyright (c) 2022=2023. Analog Devices Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""
-Implementation of python_xref handler
-"""
-
-from __future__ import annotations
-
-from collections import ChainMap
-from pathlib import Path
-from typing import Any, List, Mapping, Optional
-
-from griffe import Object
-from mkdocstrings.loggers import get_logger
-from mkdocstrings_handlers.python.handler import PythonHandler
-
-from .crossref import substitute_relative_crossrefs
-
-__all__ = [
- 'PythonRelXRefHandler'
-]
-
-logger = get_logger(__name__)
-
-class PythonRelXRefHandler(PythonHandler):
- """Extended version of mkdocstrings Python handler
-
- * Converts relative cross-references into full references
- * Checks cross-references early in order to produce errors with source location
- """
-
- handler_name: str = __name__.rsplit('.', 2)[1]
-
- default_config = dict(
- PythonHandler.default_config,
- relative_crossrefs = False,
- check_crossrefs = True,
- )
-
- def __init__(self,
- theme: str,
- custom_templates: Optional[str] = None,
- config_file_path: Optional[str] = None,
- paths: Optional[List[str]] = None,
- locale: str = "en",
- **_config: Any,
- ):
- super().__init__(
- handler = self.handler_name,
- theme = theme,
- custom_templates = custom_templates,
- config_file_path = config_file_path,
- paths = paths,
- locale=locale,
- )
-
- def render(self, data: Object, config: Mapping[str,Any]) -> str:
- final_config = ChainMap(config, self.default_config) # type: ignore[arg-type]
-
- if final_config["relative_crossrefs"]:
- checkref = self._check_ref if final_config["check_crossrefs"] else None
- substitute_relative_crossrefs(data, checkref=checkref)
-
- try:
- return super().render(data, config)
- except Exception: # pragma: no cover
- print(f"{data.path=}")
- raise
-
- def get_templates_dir(self, handler: Optional[str] = None) -> Path:
- """See [render][.barf]"""
- if handler == self.handler_name:
- handler = 'python'
- return super().get_templates_dir(handler)
-
- def _check_ref(self, ref:str) -> bool:
- """Check for existence of reference"""
- try:
- self.collect(ref, {})
- return True
- except Exception: # pylint: disable=broad-except
- # Only expect a CollectionError but we may as well catch everything.
- return False
-
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..5c39f54
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,9 @@
+from pathlib import Path
+
+import pytest
+
+
+@pytest.fixture
+def test_project() -> Path:
+ """Fixture to obtain a directory with a very basic mkdocs project."""
+ return Path(__file__).parent / "project"
diff --git a/tests/project/README.md b/tests/project/README.md
new file mode 100644
index 0000000..80108c2
--- /dev/null
+++ b/tests/project/README.md
@@ -0,0 +1,4 @@
+# Test project
+
+This is a simple project used for unit-testing purposes. It uses this custom handler and contains code that tests
+various aspects of the handler.
diff --git a/tests/project/mkdocs.yml b/tests/project/mkdocs.yml
index f3031d2..5ed899f 100644
--- a/tests/project/mkdocs.yml
+++ b/tests/project/mkdocs.yml
@@ -6,57 +6,26 @@ watch:
- src/myproj
nav:
-- Home: index.md
-- API:
- - myproj.foo: foo.md
- - myproj.bar: bar.md
- - myproj.pkg: pkg.md
- - myproj.baz: pkg-baz.md
-
-theme:
- name: material
- features:
- - navigation.tabs
- - navigation.tabs.sticky
- - navigation.top
- palette:
- - media: "(prefers-color-scheme: light)"
- scheme: default
- primary: white
- accent: purple
- toggle:
- icon: material/weather-sunny
- name: Switch to dark mode
- - media: "(prefers-color-scheme: dark)"
- scheme: slate
- primary: black
- accent: lime
- toggle:
- icon: material/weather-night
- name: Switch to light mode
-
-markdown_extensions:
-- admonition
-- pymdownx.snippets:
- check_paths: true
-- pymdownx.superfences
-- pymdownx.tabbed:
- alternate_style: true
+ - Home: index.md
+ - API:
+ - myproj.foo: foo.md
+ - myproj.bar: bar.md
+ - myproj.pkg: pkg.md
+ - myproj.baz: pkg-baz.md
plugins:
-- search
-- mkdocstrings:
- default_handler: python_xref
- handlers:
- python_xref:
- paths: [src]
- options:
- docstring_style: google
- docstring_options:
- ignore_init_summary: yes
- merge_init_into_class: yes
- separate_signature: yes
- show_source: no
- show_root_full_path: no
- relative_crossrefs: yes
-
+ - mkdocstrings:
+ default_handler: python_betterrefs
+ handlers:
+ python_betterrefs:
+ paths: [src]
+ options:
+ docstring_style: google
+ docstring_options:
+ ignore_init_summary: true
+ merge_init_into_class: true
+ separate_signature: true
+ show_source: false
+ show_root_full_path: false
+ better_crossrefs: true
+ check_crossrefs: true
diff --git a/tests/project/src/myproj/__init__.py b/tests/project/src/myproj/__init__.py
index 411c17c..e69de29 100644
--- a/tests/project/src/myproj/__init__.py
+++ b/tests/project/src/myproj/__init__.py
@@ -1,13 +0,0 @@
-# Copyright (c) 2022. Analog Devices Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
diff --git a/tests/project/src/myproj/bar.py b/tests/project/src/myproj/bar.py
index fc75c9d..6b9c071 100644
--- a/tests/project/src/myproj/bar.py
+++ b/tests/project/src/myproj/bar.py
@@ -1,35 +1,26 @@
-# Copyright (c) 2022. Analog Devices Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
"""This is another module.
This is a [bad][.] reference.
"""
+from typing_extensions import override
+
from .foo import Foo
+
class Bar(Foo):
"""See [bar][.] method."""
def bar(self) -> None:
"""This is in the [Bar][(c)] class.
+
Also see the [foo][^.] method and the [func][(m).] function.
"""
+ @override
def foo(self) -> None:
- """Overrides [Foo.foo][^^^.foo.]"""
+ """Overrides [Foo.foo][^^^.foo.]."""
def func() -> None:
"""This is a function in the [bar][(m)] module."""
-
diff --git a/tests/project/src/myproj/foo.py b/tests/project/src/myproj/foo.py
index 74bef28..1118ef3 100644
--- a/tests/project/src/myproj/foo.py
+++ b/tests/project/src/myproj/foo.py
@@ -1,22 +1,8 @@
-# Copyright (c) 2022. Analog Devices Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""
-This is a test module.
-"""
+"""This is a test module."""
+
class Foo:
- """This is a base class"""
+ """This is a base class."""
def foo(self) -> None:
- """a method"""
+ """A method."""
diff --git a/tests/project/src/myproj/pkg/__init__.py b/tests/project/src/myproj/pkg/__init__.py
index 9e30385..4b31f54 100644
--- a/tests/project/src/myproj/pkg/__init__.py
+++ b/tests/project/src/myproj/pkg/__init__.py
@@ -1,21 +1,5 @@
-# Copyright (c) 2022. Analog Devices Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""
-A module
-"""
+"""A module."""
+
def func() -> None:
- """
- A function
- """
+ """A function."""
diff --git a/tests/project/src/myproj/pkg/baz.py b/tests/project/src/myproj/pkg/baz.py
index 1ac5406..210c854 100644
--- a/tests/project/src/myproj/pkg/baz.py
+++ b/tests/project/src/myproj/pkg/baz.py
@@ -1,16 +1 @@
-# Copyright (c) 2022. Analog Devices Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""
-See [func][(p).]
-"""
+"""See [func][(p).]."""
diff --git a/tests/test_crossref.py b/tests/test_crossref.py
index 48e102d..71304d3 100644
--- a/tests/test_crossref.py
+++ b/tests/test_crossref.py
@@ -1,16 +1,3 @@
-# Copyright (c) 2022-2024. Analog Devices Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
"""Unit tests for relative crossref expansion."""
from __future__ import annotations
@@ -18,21 +5,22 @@
import inspect
import logging
import re
+from collections.abc import Callable
from pathlib import Path
-from typing import Callable, Optional
import pytest
from griffe import Class, Docstring, Function, Module, Object
# noinspection PyProtectedMember
-from mkdocstrings_handlers.python_xref.crossref import (
- _RE_CROSSREF,
- _RE_REL_CROSSREF,
- _RelativeCrossrefProcessor,
+from mkdocstrings_handlers.python_betterrefs.crossref import (
+ _RE_CROSSREF, # pyright: ignore[reportPrivateUsage]
+ _RE_REL_CROSSREF, # pyright: ignore[reportPrivateUsage]
+ _RelativeCrossrefProcessor, # pyright: ignore[reportPrivateUsage]
substitute_relative_crossrefs,
)
-def test_RelativeCrossrefProcessor(caplog: pytest.LogCaptureFixture) -> None:
+
+def test_RelativeCrossrefProcessor(caplog: pytest.LogCaptureFixture) -> None: # noqa: N802
"""Unit test for internal _RelativeCrossrefProcessor class.
Arguments:
@@ -46,14 +34,17 @@ def test_RelativeCrossrefProcessor(caplog: pytest.LogCaptureFixture) -> None:
meth1 = Function(name="meth1", parent=cls1)
cls1.members.update(meth1=meth1)
- def assert_sub(parent: Object, title: str, ref: str,
- expected: str = "",
- *,
- warning: str = "",
- relative: bool = True,
- checkref: Optional[Callable[[str],bool]] = None
- ) -> None:
- """Tests a relative crossref substitution
+ def assert_sub(
+ parent: Object,
+ title: str,
+ ref: str,
+ expected: str = "",
+ *,
+ warning: str = "",
+ relative: bool = True,
+ checkref: Callable[[str], bool] | None = None,
+ ) -> None:
+ """Tests a relative crossref substitution.
Arguments:
parent: assumed parent object for docstring
@@ -98,8 +89,8 @@ def assert_sub(parent: Object, title: str, ref: str,
assert_sub(meth1, "foo", "(c).baz.", "mod1.mod2.Class1.baz.foo")
assert_sub(meth1, "foo", "(m).", "mod1.mod2.foo")
assert_sub(meth1, "foo", "mod3.", "mod3.foo")
- assert_sub(meth1, "foo", "^^.", "mod1.mod2.foo", checkref = lambda x: True)
- assert_sub(meth1, "foo", "...", "mod1.mod2.foo", checkref = lambda x: True)
+ assert_sub(meth1, "foo", "^^.", "mod1.mod2.foo", checkref=lambda x: True)
+ assert_sub(meth1, "foo", "...", "mod1.mod2.foo", checkref=lambda x: True)
assert_sub(meth1, "Class1", "(p).mod2.", "mod1.mod2.Class1")
assert_sub(mod1, "Class1", "(P).mod2.Class1", "mod1.mod2.Class1")
@@ -110,8 +101,7 @@ def assert_nocheck(val: str) -> bool:
return False
assert_sub(cls1, "foo", "?.", "mod1.mod2.Class1.foo", checkref=assert_nocheck)
- assert_sub(cls1, "foo", "?mod1.mod2.Class1.foo", "mod1.mod2.Class1.foo",
- checkref=assert_nocheck, relative=False)
+ assert_sub(cls1, "foo", "?mod1.mod2.Class1.foo", "mod1.mod2.Class1.foo", checkref=assert_nocheck, relative=False)
# Error cases
@@ -121,13 +111,23 @@ def assert_nocheck(val: str) -> bool:
assert_sub(meth1, "bad id", "..", warning="not a qualified identifier")
assert_sub(mod2, "foo", "(c)", warning="not in a class")
assert_sub(meth1, "foo", "^^^^", warning="too many levels")
- assert_sub(meth1, "foo", "..", "mod1.mod2.Class1.foo",
- warning = "Cannot load reference 'mod1.mod2.Class1.foo'",
- checkref=lambda x: False)
- assert_sub(meth1, "foo", "mod1.mod2.Class1.foo", "mod1.mod2.Class1.foo",
- warning = "Cannot load reference 'mod1.mod2.Class1.foo'",
- relative=False,
- checkref=lambda x: False)
+ assert_sub(
+ meth1,
+ "foo",
+ "..",
+ "mod1.mod2.Class1.foo",
+ warning="Cannot load reference 'mod1.mod2.Class1.foo'",
+ checkref=lambda x: False,
+ )
+ assert_sub(
+ meth1,
+ "foo",
+ "mod1.mod2.Class1.foo",
+ "mod1.mod2.Class1.foo",
+ warning="Cannot load reference 'mod1.mod2.Class1.foo'",
+ relative=False,
+ checkref=lambda x: False,
+ )
def test_substitute_relative_crossrefs(caplog: pytest.LogCaptureFixture) -> None:
@@ -169,7 +169,7 @@ def test_substitute_relative_crossrefs(caplog: pytest.LogCaptureFixture) -> None
"""
[foo][mod1.mod2.Class1.foo]
[bar][mod1.mod2.bar]
- """
+ """,
)
assert len(caplog.records) == 0
diff --git a/tests/test_handler.py b/tests/test_handler.py
index bd61bf4..3a61e97 100644
--- a/tests/test_handler.py
+++ b/tests/test_handler.py
@@ -1,127 +1,329 @@
-# Copyright (c) 2022-2024. Analog Devices Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""Unit test for mkdocstrings_handlers.python_xref.handler module"""
+"""Unit test for mkdocstrings_handlers.python_betterrefs.handler module."""
from __future__ import annotations
-import logging
-import os
-from os import PathLike
from pathlib import Path
from typing import Any
import pytest
+from griffe import Docstring, Module, Object
+from mkdocstrings.handlers.base import CollectorItem
-from griffe import Docstring, Object, Module
-from mkdocstrings.handlers.base import CollectionError
+from mkdocstrings_handlers.python.config import PythonConfig, PythonOptions
from mkdocstrings_handlers.python.handler import PythonHandler
-from mkdocstrings_handlers.python_xref.handler import PythonRelXRefHandler
+from mkdocstrings_handlers.python_betterrefs.config import PythonBetterRefsOptions
+from mkdocstrings_handlers.python_betterrefs.handler import PythonBetterRefsHandler
-def test_handler(tmpdir: PathLike,
- monkeypatch: pytest.MonkeyPatch,
- caplog: pytest.LogCaptureFixture) -> None:
- """Unit test for GarpyPythonHandler class
- This is a minimal whitebox test that just checks whether PythonHandler class has been
- overridden correctly. A separate test should do doc generation and check the results.
+def test_init(test_project: Path) -> None:
+ """Test the initialization of the custom handler.
+
+ This is primarily to make sure that the inheritance works.
"""
+ handler = PythonBetterRefsHandler(
+ config=PythonConfig.from_data(
+ options={"better_crossrefs": True, "check_crossrefs": False, "show_source": True},
+ ),
+ base_dir=test_project,
+ theme="material",
+ mdx=[],
+ mdx_config={},
+ )
+
+ # Custom name for our handler
+ assert handler.name == "python_betterrefs"
+
+ # Template dirs should be redirected to python handler ones
+ assert handler.get_templates_dir(handler.name) == handler.get_templates_dir("python")
- config_file = os.path.join(tmpdir, 'mkdocs.yml')
- os.mkdir(os.path.join(tmpdir, 'path1'))
- os.mkdir(os.path.join(tmpdir, 'path2'))
- os.makedirs(os.path.join(tmpdir, 'custom_templates', 'python'))
- #
- # Test construction
- #
+@pytest.mark.parametrize(
+ ("input_opts", "expected_global_opts", "expected_betterrefs_opts"),
+ [
+ pytest.param(
+ {},
+ {},
+ {},
+ id="empty-opts",
+ ),
+ pytest.param(
+ {"show_source": True, "docstring_style": "google"},
+ {"show_source": True, "docstring_style": "google"},
+ {},
+ id="external-opts",
+ ),
+ pytest.param(
+ {"better_crossrefs": True, "check_crossrefs": True},
+ {},
+ {"better_crossrefs": True, "check_crossrefs": True},
+ id="internal-opts",
+ ),
+ pytest.param(
+ {"show_source": True, "docstring_style": "google", "better_crossrefs": True, "check_crossrefs": True},
+ {"show_source": True, "docstring_style": "google"},
+ {"better_crossrefs": True, "check_crossrefs": True},
+ id="mix",
+ ),
+ ],
+)
+def test_options(
+ test_project: Path,
+ input_opts: dict[str, Any],
+ expected_global_opts: dict[str, Any],
+ expected_betterrefs_opts: dict[str, Any],
+) -> None:
+ """Test whether the options are parsed successfully.
- handler = PythonRelXRefHandler(
- 'material',
- config_file_path = config_file,
- custom_templates = 'custom_templates',
- paths = ['path1', 'path2']
+ This makes sure the custom options for this handler aren't being propagated to
+ the original handler, where they'd cause issues and are instead correctly extracted.
+ """
+ handler = PythonBetterRefsHandler(
+ config=PythonConfig.from_data(options=input_opts),
+ base_dir=test_project,
+ theme="material",
+ mdx=[],
+ mdx_config={},
)
- assert handler.handler_name == 'python_xref'
- # NOTE: these could break if PythonHandler changes
- # pylint: disable=protected-access
- assert handler.handler_name == 'python_xref'
- assert handler._config_file_path == config_file
- assert os.path.join(tmpdir, 'path1') in handler._paths
- assert os.path.join(tmpdir, 'path2') in handler._paths
+ assert handler.global_options == expected_global_opts
+ assert handler.better_refs_opts == expected_betterrefs_opts
+
- #
- # Test get_templates_dir() redirection
- #
+@pytest.mark.parametrize(
+ ("input_opts", "local_opts", "expected_merged_opts"),
+ [
+ pytest.param(
+ {
+ "better_crossrefs": True,
+ "check_crossrefs": False,
+ "show_source": True,
+ },
+ {},
+ PythonBetterRefsOptions.from_data(
+ better_crossrefs=True,
+ check_crossrefs=False,
+ show_source=True,
+ ),
+ id="only-global-opts",
+ ),
+ pytest.param(
+ {
+ "better_crossrefs": True,
+ "check_crossrefs": False,
+ "show_source": True,
+ },
+ {"show_source": True},
+ PythonBetterRefsOptions.from_data(
+ better_crossrefs=True,
+ check_crossrefs=False,
+ show_source=True,
+ ),
+ id="external-local-opt-unchanged",
+ ),
+ pytest.param(
+ {
+ "better_crossrefs": True,
+ "check_crossrefs": False,
+ "show_source": True,
+ },
+ {"show_source": False},
+ PythonBetterRefsOptions.from_data(
+ better_crossrefs=True,
+ check_crossrefs=False,
+ show_source=False,
+ ),
+ id="external-local-opt-changed",
+ ),
+ pytest.param(
+ {
+ "better_crossrefs": True,
+ "check_crossrefs": False,
+ "show_source": True,
+ },
+ {"check_crossrefs": False},
+ PythonBetterRefsOptions.from_data(
+ better_crossrefs=True,
+ check_crossrefs=False,
+ show_source=True,
+ ),
+ id="internal-local-opt-unchanged",
+ ),
+ pytest.param(
+ {
+ "better_crossrefs": True,
+ "check_crossrefs": False,
+ "show_source": True,
+ },
+ {"check_crossrefs": True},
+ PythonBetterRefsOptions.from_data(
+ better_crossrefs=True,
+ check_crossrefs=True,
+ show_source=True,
+ ),
+ id="internal-local-opt-changed",
+ ),
+ pytest.param(
+ {
+ "better_crossrefs": True,
+ "check_crossrefs": False,
+ "show_source": True,
+ },
+ {"check_crossrefs": True, "show_source": True},
+ PythonBetterRefsOptions.from_data(
+ better_crossrefs=True,
+ check_crossrefs=True,
+ show_source=True,
+ ),
+ id="mix",
+ ),
+ ],
+)
+def test_options_merging(
+ input_opts: dict[str, Any],
+ local_opts: dict[str, Any],
+ expected_merged_opts: PythonBetterRefsOptions,
+ test_project: Path,
+) -> None:
+ """Test the logic for local opts merging was overwritten correctly."""
+ handler = PythonBetterRefsHandler(
+ config=PythonConfig.from_data(options=input_opts),
+ base_dir=test_project,
+ theme="material",
+ mdx=[],
+ mdx_config={},
+ )
- assert handler.get_templates_dir(handler.handler_name) == handler.get_templates_dir('python')
+ merged_opts = handler.get_options(local_opts)
+ assert type(merged_opts) is PythonBetterRefsOptions
+ assert merged_opts == expected_merged_opts
- #
- # Test render()
- #
- def fake_collect(_self: PythonHandler, identifier: str, _config: dict) -> Any:
- if identifier.startswith('mod'):
- return Object(identifier)
- raise CollectionError(identifier)
+@pytest.mark.parametrize(
+ ("griffe_obj", "options", "checkref_result", "expected_rendered", "expected_error"),
+ [
+ pytest.param(
+ Module(name="mod", filepath=Path("mod."), docstring=Docstring("[foo][.foo]")),
+ {"better_crossrefs": False, "check_crossrefs": False},
+ True,
+ "[foo][.foo]",
+ False,
+ id="rel-ref-but-disabled",
+ ),
+ pytest.param(
+ Module(name="mod", filepath=Path("mod."), docstring=Docstring("[foo][.foo]")),
+ {"better_crossrefs": True, "check_crossrefs": False},
+ True,
+ "[foo][mod.foo]",
+ False,
+ id="rel-ref-enabled",
+ ),
+ pytest.param(
+ Module(name="mod", filepath=Path("mod."), docstring=Docstring("[bar][.]")),
+ {"better_crossrefs": True, "check_crossrefs": False},
+ True,
+ "[bar][mod.bar]",
+ False,
+ id="rel-ref-enabled2",
+ ),
+ pytest.param(
+ Module(name="mod", filepath=Path("mod."), docstring=Docstring("[foo][mod.foo]")),
+ {"better_crossrefs": True, "check_crossrefs": True},
+ True,
+ "[foo][mod.foo]",
+ False,
+ id="abs-ref-check-crossrefs-pass",
+ ),
+ pytest.param(
+ Module(name="mod", filepath=Path("mod."), docstring=Docstring("[foo][mod.foo]")),
+ {"better_crossrefs": True, "check_crossrefs": True},
+ False,
+ "[foo][mod.foo]",
+ True,
+ id="abs-ref-check-crossrefs-fail",
+ ),
+ pytest.param(
+ Module(name="mod", filepath=Path("mod."), docstring=Docstring("[foo][.foo]")),
+ {"better_crossrefs": True, "check_crossrefs": True},
+ True,
+ "[foo][mod.foo]",
+ False,
+ id="rel-ref-check-crossrefs-pass",
+ ),
+ pytest.param(
+ Module(name="mod", filepath=Path("mod."), docstring=Docstring("[foo][.foo]")),
+ {"better_crossrefs": True, "check_crossrefs": True},
+ False,
+ "[foo][mod.foo]",
+ True,
+ id="rel-ref-check-crossrefs-fail",
+ ),
+ pytest.param(
+ Module(name="mod", filepath=Path("mod."), docstring=Docstring("[foo][mod.foo]")),
+ {"better_crossrefs": False, "check_crossrefs": True},
+ False,
+ "[foo][mod.foo]",
+ False,
+ id="abs-ref-check-crossrefs-nofail-without-betterrefs",
+ ),
+ pytest.param(
+ Module(name="mod", filepath=Path("mod."), docstring=Docstring("[foo][.foo]")),
+ {"better_crossrefs": False, "check_crossrefs": True},
+ False,
+ "[foo][.foo]",
+ False,
+ id="rel-ref-check-crossrefs-nofail-without-betterrefs",
+ ),
+ pytest.param(
+ Module(name="mod", filepath=Path("mod."), docstring=Docstring("[foo][.foo] [bar][.]")),
+ {"better_crossrefs": True, "check_crossrefs": False},
+ True,
+ "[foo][mod.foo] [bar][mod.bar]",
+ False,
+ id="rel-refs-enabled-multiple",
+ ),
+ ],
+)
+def test_render(
+ griffe_obj: Object,
+ options: dict[str, Any],
+ checkref_result: bool,
+ expected_rendered: str,
+ expected_error: bool,
+ test_project: Path,
+ monkeypatch: pytest.MonkeyPatch,
+ caplog: pytest.LogCaptureFixture,
+) -> None:
+ """Test that the rendering was overwritten correctly.
- def fake_render(_self: PythonHandler, data: Object, _config: dict) -> str:
- assert data.docstring is not None
+ Note that the goal of this test isn't necessarily to test the better ref logic,
+ rather, it is to test that the handler is using this logic properly and applying
+ it accordingly to the passed options.
+ """
+ handler = PythonBetterRefsHandler(
+ config=PythonConfig(),
+ base_dir=test_project,
+ theme="material",
+ mdx=[],
+ mdx_config={},
+ )
+
+ # patch the render method of the parent class to only return the docstring,
+ # we don't need/want to see the full template here
+ def fake_render(_self: PythonHandler, data: CollectorItem, options: PythonOptions) -> str:
return data.docstring.value
- # Monkeypatch render/collect methods on parent class
- monkeypatch.setattr(PythonHandler, 'collect', fake_collect)
- monkeypatch.setattr(PythonHandler, 'render', fake_render)
-
- obj = Module(name='mod', filepath= Path('mod.py'))
- docstring = "[foo][.] [bar][bad.]"
- obj.docstring = Docstring(docstring, parent=obj)
-
- rendered = handler.render(obj, {})
- assert rendered == docstring
-
- rendered = handler.render(obj, dict(relative_crossrefs=False))
- assert rendered == docstring
-
- rendered = handler.render(obj, dict(relative_crossrefs=True))
- assert rendered == "[foo][mod.foo] [bar][bad.bar]"
- assert len(caplog.records) == 1
- _, level, msg = caplog.record_tuples[0]
- assert level == logging.WARNING
- assert "Cannot load reference 'bad.bar'" in msg
- caplog.clear()
-
- rendered = handler.render(obj, dict(relative_crossrefs=True, check_crossrefs=False))
- assert rendered == "[foo][mod.foo] [bar][bad.bar]"
- assert len(caplog.records) == 0
-
- rendered = handler.render(obj, dict(relative_crossrefs=True, check_crossrefs=False))
- assert rendered == "[foo][mod.foo] [bar][bad.bar]"
- assert len(caplog.records) == 0
-
- docstring = "\n\n[foo][bad.foo]"
- obj.docstring = Docstring(docstring, parent=obj)
- rendered = handler.render(obj, dict(relative_crossrefs=True))
- assert rendered == "[foo][bad.foo]"
- assert len(caplog.records) == 1
- _, level, msg = caplog.record_tuples[0]
- assert level == logging.WARNING
- assert "Cannot load reference 'bad.foo'" in msg
- caplog.clear()
-
- docstring = "[foo][?bad.foo] [bar][?bad.]"
- obj.docstring = Docstring(docstring, parent=obj)
- rendered = handler.render(obj, dict(relative_crossrefs=True, check_crossrefs=True))
- assert rendered == "[foo][bad.foo] [bar][bad.bar]"
- assert len(caplog.records) == 0
+ monkeypatch.setattr(PythonHandler, "render", fake_render)
+
+ # Patch check-ref according to the test parameters
+ monkeypatch.setattr(PythonBetterRefsHandler, "_check_ref", lambda self, ref: checkref_result)
+
+ # Test if rendering works as expected
+ rendered = handler.render(griffe_obj, PythonBetterRefsOptions.from_data(**options))
+ assert rendered == expected_rendered
+
+ # Check whether a log was produced (when a reference wasn't found)
+ if expected_error:
+ assert len(caplog.records) >= 1
+ assert "Cannot load reference" in caplog.messages[0]
+ else:
+ assert len(caplog.records) == 0
diff --git a/tests/test_integration.py b/tests/test_integration.py
index 05b82ed..9069396 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -1,117 +1,119 @@
-# Copyright (c) 2022-2024. Analog Devices Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""Integration test for python_xref handler"""
-
-import os
+"""Integration test for python_betterrefs handler."""
+
import re
import subprocess as sp
+from collections.abc import Mapping
from os import PathLike
from pathlib import Path
-from typing import Dict, List, Tuple
+from typing import cast
import bs4
-this_dir = Path(__file__).parent
-test_project_dir = this_dir.joinpath('project').absolute()
-test_project_mkdocs = test_project_dir.joinpath('mkdocs.yml')
-bar_src_file = test_project_dir.joinpath('src', 'myproj', 'bar.py')
+def run_test_project_mkdocs(test_project: Path, site_dir: Path) -> sp.CompletedProcess[str]:
+ """Run mkdocs command on a tiny sample project (contained in the same dir as this file)."""
+ mkdocs_cmd = [
+ "mkdocs",
+ "build",
+ "-f",
+ str(test_project / "mkdocs.yml"),
+ "-d",
+ str(site_dir),
+ ]
+ return sp.run(mkdocs_cmd, capture_output=True, encoding="utf8", check=False) # noqa: S603 # input is trusted
+
+def check_autorefs(html_file: Path, cases: Mapping[tuple[str, str], str]) -> None:
+ """Verify given HTML file contains all of the expected autorefs.
-def check_autorefs(autorefs: List[bs4.Tag], cases: Dict[Tuple[str,str],str] ) -> None:
- """
- Verify autorefs contain expected cases
+ Note:
+ If the HTML file contains some additional autorefs, an AssertionError will be raised.
- Arguments:
- autorefs: list of autoref tags parsed from HTML
- cases: mapping from (],) to generated reference tag
- where ,) to generated reference link ()
+ where is the qualified name of the object whose doc string
contains the cross-reference, and is the text in the cross-reference.
"""
- cases = cases.copy()
+ html = html_file.read_text()
+ soup = bs4.BeautifulSoup(html, "html.parser")
+ autorefs = soup.find_all("a", class_=["autorefs"])
+
+ cases = dict(cases)
for autoref in autorefs:
- curid = autoref.find_previous(id=True).attrs['id']
+ # This is for typing purposes only, the find_all filter shouldn't ever find non-tags
+ if not isinstance(autoref, bs4.Tag):
+ raise TypeError("Autorefs contained non-tag")
+
+ cur_id = cast(bs4.Tag, autoref.find_previous(id=True)).attrs["id"]
text = autoref.string
- href = autoref.attrs['href']
- expected_href = cases.get((curid, text))
+ href = autoref.attrs["href"]
+ expected_href = cases.get((cur_id, text)) # pyright: ignore[reportArgumentType]
+
if expected_href:
assert href == expected_href
- cases.pop((curid, text))
+ _ = cases.pop((cur_id, text)) # pyright: ignore[reportArgumentType]
+ else:
+ raise AssertionError(f"Skipping ref: {cur_id=},{text=} -> {href!r} ({autoref!s}")
assert len(cases) == 0
-def test_integration(tmpdir: PathLike) -> None:
- """An integration test that runs mkdocs on a tiny sample project and
- grovels the generated HTML to see that the links were resolved.
- """
-
- site_dir = Path(tmpdir).joinpath('site')
- mkdocs_cmd = [
- 'mkdocs',
- 'build',
- '-f',
- str(test_project_mkdocs),
- '-d',
- str(site_dir)
- ]
- result = sp.run(mkdocs_cmd, stdout=sp.PIPE, stderr=sp.PIPE, encoding='utf8', check=False)
-
- assert result.returncode == 0
+def test_integration(test_project: Path, tmpdir: PathLike[str]) -> None:
+ """An integration test that runs mkdocs on a tiny sample project.
+ This then grovels the generated HTML to see that the links were resolved.
+ """
+ site_dir = Path(tmpdir).joinpath("site")
+ result = run_test_project_mkdocs(test_project, site_dir)
+
+ # Make sure the command succeeded
+ try:
+ result.check_returncode()
+ except sp.CalledProcessError:
+ print(result.stderr) # noqa: T201
+ raise
+
+ # There is a single intentional bad reference in the bar.py file
+ # make sure it was reported (check_crossrefs).
m = re.search(
- r"WARNING.*file://(/.*/myproj/bar.py):(\d+):\s*\n\s*Cannot load reference '(.*)'",
- result.stderr
+ r"WARNING.*file://(.*[/\\]myproj[/\\]bar\.py):(\d+):\s*\n\s*Cannot load reference '(.*)'",
+ result.stderr,
)
- assert m is not None
- if os.path.sep == '/':
- assert m[1] == str(bar_src_file)
- assert m[3] == 'myproj.bar.bad'
- # Source location not accurate in python 3.7
- bad_line = int(m[2])
- bar_lines = bar_src_file.read_text().splitlines()
- assert '[bad]' in bar_lines[bad_line - 1]
+ assert m is not None, result.stderr
+ assert m[1] == str(test_project.joinpath("src", "myproj", "bar.py"))
+ assert m[2] == "3"
+ assert m[3] == "myproj.bar.bad"
- bar_html = site_dir.joinpath('bar', 'index.html').read_text()
- bar_bs = bs4.BeautifulSoup(bar_html, 'html.parser')
-
- autorefs: List[bs4.Tag] = bar_bs.find_all('a', attrs=['autorefs'])
- assert len(autorefs) >= 5
+ # The original error for invalid references from autorefs should still be present too
+ m = re.search(
+ (
+ r"WARNING.*mkdocs_autorefs: bar\.md: from (.*[/\\]myproj[/\\]bar.py):(\d+): \(myproj\.bar\) "
+ r"Could not find cross-reference target '(.*)'"
+ ),
+ result.stderr,
+ )
+ assert m is not None, result.stderr
+ assert m[1] == str(test_project.joinpath("src", "myproj", "bar.py"))
+ assert m[2] == "1" # line numbers aren't supported by mkdocs_autorefs, this is always 1
+ assert m[3] == "myproj.bar.bad"
+ # Verify the references (autorefs anchor tags) in the generated documentation HTML
check_autorefs(
- autorefs,
+ site_dir.joinpath("bar", "index.html"),
{
- ('myproj.bar.Bar', 'bar') : '#myproj.bar.Bar.bar',
- ('myproj.bar.Bar.bar' , 'Bar') : '#myproj.bar.Bar',
- ('myproj.bar.Bar.bar', 'foo') : '#myproj.bar.Bar.foo',
- ('myproj.bar.Bar.bar', 'func') : '#myproj.bar.func',
- ('myproj.bar.Bar.foo', 'Foo.foo') : '../foo/#myproj.foo.Foo.foo',
- ('myproj.bar.func', 'bar') : '#myproj.bar'
- }
+ ("myproj.bar.Bar", "Foo"): "../foo/#myproj.foo.Foo", # from bases (parent class)
+ ("myproj.bar.Bar", "bar"): "#myproj.bar.Bar.bar",
+ ("myproj.bar.Bar.bar", "Bar"): "#myproj.bar.Bar",
+ ("myproj.bar.Bar.bar", "foo"): "#myproj.bar.Bar.foo",
+ ("myproj.bar.Bar.bar", "func"): "#myproj.bar.func",
+ ("myproj.bar.Bar.foo", "Foo.foo"): "../foo/#myproj.foo.Foo.foo",
+ ("myproj.bar.func", "bar"): "#myproj.bar",
+ },
)
-
- baz_html = site_dir.joinpath('pkg-baz', 'index.html').read_text()
- baz_bs = bs4.BeautifulSoup(baz_html, 'html.parser')
-
- autorefs = baz_bs.find_all('a', attrs=['autorefs'])
- assert len(autorefs) >= 1
-
check_autorefs(
- autorefs,
+ site_dir.joinpath("pkg-baz", "index.html"),
{
- ('myproj.pkg.baz', 'func') : '../pkg/#myproj.pkg.func',
- }
+ ("myproj.pkg.baz", "func"): "../pkg/#myproj.pkg.func",
+ },
)
-
-
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..7e7b85e
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,1018 @@
+version = 1
+requires-python = ">=3.9"
+
+[[package]]
+name = "babel"
+version = "2.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 },
+]
+
+[[package]]
+name = "basedpyright"
+version = "1.26.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nodejs-wheel-binaries" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/18/c2/5685d040d4f2598788d42bfd2db5f808e9aa2eaee77fcae3c2fbe4ea0e7c/basedpyright-1.26.0.tar.gz", hash = "sha256:5e01f6eb9290a09ef39672106cf1a02924fdc8970e521838bc502ccf0676f32f", size = 24932771 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8e/72/65308f45bb73efc93075426cac5f37eea937ae364aa675785521cb3512c7/basedpyright-1.26.0-py3-none-any.whl", hash = "sha256:5a6a17f2c389ec313dd2c3644f40e8221bc90252164802e626055341c0a37381", size = 11504579 },
+]
+
+[[package]]
+name = "beautifulsoup4"
+version = "4.13.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "soupsieve" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f0/3c/adaf39ce1fb4afdd21b611e3d530b183bb7759c9b673d60db0e347fd4439/beautifulsoup4-4.13.3.tar.gz", hash = "sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b", size = 619516 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f9/49/6abb616eb3cbab6a7cca303dc02fdf3836de2e0b834bf966a7f5271a34d8/beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16", size = 186015 },
+]
+
+[[package]]
+name = "certifi"
+version = "2025.1.31"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
+]
+
+[[package]]
+name = "cfgv"
+version = "3.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013 },
+ { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285 },
+ { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449 },
+ { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892 },
+ { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123 },
+ { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943 },
+ { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063 },
+ { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578 },
+ { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629 },
+ { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778 },
+ { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453 },
+ { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479 },
+ { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790 },
+ { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 },
+ { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 },
+ { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 },
+ { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 },
+ { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 },
+ { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 },
+ { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 },
+ { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 },
+ { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 },
+ { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 },
+ { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 },
+ { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 },
+ { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 },
+ { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 },
+ { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 },
+ { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 },
+ { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 },
+ { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 },
+ { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 },
+ { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 },
+ { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 },
+ { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 },
+ { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 },
+ { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 },
+ { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 },
+ { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 },
+ { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 },
+ { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 },
+ { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 },
+ { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 },
+ { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 },
+ { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 },
+ { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 },
+ { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 },
+ { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 },
+ { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 },
+ { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 },
+ { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 },
+ { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 },
+ { url = "https://files.pythonhosted.org/packages/7f/c0/b913f8f02836ed9ab32ea643c6fe4d3325c3d8627cf6e78098671cafff86/charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", size = 197867 },
+ { url = "https://files.pythonhosted.org/packages/0f/6c/2bee440303d705b6fb1e2ec789543edec83d32d258299b16eed28aad48e0/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", size = 141385 },
+ { url = "https://files.pythonhosted.org/packages/3d/04/cb42585f07f6f9fd3219ffb6f37d5a39b4fd2db2355b23683060029c35f7/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", size = 151367 },
+ { url = "https://files.pythonhosted.org/packages/54/54/2412a5b093acb17f0222de007cc129ec0e0df198b5ad2ce5699355269dfe/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", size = 143928 },
+ { url = "https://files.pythonhosted.org/packages/5a/6d/e2773862b043dcf8a221342954f375392bb2ce6487bcd9f2c1b34e1d6781/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", size = 146203 },
+ { url = "https://files.pythonhosted.org/packages/b9/f8/ca440ef60d8f8916022859885f231abb07ada3c347c03d63f283bec32ef5/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", size = 148082 },
+ { url = "https://files.pythonhosted.org/packages/04/d2/42fd330901aaa4b805a1097856c2edf5095e260a597f65def493f4b8c833/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", size = 142053 },
+ { url = "https://files.pythonhosted.org/packages/9e/af/3a97a4fa3c53586f1910dadfc916e9c4f35eeada36de4108f5096cb7215f/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", size = 150625 },
+ { url = "https://files.pythonhosted.org/packages/26/ae/23d6041322a3556e4da139663d02fb1b3c59a23ab2e2b56432bd2ad63ded/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", size = 153549 },
+ { url = "https://files.pythonhosted.org/packages/94/22/b8f2081c6a77cb20d97e57e0b385b481887aa08019d2459dc2858ed64871/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", size = 150945 },
+ { url = "https://files.pythonhosted.org/packages/c7/0b/c5ec5092747f801b8b093cdf5610e732b809d6cb11f4c51e35fc28d1d389/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", size = 146595 },
+ { url = "https://files.pythonhosted.org/packages/0c/5a/0b59704c38470df6768aa154cc87b1ac7c9bb687990a1559dc8765e8627e/charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", size = 95453 },
+ { url = "https://files.pythonhosted.org/packages/85/2d/a9790237cb4d01a6d57afadc8573c8b73c609ade20b80f4cda30802009ee/charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", size = 102811 },
+ { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 },
+]
+
+[[package]]
+name = "click"
+version = "8.1.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
+]
+
+[[package]]
+name = "distlib"
+version = "0.3.9"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.2.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 },
+]
+
+[[package]]
+name = "filelock"
+version = "3.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/dc/9c/0b15fb47b464e1b663b1acd1253a062aa5feecb07d4e597daea542ebd2b5/filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e", size = 18027 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/89/ec/00d68c4ddfedfe64159999e5f8a98fb8442729a63e2077eb9dcd89623d27/filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338", size = 16164 },
+]
+
+[[package]]
+name = "ghp-import"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "python-dateutil" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034 },
+]
+
+[[package]]
+name = "griffe"
+version = "1.5.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/59/80/13b6456bfbf8bc854875e58d3a3bad297ee19ebdd693ce62a10fab007e7a/griffe-1.5.7.tar.gz", hash = "sha256:465238c86deaf1137761f700fb343edd8ffc846d72f6de43c3c345ccdfbebe92", size = 391503 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/67/b43330ed76f96be098c165338d47ccb952964ed77ba1d075247fbdf05c04/griffe-1.5.7-py3-none-any.whl", hash = "sha256:4af8ec834b64de954d447c7b6672426bb145e71605c74a4e22d510cc79fe7d8b", size = 128294 },
+]
+
+[[package]]
+name = "identify"
+version = "2.6.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/bf/c68c46601bacd4c6fb4dd751a42b6e7087240eaabc6487f2ef7a48e0e8fc/identify-2.6.6.tar.gz", hash = "sha256:7bec12768ed44ea4761efb47806f0a41f86e7c0a5fdf5950d4648c90eca7e251", size = 99217 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/74/a1/68a395c17eeefb04917034bd0a1bfa765e7654fa150cca473d669aa3afb5/identify-2.6.6-py2.py3-none-any.whl", hash = "sha256:cbd1810bce79f8b671ecb20f53ee0ae8e86ae84b557de31d89709dc2a48ba881", size = 99083 },
+]
+
+[[package]]
+name = "idna"
+version = "3.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
+]
+
+[[package]]
+name = "importlib-metadata"
+version = "8.6.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "zipp" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/33/08/c1395a292bb23fd03bdf572a1357c5a733d3eecbab877641ceacab23db6e/importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580", size = 55767 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e", size = 26971 },
+]
+
+[[package]]
+name = "importlib-resources"
+version = "6.5.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "zipp", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461 },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
+]
+
+[[package]]
+name = "jinja2"
+version = "3.1.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/af/92/b3130cbbf5591acf9ade8708c365f3238046ac7cb8ccba6e81abccb0ccff/jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", size = 244674 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 },
+]
+
+[[package]]
+name = "markdown"
+version = "3.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "importlib-metadata", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349 },
+]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 },
+ { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 },
+ { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 },
+ { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 },
+ { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 },
+ { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 },
+ { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 },
+ { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 },
+ { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 },
+ { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 },
+ { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 },
+ { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 },
+ { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 },
+ { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 },
+ { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 },
+ { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 },
+ { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 },
+ { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 },
+ { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 },
+ { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 },
+ { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 },
+ { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 },
+ { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 },
+ { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 },
+ { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 },
+ { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 },
+ { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 },
+ { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 },
+ { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 },
+ { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 },
+ { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 },
+ { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 },
+ { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 },
+ { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 },
+ { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 },
+ { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 },
+ { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 },
+ { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 },
+ { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 },
+ { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 },
+ { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 },
+ { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 },
+ { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 },
+ { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 },
+ { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 },
+ { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 },
+ { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 },
+ { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 },
+ { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 },
+ { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 },
+ { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344 },
+ { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389 },
+ { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607 },
+ { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728 },
+ { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826 },
+ { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843 },
+ { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219 },
+ { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946 },
+ { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063 },
+ { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506 },
+]
+
+[[package]]
+name = "mergedeep"
+version = "1.3.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354 },
+]
+
+[[package]]
+name = "mike"
+version = "2.1.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "importlib-metadata" },
+ { name = "importlib-resources" },
+ { name = "jinja2" },
+ { name = "mkdocs" },
+ { name = "pyparsing" },
+ { name = "pyyaml" },
+ { name = "pyyaml-env-tag" },
+ { name = "verspec" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ab/f7/2933f1a1fb0e0f077d5d6a92c6c7f8a54e6128241f116dff4df8b6050bbf/mike-2.1.3.tar.gz", hash = "sha256:abd79b8ea483fb0275b7972825d3082e5ae67a41820f8d8a0dc7a3f49944e810", size = 38119 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fd/1a/31b7cd6e4e7a02df4e076162e9783620777592bea9e4bb036389389af99d/mike-2.1.3-py3-none-any.whl", hash = "sha256:d90c64077e84f06272437b464735130d380703a76a5738b152932884c60c062a", size = 33754 },
+]
+
+[[package]]
+name = "mkdocs"
+version = "1.6.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "ghp-import" },
+ { name = "importlib-metadata", marker = "python_full_version < '3.10'" },
+ { name = "jinja2" },
+ { name = "markdown" },
+ { name = "markupsafe" },
+ { name = "mergedeep" },
+ { name = "mkdocs-get-deps" },
+ { name = "packaging" },
+ { name = "pathspec" },
+ { name = "pyyaml" },
+ { name = "pyyaml-env-tag" },
+ { name = "watchdog" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451 },
+]
+
+[[package]]
+name = "mkdocs-autorefs"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown" },
+ { name = "markupsafe" },
+ { name = "mkdocs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fe/18/fb1e17fb705228b51bf7b2f791adaf83c0fa708e51bbc003411ba48ae21e/mkdocs_autorefs-1.3.0.tar.gz", hash = "sha256:6867764c099ace9025d6ac24fd07b85a98335fbd30107ef01053697c8f46db61", size = 42597 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/4a/960c441950f98becfa5dd419adab20274939fd575ab848aee2c87e3599ac/mkdocs_autorefs-1.3.0-py3-none-any.whl", hash = "sha256:d180f9778a04e78b7134e31418f238bba56f56d6a8af97873946ff661befffb3", size = 17642 },
+]
+
+[[package]]
+name = "mkdocs-get-deps"
+version = "0.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "importlib-metadata", marker = "python_full_version < '3.10'" },
+ { name = "mergedeep" },
+ { name = "platformdirs" },
+ { name = "pyyaml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521 },
+]
+
+[[package]]
+name = "mkdocs-material"
+version = "9.6.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "babel" },
+ { name = "colorama" },
+ { name = "jinja2" },
+ { name = "markdown" },
+ { name = "mkdocs" },
+ { name = "mkdocs-material-extensions" },
+ { name = "paginate" },
+ { name = "pygments" },
+ { name = "pymdown-extensions" },
+ { name = "regex" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0f/1e/65b4fda4debf5e337b2ad4e692423dba4f5c77f49c4dee170c47a7dbac25/mkdocs_material-9.6.3.tar.gz", hash = "sha256:c87f7d1c39ce6326da5e10e232aed51bae46252e646755900f4b0fc9192fa832", size = 3942608 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/11/a4/e0da0bc6a7dbfda6a786427f82a0caa4dd1f163249a5a5e5dccbb50c5f1e/mkdocs_material-9.6.3-py3-none-any.whl", hash = "sha256:1125622067e26940806701219303b27c0933e04533560725d97ec26fd16a39cf", size = 8688709 },
+]
+
+[[package]]
+name = "mkdocs-material-extensions"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728 },
+]
+
+[[package]]
+name = "mkdocstrings"
+version = "0.28.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "importlib-metadata", marker = "python_full_version < '3.10'" },
+ { name = "jinja2" },
+ { name = "markdown" },
+ { name = "markupsafe" },
+ { name = "mkdocs" },
+ { name = "mkdocs-autorefs" },
+ { name = "mkdocs-get-deps" },
+ { name = "pymdown-extensions" },
+ { name = "typing-extensions", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/86/4b/70522427768a4637ffac376140f362dc3d159364fb64e698667e51053d57/mkdocstrings-0.28.0.tar.gz", hash = "sha256:df20afef1eafe36ba466ae20732509ecb74237653a585f5061937e54b553b4e0", size = 3392797 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/75/c3/e5a319d4de0867c1b59ff22abb93bf898f9812e934ab75dcf7fe94e85bb6/mkdocstrings-0.28.0-py3-none-any.whl", hash = "sha256:84cf3dc910614781fe0fee46ce8006fde7df6cc7cca2e3f799895fb8a9170b39", size = 4700952 },
+]
+
+[[package]]
+name = "mkdocstrings-python"
+version = "1.14.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "griffe" },
+ { name = "mkdocs-autorefs" },
+ { name = "mkdocstrings" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/86/09/1f14b7e6ca02115e1491f425147d2f218301099db0699a1b40914a125d6e/mkdocstrings_python-1.14.7.tar.gz", hash = "sha256:35100ea5545a9b42155da73de8be74484216031e912feff7a4f6115f206139c7", size = 422162 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d5/54/46a60fd1e4a696d19e9a47b3eddc025e999edc43ff5cffcc0c84121b6428/mkdocstrings_python-1.14.7-py3-none-any.whl", hash = "sha256:bdcce5544cc2fbee4163a1cf14e218de688849e75ca46c3ece4a28825aac9b41", size = 448699 },
+]
+
+[[package]]
+name = "mkdocstrings-python-betterrefs"
+version = "1.0.0"
+source = { editable = "." }
+dependencies = [
+ { name = "griffe" },
+ { name = "mkdocstrings-python" },
+ { name = "typing-extensions" },
+]
+
+[package.dev-dependencies]
+docs = [
+ { name = "mike" },
+ { name = "mkdocs-material" },
+]
+lint = [
+ { name = "basedpyright" },
+ { name = "pre-commit" },
+ { name = "ruff" },
+]
+test = [
+ { name = "beautifulsoup4" },
+ { name = "pytest" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "griffe", specifier = ">=1.0.0" },
+ { name = "mkdocstrings-python", specifier = ">=1.14.1,<1.15.0" },
+ { name = "typing-extensions", specifier = ">=4.0" },
+]
+
+[package.metadata.requires-dev]
+docs = [
+ { name = "mike", specifier = ">=2.1.2" },
+ { name = "mkdocs-material", specifier = ">=9.5.30" },
+]
+lint = [
+ { name = "basedpyright", specifier = ">=1.13.3" },
+ { name = "pre-commit", specifier = ">=3.3.3" },
+ { name = "ruff", specifier = ">=0.9.2" },
+]
+test = [
+ { name = "beautifulsoup4", specifier = ">=4.13.3" },
+ { name = "pytest", specifier = ">=8.3.4" },
+]
+
+[[package]]
+name = "nodeenv"
+version = "1.9.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 },
+]
+
+[[package]]
+name = "nodejs-wheel-binaries"
+version = "22.13.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5d/c5/1af2fc54fcc18f4a99426b46f18832a04f755ee340019e1be536187c1e1c/nodejs_wheel_binaries-22.13.1.tar.gz", hash = "sha256:a0c15213c9c3383541be4400a30959883868ce5da9cebb3d63ddc7fe61459308", size = 8053 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7c/e9/b0dd118e0fd4eabe1ec9c3d9a68df4d811282e8837b811d804f23742e117/nodejs_wheel_binaries-22.13.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:e4f64d0e26600d51cbdd98a6718a19c2d1b8c7538e9e353e95a634a06a8e1a58", size = 51015650 },
+ { url = "https://files.pythonhosted.org/packages/cc/a6/9ba835f5d4f3f6b1f01191e7ac0874871f9743de5c42a5a9a54e67c2e2a6/nodejs_wheel_binaries-22.13.1-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:afcb40484bb02f23137f838014724604ae183fd767b30da95b0be1510a40c06d", size = 51814957 },
+ { url = "https://files.pythonhosted.org/packages/0d/2e/a430207e5f22bd3dcffb81acbddf57ee4108b9e2b0f99a5578dc2c1ff7fc/nodejs_wheel_binaries-22.13.1-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fc88c98eebabfc36b5270a4ab974a2682746931567ca76a5ca49c54482bbb51", size = 57148437 },
+ { url = "https://files.pythonhosted.org/packages/97/f4/5731b6f0c8af434619b4f1b8fd895bc33fca60168cd68133e52841872114/nodejs_wheel_binaries-22.13.1-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b9f75ea8f5e3e5416256fcb00a98cbe14c8d3b6dcaf17da29c4ade5723026d8", size = 57634451 },
+ { url = "https://files.pythonhosted.org/packages/49/28/83166f7e39812e9ef99cfa3e722c54e32dd9de6a1290f3216c2e5d1f4957/nodejs_wheel_binaries-22.13.1-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:94608702ef6c389d32e89ff3b7a925cb5dedaf55b5d98bd0c4fb3450a8b6d1c1", size = 58794510 },
+ { url = "https://files.pythonhosted.org/packages/f7/64/4832ec26d0a7ca7a5574df265d85c6832f9a624024511fc34958227ad740/nodejs_wheel_binaries-22.13.1-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:53a40d0269689aa2eaf2e261cbe5ec256644bc56aae0201ef344b7d8f40ccc79", size = 59738596 },
+ { url = "https://files.pythonhosted.org/packages/18/cd/def29615dac250cda3d141e1c03b7153b9a027360bde0272a6768c5fae33/nodejs_wheel_binaries-22.13.1-py2.py3-none-win_amd64.whl", hash = "sha256:549371a929a29fbce8d0ab8f1b5410549946d4f1b0376a5ce635b45f6d05298f", size = 40455444 },
+ { url = "https://files.pythonhosted.org/packages/15/d7/6de2bc615203bf590ca437a5cac145b2f86d994ce329489125a0a90ba715/nodejs_wheel_binaries-22.13.1-py2.py3-none-win_arm64.whl", hash = "sha256:cf72d50d755f4e5c0709b0449de01768d96b3b1ec7aa531561415b88f179ad8b", size = 36200929 },
+]
+
+[[package]]
+name = "packaging"
+version = "24.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
+]
+
+[[package]]
+name = "paginate"
+version = "0.5.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746 },
+]
+
+[[package]]
+name = "pathspec"
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.3.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
+]
+
+[[package]]
+name = "pre-commit"
+version = "4.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cfgv" },
+ { name = "identify" },
+ { name = "nodeenv" },
+ { name = "pyyaml" },
+ { name = "virtualenv" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2a/13/b62d075317d8686071eb843f0bb1f195eb332f48869d3c31a4c6f1e063ac/pre_commit-4.1.0.tar.gz", hash = "sha256:ae3f018575a588e30dfddfab9a05448bfbd6b73d78709617b5a2b853549716d4", size = 193330 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/43/b3/df14c580d82b9627d173ceea305ba898dca135feb360b6d84019d0803d3b/pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b", size = 220560 },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
+]
+
+[[package]]
+name = "pymdown-extensions"
+version = "10.14.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown" },
+ { name = "pyyaml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7c/44/e6de2fdc880ad0ec7547ca2e087212be815efbc9a425a8d5ba9ede602cbb/pymdown_extensions-10.14.3.tar.gz", hash = "sha256:41e576ce3f5d650be59e900e4ceff231e0aed2a88cf30acaee41e02f063a061b", size = 846846 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/eb/f5/b9e2a42aa8f9e34d52d66de87941ecd236570c7ed2e87775ed23bbe4e224/pymdown_extensions-10.14.3-py3-none-any.whl", hash = "sha256:05e0bee73d64b9c71a4ae17c72abc2f700e8bc8403755a00580b49a4e9f189e9", size = 264467 },
+]
+
+[[package]]
+name = "pyparsing"
+version = "3.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8b/1a/3544f4f299a47911c2ab3710f534e52fea62a633c96806995da5d25be4b2/pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a", size = 1067694 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1c/a7/c8a2d361bf89c0d9577c934ebb7421b25dc84bf3a8e3ac0a40aed9acc547/pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1", size = 107716 },
+]
+
+[[package]]
+name = "pytest"
+version = "8.3.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 },
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 },
+ { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 },
+ { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 },
+ { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 },
+ { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 },
+ { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 },
+ { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 },
+ { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 },
+ { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 },
+ { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 },
+ { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 },
+ { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 },
+ { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 },
+ { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 },
+ { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 },
+ { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 },
+ { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 },
+ { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 },
+ { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 },
+ { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 },
+ { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 },
+ { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 },
+ { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 },
+ { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 },
+ { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 },
+ { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 },
+ { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 },
+ { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 },
+ { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 },
+ { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 },
+ { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 },
+ { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 },
+ { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 },
+ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 },
+ { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 },
+ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
+ { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777 },
+ { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318 },
+ { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891 },
+ { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614 },
+ { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360 },
+ { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006 },
+ { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577 },
+ { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593 },
+ { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312 },
+]
+
+[[package]]
+name = "pyyaml-env-tag"
+version = "0.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyyaml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911 },
+]
+
+[[package]]
+name = "regex"
+version = "2024.11.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/95/3c/4651f6b130c6842a8f3df82461a8950f923925db8b6961063e82744bddcc/regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91", size = 482674 },
+ { url = "https://files.pythonhosted.org/packages/15/51/9f35d12da8434b489c7b7bffc205c474a0a9432a889457026e9bc06a297a/regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0", size = 287684 },
+ { url = "https://files.pythonhosted.org/packages/bd/18/b731f5510d1b8fb63c6b6d3484bfa9a59b84cc578ac8b5172970e05ae07c/regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e", size = 284589 },
+ { url = "https://files.pythonhosted.org/packages/78/a2/6dd36e16341ab95e4c6073426561b9bfdeb1a9c9b63ab1b579c2e96cb105/regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde", size = 782511 },
+ { url = "https://files.pythonhosted.org/packages/1b/2b/323e72d5d2fd8de0d9baa443e1ed70363ed7e7b2fb526f5950c5cb99c364/regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e", size = 821149 },
+ { url = "https://files.pythonhosted.org/packages/90/30/63373b9ea468fbef8a907fd273e5c329b8c9535fee36fc8dba5fecac475d/regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2", size = 809707 },
+ { url = "https://files.pythonhosted.org/packages/f2/98/26d3830875b53071f1f0ae6d547f1d98e964dd29ad35cbf94439120bb67a/regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf", size = 781702 },
+ { url = "https://files.pythonhosted.org/packages/87/55/eb2a068334274db86208ab9d5599ffa63631b9f0f67ed70ea7c82a69bbc8/regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c", size = 771976 },
+ { url = "https://files.pythonhosted.org/packages/74/c0/be707bcfe98254d8f9d2cff55d216e946f4ea48ad2fd8cf1428f8c5332ba/regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86", size = 697397 },
+ { url = "https://files.pythonhosted.org/packages/49/dc/bb45572ceb49e0f6509f7596e4ba7031f6819ecb26bc7610979af5a77f45/regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67", size = 768726 },
+ { url = "https://files.pythonhosted.org/packages/5a/db/f43fd75dc4c0c2d96d0881967897926942e935d700863666f3c844a72ce6/regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d", size = 775098 },
+ { url = "https://files.pythonhosted.org/packages/99/d7/f94154db29ab5a89d69ff893159b19ada89e76b915c1293e98603d39838c/regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2", size = 839325 },
+ { url = "https://files.pythonhosted.org/packages/f7/17/3cbfab1f23356fbbf07708220ab438a7efa1e0f34195bf857433f79f1788/regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008", size = 843277 },
+ { url = "https://files.pythonhosted.org/packages/7e/f2/48b393b51900456155de3ad001900f94298965e1cad1c772b87f9cfea011/regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62", size = 773197 },
+ { url = "https://files.pythonhosted.org/packages/45/3f/ef9589aba93e084cd3f8471fded352826dcae8489b650d0b9b27bc5bba8a/regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e", size = 261714 },
+ { url = "https://files.pythonhosted.org/packages/42/7e/5f1b92c8468290c465fd50c5318da64319133231415a8aa6ea5ab995a815/regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519", size = 274042 },
+ { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669 },
+ { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684 },
+ { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589 },
+ { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121 },
+ { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275 },
+ { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257 },
+ { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727 },
+ { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667 },
+ { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963 },
+ { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700 },
+ { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592 },
+ { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929 },
+ { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213 },
+ { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734 },
+ { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052 },
+ { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 },
+ { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 },
+ { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 },
+ { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 },
+ { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 },
+ { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 },
+ { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 },
+ { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 },
+ { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 },
+ { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 },
+ { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 },
+ { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 },
+ { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 },
+ { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 },
+ { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 },
+ { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 },
+ { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 },
+ { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 },
+ { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 },
+ { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 },
+ { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 },
+ { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 },
+ { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 },
+ { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 },
+ { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 },
+ { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 },
+ { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 },
+ { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 },
+ { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 },
+ { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 },
+ { url = "https://files.pythonhosted.org/packages/89/23/c4a86df398e57e26f93b13ae63acce58771e04bdde86092502496fa57f9c/regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839", size = 482682 },
+ { url = "https://files.pythonhosted.org/packages/3c/8b/45c24ab7a51a1658441b961b86209c43e6bb9d39caf1e63f46ce6ea03bc7/regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e", size = 287679 },
+ { url = "https://files.pythonhosted.org/packages/7a/d1/598de10b17fdafc452d11f7dada11c3be4e379a8671393e4e3da3c4070df/regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf", size = 284578 },
+ { url = "https://files.pythonhosted.org/packages/49/70/c7eaa219efa67a215846766fde18d92d54cb590b6a04ffe43cef30057622/regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b", size = 782012 },
+ { url = "https://files.pythonhosted.org/packages/89/e5/ef52c7eb117dd20ff1697968219971d052138965a4d3d9b95e92e549f505/regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0", size = 820580 },
+ { url = "https://files.pythonhosted.org/packages/5f/3f/9f5da81aff1d4167ac52711acf789df13e789fe6ac9545552e49138e3282/regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b", size = 809110 },
+ { url = "https://files.pythonhosted.org/packages/86/44/2101cc0890c3621b90365c9ee8d7291a597c0722ad66eccd6ffa7f1bcc09/regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef", size = 780919 },
+ { url = "https://files.pythonhosted.org/packages/ce/2e/3e0668d8d1c7c3c0d397bf54d92fc182575b3a26939aed5000d3cc78760f/regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48", size = 771515 },
+ { url = "https://files.pythonhosted.org/packages/a6/49/1bc4584254355e3dba930a3a2fd7ad26ccba3ebbab7d9100db0aff2eedb0/regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13", size = 696957 },
+ { url = "https://files.pythonhosted.org/packages/c8/dd/42879c1fc8a37a887cd08e358af3d3ba9e23038cd77c7fe044a86d9450ba/regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2", size = 768088 },
+ { url = "https://files.pythonhosted.org/packages/89/96/c05a0fe173cd2acd29d5e13c1adad8b706bcaa71b169e1ee57dcf2e74584/regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95", size = 774752 },
+ { url = "https://files.pythonhosted.org/packages/b5/f3/a757748066255f97f14506483436c5f6aded7af9e37bca04ec30c90ca683/regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9", size = 838862 },
+ { url = "https://files.pythonhosted.org/packages/5c/93/c6d2092fd479dcaeea40fc8fa673822829181ded77d294a7f950f1dda6e2/regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f", size = 842622 },
+ { url = "https://files.pythonhosted.org/packages/ff/9c/daa99532c72f25051a90ef90e1413a8d54413a9e64614d9095b0c1c154d0/regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b", size = 772713 },
+ { url = "https://files.pythonhosted.org/packages/13/5d/61a533ccb8c231b474ac8e3a7d70155b00dfc61af6cafdccd1947df6d735/regex-2024.11.6-cp39-cp39-win32.whl", hash = "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57", size = 261756 },
+ { url = "https://files.pythonhosted.org/packages/dc/7b/e59b7f7c91ae110d154370c24133f947262525b5d6406df65f23422acc17/regex-2024.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983", size = 274110 },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
+]
+
+[[package]]
+name = "ruff"
+version = "0.9.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/02/74/6c359f6b9ed85b88df6ef31febce18faeb852f6c9855651dfb1184a46845/ruff-0.9.5.tar.gz", hash = "sha256:11aecd7a633932875ab3cb05a484c99970b9d52606ce9ea912b690b02653d56c", size = 3634177 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/17/4b/82b7c9ac874e72b82b19fd7eab57d122e2df44d2478d90825854f9232d02/ruff-0.9.5-py3-none-linux_armv6l.whl", hash = "sha256:d466d2abc05f39018d53f681fa1c0ffe9570e6d73cde1b65d23bb557c846f442", size = 11681264 },
+ { url = "https://files.pythonhosted.org/packages/27/5c/f5ae0a9564e04108c132e1139d60491c0abc621397fe79a50b3dc0bd704b/ruff-0.9.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38840dbcef63948657fa7605ca363194d2fe8c26ce8f9ae12eee7f098c85ac8a", size = 11657554 },
+ { url = "https://files.pythonhosted.org/packages/2a/83/c6926fa3ccb97cdb3c438bb56a490b395770c750bf59f9bc1fe57ae88264/ruff-0.9.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d56ba06da53536b575fbd2b56517f6f95774ff7be0f62c80b9e67430391eeb36", size = 11088959 },
+ { url = "https://files.pythonhosted.org/packages/af/a7/42d1832b752fe969ffdbfcb1b4cb477cb271bed5835110fb0a16ef31ab81/ruff-0.9.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f7cb2a01da08244c50b20ccfaeb5972e4228c3c3a1989d3ece2bc4b1f996001", size = 11902041 },
+ { url = "https://files.pythonhosted.org/packages/53/cf/1fffa09fb518d646f560ccfba59f91b23c731e461d6a4dedd21a393a1ff1/ruff-0.9.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:96d5c76358419bc63a671caac70c18732d4fd0341646ecd01641ddda5c39ca0b", size = 11421069 },
+ { url = "https://files.pythonhosted.org/packages/09/27/bb8f1b7304e2a9431f631ae7eadc35550fe0cf620a2a6a0fc4aa3d736f94/ruff-0.9.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:deb8304636ed394211f3a6d46c0e7d9535b016f53adaa8340139859b2359a070", size = 12625095 },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/ab00bc9d3df35a5f1b64f5117458160a009f93ae5caf65894ebb63a1842d/ruff-0.9.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:df455000bf59e62b3e8c7ba5ed88a4a2bc64896f900f311dc23ff2dc38156440", size = 13257797 },
+ { url = "https://files.pythonhosted.org/packages/88/81/c639a082ae6d8392bc52256058ec60f493c6a4d06d5505bccface3767e61/ruff-0.9.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de92170dfa50c32a2b8206a647949590e752aca8100a0f6b8cefa02ae29dce80", size = 12763793 },
+ { url = "https://files.pythonhosted.org/packages/b3/d0/0a3d8f56d1e49af466dc770eeec5c125977ba9479af92e484b5b0251ce9c/ruff-0.9.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d28532d73b1f3f627ba88e1456f50748b37f3a345d2be76e4c653bec6c3e393", size = 14386234 },
+ { url = "https://files.pythonhosted.org/packages/04/70/e59c192a3ad476355e7f45fb3a87326f5219cc7c472e6b040c6c6595c8f0/ruff-0.9.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c746d7d1df64f31d90503ece5cc34d7007c06751a7a3bbeee10e5f2463d52d2", size = 12437505 },
+ { url = "https://files.pythonhosted.org/packages/55/4e/3abba60a259d79c391713e7a6ccabf7e2c96e5e0a19100bc4204f1a43a51/ruff-0.9.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:11417521d6f2d121fda376f0d2169fb529976c544d653d1d6044f4c5562516ee", size = 11884799 },
+ { url = "https://files.pythonhosted.org/packages/a3/db/b0183a01a9f25b4efcae919c18fb41d32f985676c917008620ad692b9d5f/ruff-0.9.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b9d71c3879eb32de700f2f6fac3d46566f644a91d3130119a6378f9312a38e1", size = 11527411 },
+ { url = "https://files.pythonhosted.org/packages/0a/e4/3ebfcebca3dff1559a74c6becff76e0b64689cea02b7aab15b8b32ea245d/ruff-0.9.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2e36c61145e70febcb78483903c43444c6b9d40f6d2f800b5552fec6e4a7bb9a", size = 12078868 },
+ { url = "https://files.pythonhosted.org/packages/ec/b2/5ab808833e06c0a1b0d046a51c06ec5687b73c78b116e8d77687dc0cd515/ruff-0.9.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2f71d09aeba026c922aa7aa19a08d7bd27c867aedb2f74285a2639644c1c12f5", size = 12524374 },
+ { url = "https://files.pythonhosted.org/packages/e0/51/1432afcc3b7aa6586c480142caae5323d59750925c3559688f2a9867343f/ruff-0.9.5-py3-none-win32.whl", hash = "sha256:134f958d52aa6fdec3b294b8ebe2320a950d10c041473c4316d2e7d7c2544723", size = 9853682 },
+ { url = "https://files.pythonhosted.org/packages/b7/ad/c7a900591bd152bb47fc4882a27654ea55c7973e6d5d6396298ad3fd6638/ruff-0.9.5-py3-none-win_amd64.whl", hash = "sha256:78cc6067f6d80b6745b67498fb84e87d32c6fc34992b52bffefbdae3442967d6", size = 10865744 },
+ { url = "https://files.pythonhosted.org/packages/75/d9/fde7610abd53c0c76b6af72fc679cb377b27c617ba704e25da834e0a0608/ruff-0.9.5-py3-none-win_arm64.whl", hash = "sha256:18a29f1a005bddb229e580795627d297dfa99f16b30c7039e73278cf6b5f9fa9", size = 10064595 },
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
+]
+
+[[package]]
+name = "soupsieve"
+version = "2.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 },
+]
+
+[[package]]
+name = "tomli"
+version = "2.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 },
+ { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 },
+ { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 },
+ { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 },
+ { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 },
+ { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 },
+ { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 },
+ { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 },
+ { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 },
+ { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 },
+ { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 },
+ { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 },
+ { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 },
+ { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 },
+ { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 },
+ { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 },
+ { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 },
+ { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 },
+ { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 },
+ { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 },
+ { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 },
+ { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 },
+ { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 },
+ { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 },
+ { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 },
+ { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 },
+ { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 },
+ { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 },
+ { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 },
+ { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 },
+ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.12.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 },
+]
+
+[[package]]
+name = "verspec"
+version = "0.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/44/8126f9f0c44319b2efc65feaad589cadef4d77ece200ae3c9133d58464d0/verspec-0.1.0.tar.gz", hash = "sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e", size = 27123 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31", size = 19640 },
+]
+
+[[package]]
+name = "virtualenv"
+version = "20.29.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "distlib" },
+ { name = "filelock" },
+ { name = "platformdirs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a7/ca/f23dcb02e161a9bba141b1c08aa50e8da6ea25e6d780528f1d385a3efe25/virtualenv-20.29.1.tar.gz", hash = "sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35", size = 7658028 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/89/9b/599bcfc7064fbe5740919e78c5df18e5dceb0887e676256a1061bb5ae232/virtualenv-20.29.1-py3-none-any.whl", hash = "sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779", size = 4282379 },
+]
+
+[[package]]
+name = "watchdog"
+version = "6.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390 },
+ { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389 },
+ { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020 },
+ { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393 },
+ { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392 },
+ { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019 },
+ { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471 },
+ { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449 },
+ { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054 },
+ { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480 },
+ { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451 },
+ { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057 },
+ { url = "https://files.pythonhosted.org/packages/05/52/7223011bb760fce8ddc53416beb65b83a3ea6d7d13738dde75eeb2c89679/watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8", size = 96390 },
+ { url = "https://files.pythonhosted.org/packages/9c/62/d2b21bc4e706d3a9d467561f487c2938cbd881c69f3808c43ac1ec242391/watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a", size = 88386 },
+ { url = "https://files.pythonhosted.org/packages/ea/22/1c90b20eda9f4132e4603a26296108728a8bfe9584b006bd05dd94548853/watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c", size = 89017 },
+ { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902 },
+ { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380 },
+ { url = "https://files.pythonhosted.org/packages/5b/79/69f2b0e8d3f2afd462029031baafb1b75d11bb62703f0e1022b2e54d49ee/watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa", size = 87903 },
+ { url = "https://files.pythonhosted.org/packages/e2/2b/dc048dd71c2e5f0f7ebc04dd7912981ec45793a03c0dc462438e0591ba5d/watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e", size = 88381 },
+ { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 },
+ { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 },
+ { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 },
+ { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 },
+ { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 },
+ { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 },
+ { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 },
+ { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 },
+ { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 },
+ { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 },
+]
+
+[[package]]
+name = "zipp"
+version = "3.21.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 },
+]