pax_global_header00006660000000000000000000000064151715512130014513gustar00rootroot0000000000000052 comment=0032269a0e65c068af9cd86c9a8859a0ddd568d6 docformatter-1.7.8/000077500000000000000000000000001517155121300142215ustar00rootroot00000000000000docformatter-1.7.8/.github/000077500000000000000000000000001517155121300155615ustar00rootroot00000000000000docformatter-1.7.8/.github/release-drafter.yml000066400000000000000000000014771517155121300213620ustar00rootroot00000000000000name-template: '$RESOLVED_VERSION' tag-template: 'v$RESOLVED_VERSION' categories: - title: 'Bug Fixes' labels: - 'P: bug' - 'V: patch' - title: '🚀Features' labels: - 'P: enhancement' - 'V: minor' - title: 'Maintenance' labels: - 'chore' change-template: '- [#$NUMBER] $TITLE (@$AUTHOR)' change-title-escapes: '\<*_&' version-resolver: major: labels: - 'V: major' minor: labels: - 'V: minor' patch: labels: - 'V: patch' default: patch exclude-labels: - 'dependencies' - 'fresh' - 'help wanted' - 'question' - 'release' - 'C: convention' - 'C: stakeholder' - 'C: style' - 'S: duplicate' - 'S: feedback' - 'S: invalid' - 'S: merged' - 'S: wontfix' include-pre-releases: false template: | ## What Changed $CHANGES docformatter-1.7.8/.github/workflows/000077500000000000000000000000001517155121300176165ustar00rootroot00000000000000docformatter-1.7.8/.github/workflows/ci.yml000066400000000000000000000020101517155121300207250ustar00rootroot00000000000000name: Execute Test Suite on: push: branches: - "*" pull_request: branches: - master jobs: test: strategy: fail-fast: false matrix: python-version: - "pypy3.9" - "3.14" - "3.13" - "3.12" - "3.11" - "3.10" os: [ubuntu-latest] runs-on: ${{ matrix.os }} name: "${{ matrix.os }} Python: ${{ matrix.python-version }}" steps: - name: Setup Python for tox uses: actions/setup-python@v4 with: python-version: "3.13" - name: Install tox run: python -m pip install tox tox-gh-actions - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} for test uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Setup test suite run: tox -vv --notest - name: Run tests with tox run: tox -e py --skip-pkg-install docformatter-1.7.8/.github/workflows/do-lint.yml000066400000000000000000000010111517155121300217000ustar00rootroot00000000000000name: Lint on: push: branches: - "*" pull_request: branches: - master jobs: test: runs-on: ubuntu-latest name: "Run linters on code base" steps: - name: Setup Python for linting uses: actions/setup-python@v4 with: python-version: "3.13" - name: Install tox run: python -m pip install tox tox-gh-actions - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Lint code base run: tox -e pre-commit docformatter-1.7.8/.github/workflows/do-prioritize-issues.yml000066400000000000000000000036171517155121300244610ustar00rootroot00000000000000# This workflow runs when labels are applied to issues. # # - Get list of labels. # - Determine issue priority based on labels: # - C: convention && P: bug --> U: high # - C: style && P: bug --> U: medium # - C: stakeholder && P:bug --> U: medium # - C: convention && P: enhancement --> U: medium # - C: style && P: enhancement --> U: low # - C: stakeholder && P: enhancement --> U: low # - chore || P: docs --> U: low name: Prioritize Issues Workflow on: issues: types: ['labeled', 'unlabeled'] jobs: prioritize_issues: runs-on: ubuntu-latest steps: - name: Get Issue Labels id: getlabels uses: weibullguy/get-labels-action@main - name: Add High Urgency Labels if: "${{ (contains(steps.getlabels.outputs.labels, 'C: convention') && contains (steps.getlabels.outputs.labels, 'P: bug')) }}" uses: andymckay/labeler@master with: add-labels: "U: high" - name: Add Medium Urgency Labels if: "${{ (contains(steps.getlabels.outputs.labels, 'C: style') && contains(steps.getlabels.outputs.labels, 'P: bug')) || (contains(steps.getlabels.outputs.labels, 'C: stakeholder') && contains(steps.getlabels.outputs.labels, 'P: bug')) || (contains(steps.getlabels.outputs.labels, 'C: convention') && contains(steps.getlabels.outputs.labels, 'P: enhancement')) }}" uses: andymckay/labeler@master with: add-labels: "U: medium" - name: Add Low Urgency Labels if: "${{ (contains(steps.getlabels.outputs.labels, 'C: style') && contains(steps.getlabels.outputs.labels, 'P: enhancement')) || (contains(steps.getlabels.outputs.labels, 'C: stakeholder') && contains(steps.getlabels.outputs.labels, 'P: enhancement')) || contains(steps.getlabels.outputs.labels, 'doc') || contains(steps.getlabels.outputs.labels, 'chore') }}" uses: andymckay/labeler@master with: add-labels: "U: low" docformatter-1.7.8/.github/workflows/do-release.yml000066400000000000000000000131741517155121300223670ustar00rootroot00000000000000# This workflow runs when a pull request is closed. # # - Gets list of PR labels. # - If 'release' label: # - Get release version using Poetry. # - Build the release. # - Draft a new GitHub release. # - Upload the wheel to the new GitHub release. # - Upload wheel to Test PyPi if build succeeds. (Future) # - Test install from Test PyPi. (Future) # - Upload wheel to PyPi if install test succeeds. (Future) # - Generate new CHANGELOG. # - Get next semantic version. # - Close old milestones. # - Create new minor version milestone. # - Create new major version milestone. name: Do Release Workflow on: pull_request: branches: - master types: - closed jobs: create_new_release: name: Create New Release runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Get PR labels id: prlabels uses: joerick/pr-labels-action@v1.0.8 - name: Get release version id: relversion if: contains(steps.prlabels.outputs.labels, ' release ') run: | pip install poetry echo "version=$(echo $(poetry version | cut -d' ' -f2))" >> $GITHUB_OUTPUT if [[ $version != *"-rc"* ]]; then echo "do_release=1" >> $GITHUB_ENV echo "do_changelog=1" >> $GITHUB_ENV echo "do_milestones=1" >> $GITHUB_ENV fi - name: Build release id: build if: ${{ env.do_release == 1 }} run: | pip install -U pip poetry twine poetry build && twine check dist/* && echo "build_ok=1" >> $GITHUB_ENV - name: Cut the release id: cutrelease if: ${{ env.build_ok == 1 }} uses: release-drafter/release-drafter@v5 with: name: "${{ steps.relversion.outputs.new_tag }}" tag: "${{ steps.relversion.outputs.new_tag }}" version: "${{ steps.relversion.outputs.new_tag }}" prerelease: false publish: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload wheel to GitHub release id: upload-wheel if: ${{ env.build_ok == 1 }} uses: shogo82148/actions-upload-release-asset@v1 with: upload_url: ${{ steps.cutrelease.outputs.upload_url }} asset_path: ./dist/*.whl # - name: Publish to Test PyPi # if: ${{ env.build_ok == 1 }} # uses: pypa/gh-action-pypi-publish@release/v1 # with: # user: __token__ # password: ${{ secrets.TEST_PYPI_API_TOKEN }} # repository_url: https://test.pypi.org/legacy/ # - name: Test install from Test PyPI # if: ${{ env.build_ok == 1 }} # run: | # sudo apt-get update # pip install \ # --index-url https://test.pypi.org/simple/ \ # --extra-index-url https://pypi.org/simple \ # docformatter==${{ steps.newversion.outputs.new_version }} && echo "install_ok=1" >> $GITHUB_ENV # - name: Publish to PyPi # if: ${{ env.install_ok == 1 }} # uses: pypa/gh-action-pypi-publish@release/v1 # with: # user: __token__ # password: ${{ secrets.PYPI_API_TOKEN }} - name: Generate release changelog uses: heinrichreimer/github-changelog-generator-action@master if: ${{ env.do_changelog == 1 }} with: token: ${{ secrets.GITHUB_TOKEN }} sinceTag: "v1.3.1" excludeTagsRegex: "-rc[0-9]" breakingLabel: "Breaking Changes" breakingLabels: "V: major" enhancementLabel: "Features" enhancementLabels: "P: enhancement" bugsLabel: "Bug Fixes" bugLabels: "P: bug" excludeLabels: "release" issues: false issuesWoLabels: false maxIssues: 100 pullRequests: true prWoLabels: false author: true unreleased: true compareLink: true stripGeneratorNotice: true verbose: true - name: Check if diff if: ${{ env.do_changelog == 1 }} continue-on-error: true run: > git diff --exit-code CHANGELOG.md && (echo "### No update" && exit 1) || (echo "### Commit update") - uses: EndBug/add-and-commit@v9 name: Commit and push if diff if: ${{ env.do_changelog == 1 }} with: add: CHANGELOG.md message: 'chore: update CHANGELOG.md for new release' author_name: GitHub Actions author_email: action@github.com committer_name: GitHub Actions committer_email: actions@github.com push: true - name: Get next semantic version id: nextversion if: ${{ env.do_milestones == 1 }} uses: WyriHaximus/github-action-next-semvers@v1.2.1 with: version: ${{ steps.relversion.outputs.version }} - name: Close old milestone if: ${{ env.do_milestones == 1 }} uses: WyriHaximus/github-action-close-milestone@master with: number: ${{ steps.relversion.outputs.version }} - name: Create new minor release milestone if: ${{ env.do_milestones == 1 }} uses: WyriHaximus/github-action-create-milestone@v1.2.0 with: title: "${{ steps.nextversion.outputs.v_minor }}" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create new major release milestone if: ${{ env.do_milestones == 1 }} uses: WyriHaximus/github-action-create-milestone@v1.2.0 with: title: "${{ steps.nextversion.outputs.v_major }}" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} docformatter-1.7.8/.github/workflows/do-update-authors.yml000066400000000000000000000047131517155121300237130ustar00rootroot00000000000000name: Update AUTHORS.rst # What this workflow does: # 1. Update the AUTHORS.rst file # 2. Git commit and push the file if there are changes. on: # yamllint disable-line rule:truthy workflow_dispatch: push: tags: - "!*" branches: - master jobs: update-authors: name: Update AUTHORS.rst file runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - uses: actions/setup-python@v3 - name: Update AUTHORS.rst file shell: python run: | import subprocess git_authors = subprocess.run( ["git", "log", "--format=%aN <%aE>"], capture_output=True, check=True ).stdout.decode() skip_list = ( "Steven Myint", "dependabot", "pre-commit-ci", "github-action", "GitHub Actions", "Sourcery AI", ) authors = [ author for author in set(git_authors.strip().split("\n")) if not author.startswith(skip_list) ] authors.sort() file_head = ( ".. This file is automatically generated/updated by a github actions workflow.\n" ".. Every manual change will be overwritten on push to master.\n" ".. You can find it here: ``.github/workflows/do-update-authors.yml``\n\n" "Author\n" "------\n" "Steven Myint \n\n" "Additional contributions by (sorted by name)\n" "--------------------------------------------\n" ) with open("AUTHORS.rst", "w") as authors_file: authors_file.write(file_head) authors_file.write("- ") authors_file.write("\n- ".join(authors)) authors_file.write("\n") - name: Check if diff continue-on-error: true run: > git diff --exit-code AUTHORS.rst && (echo "### No update" && exit 1) || (echo "### Commit update") - uses: EndBug/add-and-commit@v9 name: Commit and push if diff if: success() with: add: AUTHORS.rst message: 'chore: update AUTHORS.rst file with new author(s)' author_name: GitHub Actions author_email: action@github.com committer_name: GitHub Actions committer_email: actions@github.com push: true docformatter-1.7.8/.github/workflows/on-issue-open.yml000066400000000000000000000005061517155121300230430ustar00rootroot00000000000000# This workflow runs when a new issue is opened. # # - Apply the 'fresh' label. name: Issue Open Workflow on: issues: types: [opened] jobs: label_issue_backlog: runs-on: ubuntu-latest steps: - name: Add fresh new label uses: andymckay/labeler@master with: add-labels: "fresh" docformatter-1.7.8/.github/workflows/on-push-tag.yml000066400000000000000000000022451517155121300225060ustar00rootroot00000000000000# This workflow runs when a version tag is pushed. # # - Get new tag. # - If release condidate tag: # - Cut GitHub pre-release. name: Prerelease Tag Workflow on: push: tags: - 'v*' jobs: cut_prerelease: permissions: contents: write pull-requests: write name: Cut Pre-Release runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v3 with: fetch-depth: 0 ref: master - name: Get new tag id: newversion run: | tag=${GITHUB_REF/refs\/tags\//} if [[ $tag == *"-rc"* ]]; then echo "do_prerelease=1" >> $GITHUB_ENV fi echo "tag=$(echo $tag)" >> $GITHUB_ENV echo "New tag is: $tag" echo "GitHub ref: ${{ github.ref }}" - name: Cut pre-release id: cutprerelease if: ${{ env.build_ok == 1 }} uses: release-drafter/release-drafter@v5 with: name: ${{ env.tag }} tag: ${{ env.tag }} version: ${{ env.tag }} prerelease: true publish: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} docformatter-1.7.8/.gitignore000066400000000000000000000002501517155121300162060ustar00rootroot00000000000000*~ *.egg *.egg-info/ *.eggs/ *.pyc .*.swp .travis-solo/ MANIFEST README.html __pycache__/ build/ dist/ htmlcov/ *coverage* .python-version .idea/ .vscode/ .tox/ .venv/ docformatter-1.7.8/.pre-commit-config.yaml000066400000000000000000000035541517155121300205110ustar00rootroot00000000000000exclude: 'tests/' repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: check-merge-conflict - id: check-toml - id: check-yaml - id: debug-statements - id: end-of-file-fixer - id: no-commit-to-branch - id: trailing-whitespace - repo: https://github.com/pre-commit/pre-commit rev: v4.5.1 hooks: - id: validate_manifest - repo: https://github.com/psf/black rev: '26.3.1' hooks: - id: black types_or: [python, pyi] language_version: python3 - repo: https://github.com/PyCQA/isort rev: 8.0.1 hooks: - id: isort args: [--settings-file, ./pyproject.toml] - repo: https://github.com/PyCQA/docformatter rev: ab715b8e12b601ba392e7502898e500dde10b4e8 hooks: - id: docformatter additional_dependencies: [tomli] args: [--in-place, --config, ./pyproject.toml] - repo: https://github.com/charliermarsh/ruff-pre-commit rev: 'v0.15.11' hooks: - id: ruff args: [ --config, ./pyproject.toml ] - repo: https://github.com/pycqa/pydocstyle rev: 6.3.0 hooks: - id: pydocstyle additional_dependencies: [toml] args: [--config, ./pyproject.toml] - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.20.1 hooks: - id: mypy additional_dependencies: [types-python-dateutil] args: [--config-file, ./pyproject.toml] - repo: https://github.com/myint/eradicate rev: '3.0.1' hooks: - id: eradicate args: [] - repo: https://github.com/rstcheck/rstcheck rev: 'v6.2.5' hooks: - id: rstcheck additional_dependencies: [tomli] args: [-r, --config, ./pyproject.toml] docformatter-1.7.8/.pre-commit-hooks.yaml000066400000000000000000000002651517155121300203630ustar00rootroot00000000000000- id: docformatter name: docformatter description: 'Formats docstrings to follow PEP 257.' entry: docformatter args: [-i] language: python types: [python] docformatter-1.7.8/.sourcery.yaml000066400000000000000000000027261517155121300170450ustar00rootroot00000000000000# 🪄 This is your project's Sourcery configuration file. # You can use it to get Sourcery working in the way you want, such as # ignoring specific refactorings, skipping directories in your project, # or writing custom rules. # 📚 For a complete reference to this file, see the documentation at # https://docs.sourcery.ai/Configuration/Project-Settings/ # This file was auto-generated by Sourcery on 2022-12-28 at 22:39. version: '1' # The schema version of this config file ignore: # A list of paths or files which Sourcery will ignore. - .git - venv - .venv - env - .env - .tox rule_settings: enable: - default disable: [] # A list of rule IDs Sourcery will never suggest. rule_types: - refactoring - suggestion - comment python_version: '3.7' # rules: # A list of custom rules Sourcery will include in its analysis. # - id: no-print-statements # description: Do not use print statements in the test directory. # pattern: print(...) # replacement: # condition: # explanation: # paths: # include: # - test # exclude: # - conftest.py # tests: [] # tags: [] # rule_tags: {} # Additional rule tags. # metrics: # quality_threshold: 25.0 # github: # labels: [] # ignore_labels: # - sourcery-ignore # request_review: author # sourcery_branch: sourcery/{base_branch} # clone_detection: # min_lines: 3 # min_duplicates: 2 # identical_clones_only: false # proxy: # url: # ssl_certs_file: # no_ssl_verify: false docformatter-1.7.8/AUTHORS.rst000066400000000000000000000041141517155121300161000ustar00rootroot00000000000000.. This file is automatically generated/updated by a github actions workflow. .. Every manual change will be overwritten on push to master. .. You can find it here: ``.github/workflows/do-update-authors.yml`` Author ------ Steven Myint Additional contributions by (sorted by name) -------------------------------------------- - Adam Dangoor - Alec Merdler - Alexander Biggs - Alexander Kapshuna - Alexander Puck Neuwirth - Alexandre Detiste - Andrew Howe - Andy Hayden - Anthony Sottile - Antoine Dechaume - Asher Foa - Benjamin Schubert - Björn Holtvogt - Casey Korver <84342833+korverdev@users.noreply.github.com> - Daniel Goldman - Doyle Rowland - Elliot Ford - Eric Hutton - Filip Kucharczyk - Jonas Haag - Josef Kemetmüller - Kapshuna Alexander - Kian-Meng Ang - KotlinIsland <65446343+KotlinIsland@users.noreply.github.com> - Lisha Li <65045844+lli-fincad@users.noreply.github.com> - Manuel Kaufmann - Oliver Sieweke - Paul Angerer <48882462+dabauxi@users.noreply.github.com> - Paul Angerer <48882462+etimoz@users.noreply.github.com> - Peter Boothe - Peter Cock - Sebastian Weigand - Sho Iwamoto - Swen Kooij - Thomas Denewiler - finswimmer - happlebao - icp - serhiy-yevtushenko docformatter-1.7.8/CHANGELOG.md000066400000000000000000000172361517155121300160430ustar00rootroot00000000000000# Changelog ## [v1.7.7](https://github.com/PyCQA/docformatter/tree/v1.7.7) (2025-05-11) [Full Changelog](https://github.com/PyCQA/docformatter/compare/v1.7.6...v1.7.7) **Merged pull requests:** - chore\(deps-dev\): bump black from 22.12.0 to 24.3.0 [\#311](https://github.com/PyCQA/docformatter/pull/311) ([dependabot[bot]](https://github.com/apps/dependabot)) - chore\(deps-dev\): bump jinja2 from 3.1.4 to 3.1.6 [\#310](https://github.com/PyCQA/docformatter/pull/310) ([dependabot[bot]](https://github.com/apps/dependabot)) - chore\(deps-dev\): bump requests from 2.31.0 to 2.32.2 [\#309](https://github.com/PyCQA/docformatter/pull/309) ([dependabot[bot]](https://github.com/apps/dependabot)) - chore\(deps-dev\): bump virtualenv from 20.21.1 to 20.26.6 [\#308](https://github.com/PyCQA/docformatter/pull/308) ([dependabot[bot]](https://github.com/apps/dependabot)) - chore\(deps-dev\): bump zipp from 3.15.0 to 3.19.1 [\#307](https://github.com/PyCQA/docformatter/pull/307) ([dependabot[bot]](https://github.com/apps/dependabot)) - chore\(deps-dev\): bump urllib3 from 2.0.7 to 2.2.2 [\#306](https://github.com/PyCQA/docformatter/pull/306) ([dependabot[bot]](https://github.com/apps/dependabot)) ## [v1.7.6](https://github.com/PyCQA/docformatter/tree/v1.7.6) (2025-05-07) [Full Changelog](https://github.com/PyCQA/docformatter/compare/v1.7.5...v1.7.6) Features - prefer new unittest.mock from the standard library [\#280](https://github.com/PyCQA/docformatter/pull/280) ([a-detiste](https://github.com/a-detiste)) - Handle abbreviation 'etc.' \(et cetera\) [\#273](https://github.com/PyCQA/docformatter/pull/273) ([knedlsepp](https://github.com/knedlsepp)) Bug Fixes - Do not double-process urls [\#284](https://github.com/PyCQA/docformatter/pull/284) ([lilatomic](https://github.com/lilatomic)) **Merged pull requests:** - Fix pre-commit syntax [\#266](https://github.com/PyCQA/docformatter/pull/266) ([jonashaag](https://github.com/jonashaag)) - Update version listed in documentation's Pre-Commit example [\#262](https://github.com/PyCQA/docformatter/pull/262) ([korverdev](https://github.com/korverdev)) - Add missing comma in requirements table [\#261](https://github.com/PyCQA/docformatter/pull/261) ([EFord36](https://github.com/EFord36)) ## [v1.7.5](https://github.com/PyCQA/docformatter/tree/v1.7.5) (2023-07-12) [Full Changelog](https://github.com/PyCQA/docformatter/compare/v1.7.4...v1.7.5) Features - fix: not recognizing `yield` as a sphinx field name [\#254](https://github.com/PyCQA/docformatter/pull/254) ([weibullguy](https://github.com/weibullguy)) ## [v1.7.4](https://github.com/PyCQA/docformatter/tree/v1.7.4) (2023-07-10) [Full Changelog](https://github.com/PyCQA/docformatter/compare/v1.7.3...v1.7.4) Bug Fixes - fix: summary with back ticks and sphinx field names with periods [\#248](https://github.com/PyCQA/docformatter/pull/248) ([weibullguy](https://github.com/weibullguy)) **Merged pull requests:** - chore: update documentation link for metadata [\#247](https://github.com/PyCQA/docformatter/pull/247) ([icp1994](https://github.com/icp1994)) - test: split format tests into multiple files [\#246](https://github.com/PyCQA/docformatter/pull/246) ([weibullguy](https://github.com/weibullguy)) ## [v1.7.3](https://github.com/PyCQA/docformatter/tree/v1.7.3) (2023-06-23) [Full Changelog](https://github.com/PyCQA/docformatter/compare/v1.7.2...v1.7.3) Bug Fixes - fix: removing newline between Sphinx field lists [\#237](https://github.com/PyCQA/docformatter/pull/237) ([weibullguy](https://github.com/weibullguy)) **Merged pull requests:** - chore: move changelog to tag workflow [\#233](https://github.com/PyCQA/docformatter/pull/233) ([weibullguy](https://github.com/weibullguy)) ## [v1.7.2](https://github.com/PyCQA/docformatter/tree/v1.7.2) (2023-06-07) [Full Changelog](https://github.com/PyCQA/docformatter/compare/v1.7.1...v1.7.2) Bug Fixes - fix: wrapping issues with reST directives, quoted URLs, and Sphinx field lists [\#219](https://github.com/PyCQA/docformatter/pull/219) ([weibullguy](https://github.com/weibullguy)) ## [v1.7.1](https://github.com/PyCQA/docformatter/tree/v1.7.1) (2023-05-19) [Full Changelog](https://github.com/PyCQA/docformatter/compare/v1.7.0...v1.7.1) Features - feat: support epytext style [\#211](https://github.com/PyCQA/docformatter/pull/211) ([weibullguy](https://github.com/weibullguy)) - feat: use tomllib for Python 3.11+ [\#208](https://github.com/PyCQA/docformatter/pull/208) ([weibullguy](https://github.com/weibullguy)) - feat: wrap Sphinx style long parameter descriptions [\#201](https://github.com/PyCQA/docformatter/pull/201) ([weibullguy](https://github.com/weibullguy)) Bug Fixes - fix: improper wrapping of short anonymous hyperlnks [\#213](https://github.com/PyCQA/docformatter/pull/213) ([weibullguy](https://github.com/weibullguy)) **Merged pull requests:** - chore: update version strings [\#214](https://github.com/PyCQA/docformatter/pull/214) ([weibullguy](https://github.com/weibullguy)) - chore: update pre-commit-config [\#209](https://github.com/PyCQA/docformatter/pull/209) ([weibullguy](https://github.com/weibullguy)) ## [v1.7.0](https://github.com/PyCQA/docformatter/tree/v1.7.0) (2023-05-15) [Full Changelog](https://github.com/PyCQA/docformatter/compare/v1.6.5...v1.7.0) Features - feat: add option to format compatible with black [\#196](https://github.com/PyCQA/docformatter/pull/196) ([weibullguy](https://github.com/weibullguy)) - feat: add option for user to provide list of words not to capitalize [\#195](https://github.com/PyCQA/docformatter/pull/195) ([weibullguy](https://github.com/weibullguy)) **Merged pull requests:** - chore: update workflows [\#206](https://github.com/PyCQA/docformatter/pull/206) ([weibullguy](https://github.com/weibullguy)) ## [v1.6.5](https://github.com/PyCQA/docformatter/tree/v1.6.5) (2023-05-03) [Full Changelog](https://github.com/PyCQA/docformatter/compare/v1.6.4...v1.6.5) Bug Fixes - fix: removing blank line after import section [\#204](https://github.com/PyCQA/docformatter/pull/204) ([weibullguy](https://github.com/weibullguy)) **Merged pull requests:** - chore: add GH release badge [\#200](https://github.com/PyCQA/docformatter/pull/200) ([weibullguy](https://github.com/weibullguy)) - chore: update workflows to create release [\#198](https://github.com/PyCQA/docformatter/pull/198) ([weibullguy](https://github.com/weibullguy)) - chore: update GH actions to generate CHANGELOG [\#194](https://github.com/PyCQA/docformatter/pull/194) ([weibullguy](https://github.com/weibullguy)) ## [v1.6.4](https://github.com/PyCQA/docformatter/tree/v1.6.4) (2023-04-26) [Full Changelog](https://github.com/PyCQA/docformatter/compare/v1.6.3...v1.6.4) ## [v1.6.3](https://github.com/PyCQA/docformatter/tree/v1.6.3) (2023-04-23) [Full Changelog](https://github.com/PyCQA/docformatter/compare/v1.6.2...v1.6.3) ## [v1.6.2](https://github.com/PyCQA/docformatter/tree/v1.6.2) (2023-04-22) [Full Changelog](https://github.com/PyCQA/docformatter/compare/v1.6.1...v1.6.2) ## [v1.6.1](https://github.com/PyCQA/docformatter/tree/v1.6.1) (2023-04-21) [Full Changelog](https://github.com/PyCQA/docformatter/compare/v1.6.0...v1.6.1) ## [v1.6.0](https://github.com/PyCQA/docformatter/tree/v1.6.0) (2023-04-04) [Full Changelog](https://github.com/PyCQA/docformatter/compare/v1.5.1...v1.6.0) ## [v1.5.1](https://github.com/PyCQA/docformatter/tree/v1.5.1) (2022-12-16) [Full Changelog](https://github.com/PyCQA/docformatter/compare/v1.5.0...v1.5.1) ## [v1.5.0](https://github.com/PyCQA/docformatter/tree/v1.5.0) (2022-08-19) [Full Changelog](https://github.com/PyCQA/docformatter/compare/v1.4...v1.5.0) ## [v1.4](https://github.com/PyCQA/docformatter/tree/v1.4) (2020-12-27) [Full Changelog](https://github.com/PyCQA/docformatter/compare/v1.3.1...v1.4) docformatter-1.7.8/LICENSE000066400000000000000000000020451517155121300152270ustar00rootroot00000000000000Copyright (C) 2012-2018 Steven Myint Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. docformatter-1.7.8/README.rst000066400000000000000000000157131517155121300157170ustar00rootroot00000000000000============ docformatter ============ .. |CI| image:: https://img.shields.io/github/actions/workflow/status/PyCQA/docformatter/ci.yml?branch=master :target: https://github.com/PyCQA/docformatter/actions/workflows/ci.yml .. |COVERALLS| image:: https://img.shields.io/coveralls/github/PyCQA/docformatter :target: https://coveralls.io/github/PyCQA/docformatter .. |CONTRIBUTORS| image:: https://img.shields.io/github/contributors/PyCQA/docformatter :target: https://github.com/PyCQA/docformatter/graphs/contributors .. |COMMIT| image:: https://img.shields.io/github/last-commit/PyCQA/docformatter .. |BLACK| image:: https://img.shields.io/badge/%20style-black-000000.svg :target: https://github.com/psf/black .. |ISORT| image:: https://img.shields.io/badge/%20imports-isort-%231674b1 :target: https://pycqa.github.io/isort/ .. |SELF| image:: https://img.shields.io/badge/%20formatter-docformatter-fedcba.svg :target: https://github.com/PyCQA/docformatter .. |SPHINXSTYLE| image:: https://img.shields.io/badge/%20style-sphinx-0a507a.svg :target: https://www.sphinx-doc.org/en/master/usage/index.html .. |NUMPSTYLE| image:: https://img.shields.io/badge/%20style-numpy-459db9.svg :target: https://numpydoc.readthedocs.io/en/latest/format.html .. |GOOGSTYLE| image:: https://img.shields.io/badge/%20style-google-3666d6.svg :target: https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings .. |VERSION| image:: https://img.shields.io/pypi/v/docformatter .. |LICENSE| image:: https://img.shields.io/pypi/l/docformatter .. |PYVERS| image:: https://img.shields.io/pypi/pyversions/docformatter .. |PYMAT| image:: https://img.shields.io/pypi/format/docformatter .. |DD| image:: https://img.shields.io/pypi/dd/docformatter .. |PRE| image:: https://img.shields.io/github/v/release/PyCQA/docformatter?include_prereleases +----------------+----------------------------------------------------------+ | **Code** + |BLACK| |ISORT| + +----------------+----------------------------------------------------------+ | **Docstrings** + |SELF| |NUMPSTYLE| + +----------------+----------------------------------------------------------+ | **GitHub** + |CI| |CONTRIBUTORS| |COMMIT| |PRE| + +----------------+----------------------------------------------------------+ | **PyPi** + |VERSION| |LICENSE| |PYVERS| |PYMAT| |DD| + +----------------+----------------------------------------------------------+ Formats docstrings to follow `PEP 257`_. .. _`PEP 257`: http://www.python.org/dev/peps/pep-0257/ Features ======== ``docformatter`` automatically formats docstrings to follow a subset of the PEP 257 conventions. Below are the relevant items quoted from PEP 257. - For consistency, always use triple double quotes around docstrings. - Triple quotes are used even though the string fits on one line. - Multi-line docstrings consist of a summary line just like a one-line docstring, followed by a blank line, followed by a more elaborate description. - Unless the entire docstring fits on a line, place the closing quotes on a line by themselves. ``docformatter`` also handles some of the PEP 8 conventions. - Don't write string literals that rely on significant trailing whitespace. Such trailing whitespace is visually indistinguishable and some editors (or more recently, reindent.py) will trim them. ``docformatter`` formats docstrings compatible with ``black`` when passed the ``--black`` option. ``docformatter`` formats field lists that use Epytext or Sphinx styles. See the the full documentation at `read-the-docs`_, especially the `requirements`_ section for a more detailed discussion of PEP 257 and other requirements. .. _read-the-docs: https://docformatter.readthedocs.io .. _requirements: https://docformatter.readthedocs.io/en/latest/requirements.html Installation ============ From pip:: $ pip install --upgrade docformatter Or, if you want to use pyproject.toml to configure docformatter and you're using Python < 3.11:: $ pip install --upgrade docformatter[tomli] With Python >=3.11, ``tomllib`` from the standard library is used. Or, if you want to use a release candidate (or any other tag):: $ pip install git+https://github.com/PyCQA/docformatter.git@ Where is the release candidate tag you'd like to install. Release candidate tags will have the format v1.6.0-rc1 Release candidates will also be made available as a Github Release. Example ======= After running:: $ docformatter --in-place example.py this code .. code-block:: python """ Here are some examples. This module docstring should be dedented.""" def factorial(x): ''' Return x factorial. This uses math.factorial. ''' import math return math.factorial(x) def print_factorial(x): """Print x factorial""" print(factorial(x)) def main(): """Main function""" print_factorial(5) if factorial(10): print_factorial(22) gets formatted into this .. code-block:: python """Here are some examples. This module docstring should be dedented. """ def factorial(x): """Return x factorial. This uses math.factorial. """ import math return math.factorial(x) def print_factorial(x): """Print x factorial.""" print(factorial(x)) def main(): """Main function.""" print_factorial(5) if factorial(10): print_factorial(22) Marketing ========= Do you use *docformatter*? What style docstrings do you use? Add some badges to your project's **README** and let everyone know. |SELF| .. code-block:: .. image:: https://img.shields.io/badge/%20formatter-docformatter-fedcba.svg :target: https://github.com/PyCQA/docformatter |SPHINXSTYLE| .. code-block:: .. image:: https://img.shields.io/badge/%20style-sphinx-0a507a.svg :target: https://www.sphinx-doc.org/en/master/usage/index.html |NUMPSTYLE| .. code-block:: .. image:: https://img.shields.io/badge/%20style-numpy-459db9.svg :target: https://numpydoc.readthedocs.io/en/latest/format.html |GOOGSTYLE| .. code-block:: .. image:: https://img.shields.io/badge/%20style-google-3666d6.svg :target: https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings Assistance ========== ``docformatter`` has an IRC channel on `Libera.Chat`_ in the `#docformatter`_ room. .. _`Libera.Chat`: https://libera.chat .. _`#docformatter`: https://web.libera.chat/#docformatter There is no ``docformatter`` channel on the Python Code Quality Discord server, but you can ask for help in the `# general`_ channel. .. _`# general`: https://discord.com/channels/825463413634891776/934197425357336596 Issues ====== Bugs and patches can be reported on the `GitHub page`_. .. _`GitHub page`: https://github.com/PyCQA/docformatter/issues docformatter-1.7.8/docs/000077500000000000000000000000001517155121300151515ustar00rootroot00000000000000docformatter-1.7.8/docs/Makefile000066400000000000000000000011761517155121300166160ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = source BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) docformatter-1.7.8/docs/images/000077500000000000000000000000001517155121300164165ustar00rootroot00000000000000docformatter-1.7.8/docs/images/pycharm-file-watcher-configurations.png000066400000000000000000006540111517155121300261760ustar00rootroot00000000000000PNG  IHDRmI biCCPICC ProfileHWXS[RIhPD@J-TATBH(1&;*Eˊ Z(XPQł 7!]}{Ο3gS2s :|, _Z aMMc0.pl@!E(?UK* E H:ęB &bL^1mTX  Ve*[m7@l[U(Ȇ<!v %Rt B!?I@eg?3AΫ_ȡ,?,<{hbyd*X빓Tq43&VUkIR$=j&Ppab7!?4 b3åy1}f$1\-TI/Q3wH\/;\fn-_WeߢMhEE`BIr (r6u3`#Wƫⷅ-F,yx^+Kx1\Q NT)o qHI)F"sZE$M]YAHfn,/NcEy*5Ħ\|d\j~$v; ;5`C*<oBwJ*j:>j@hjjq'ɦ%~D,T0l( 7EzV 3_tů{p?s8 _F*(jz@(`l#x ( A*,Y`.(e`X ցM` ~{A=8,[ptg:@LKqA6!H H$ )K H%]#;ْN'br9y'0"1KQb)B4RVJ#vxOFh]^~ك1ۣㅧsu/h>yx˽k;}l}2|\c؋٧| !}+N'#FFn :9=S`{U?*~M0x[c'<-D?-׏;F% n^1=)<Ϝ'UG9%.~smnoڧzΥ5hsW PZy7swG^y{_ov_ZM׷76uѿq~~`!CKS?wHOho{r˘QO?q$S;} LYuޭu}7\6ŠG/^:qw았+mW^~y7^,{km;wݭ;w?*~'Ovw^x:i3ٳޮZo}{lw _usO\7ozߖ3y=)NHXSϷd|9(fer;Ta/ڏ{axP ;q@`TG`zx 6(<\4x!{eO }}@d]S%Dx7)X oD}*o{| (ceXIfMM*>F(iNxASCIIScreenshot]1 pHYs%%IR$iTXtXML:com.adobe.xmp 1712 Screenshot 1238 jiDOTk(kk(~Ң@IDATx ^E.\$$aH a і@ *`3 -}U\e:ڎʬBHDŽ9ZkN$y kZV[Svm|_WwW !u.Duu1ϲC7n޾ Bˋ̬Q,MEb؞V>2ImMC9T;ʫGHDȋ"|(tmfYk?eKLNlKL>ttnJɏz%]fָ)r|K;}]k &cÀi.le[B̏di4ҵ_ 7heߟi+-o;bO@ 1 Txb̮K:0S}lkc|{eh1+tmw5$lgk2o&<zB$/vT.eT<2M?RE2Ȕ|(o@ {`:I5lEKL $D{K*ޒħ꒍ R.=֞tgo'ߍmMwc>%Ol5=T2Z-#< [I+fM7f1xF@"KXXܨh64y~ˀhx?JV[Xȫ~v2CY!v6 X9F K%]p]۶"|ɨZl?1GӘH: 4 8IlpN[?Z]P8Ujz? [?&_ =n~}Iowy]~MuT>Kۅb@f5/%?$ܴU%~|X'U&%[!=0Jg@cԵD[ }=’KâE… / K{t8) Z("B@! B@! B@!&#n[oaa뇮-g  , ajaDŽm.lVNnuh'D `4ѐE-w1nY+c$롕2'(0{CnOj/W_O'[}???G?VO5jW/m*3$*AٌHcƠǾr/^^xOn `⽆/0o&oiaNcZzaL˼K@|`ΎYQAzHq4 (ˣa4T xl-3ltʧX|t6 ~ zBcܨ'wuX Z|/OO_VG?Zll;aͿh]5hh:yňol!c[6lfC"~A:j>ӊU; 4E]Ce޼pMರÎ;C00{l3j-YW . ˖EcLE1I9C/YfF"}.K5|ݥfB)+Sؓә Jy% ӗ@LOaOOהSx_ߙ9Z45^)VɧVM]SR|n¿ǥ zVñH%jAS7IU+qVSkIt0/mmmx1ߦDyJn*Z"]/۪?IGjz*ck"г"]@-`Un\f)I3AScbQƙ9 ~k$Dcv*ob5d`xd6dj.1rr25xzaȐ74lцkUWXc՜qw~ᕙk͟o߾ǭ I{Q"1ҝ) :#AiFex^Ac'Ā6eV `{v Eے:(5 ;+s$jY6-F^2ܖ˺#1\2 26e˘e$EmuBmpUH*P!חFW#Թ2\+e,?âθThU|2ҿ _S*1H7j:WkEEgXח W/õ^FW"tB%\__RpUH :RU6eVH*XNUĐK#W\2ҿ h_?WPyu)%:W^f:ЪdXJ#Nec8zvYŋџ"T,P3Ac{ޮ9 KHY yNn+k@=9eqsVs:d@kX8k%@EM3l|4NqG@?>P)yi۵ Kgs ۜi`,LÀ5cƌ#7<HWDzf4?d&Nȝ/0W|p? iSSNxc3y+j?PK1h8:4":gX܁+!CXo)%`91yҿ QY(sBų>4Dt4TCa'JZҙP ,__CJbE;ZRa˰5WСakÇ@Ae2 N¼a,yב.)/:5XYtk~0F5n# Xlixr0k65),O Z[YXJƋ<Ҡ-g$6- uo;?# )}/HlZ<=꼅΁)|LjIs ҷp缅?q08s ش7xzy S|=4og6y `4p*|=#io 8#&9-zhHߒmhT&;-zFbӒQ-pL;FLr[ А%8-Lv[Ħ%ӣ['w0!}K> q[S0MK~GO901an3@C|n ض-gToT\'3OAo(zgDNPWq>q`?lW8 #B=!z'ȁ.- aȑa7  B^^gaÊi3>+,^ sf.W5\ˏ\xjv8mE\X^M3 `Q=jdL%KNkEc!(4MV,Î;Z^*)YAi.(pMUd' T A1@35lR_"0*4jYg?k, 7|͘9o&6 ~{xrƓH 6L-/%+9`Ot~y_WOUX_WK*Q. jd+2eGQS4G%G?Wc66X1&~Eeꅙş,AЖYAf危ebEgxTXyZ̀=Si-./2oĝ9|o7wo4^DSwF? Gg ҿ |4_9?1ƔJͿ+?8A$H]ku:LǺ'g@_<쥈46GƗ%?ﵲ/reߩ8Op.egDD|fƭݚ c&av9ﴴI&mrGy폧Ϙd ;oE/Xp5Vi㭐o@8 yJDes28SC"IEz'u`p#tx8.)b$̖ Ot =͚#O紤oIJ҇T>t19K49 @f|)jy}9x(*P>IKs~fB"O]w o%%a7\" IL[|d<%7+xr뭶\!z`uxB%}x+v5kDO ,StAѸEg,@-7}O~/]vr,Z"yx o_yxX/M5>>c;M3fƟƟƟƟ3d5si̩?=;n7y$sx2cܹᢋ.jKWa?p X{m/}Ky7|x=s{^;jM»:,w}Y et;n¦cxcۭA~7p^,?Iwm[N3fZ;pqkI 3yyXK444yͼs_흯5fca?k>]i ~? 4:c}R6L+a6<O"N0`n<Lb @a|l@/m׸h$:,&N+-n:O7"dV4l2*66 ىHk!ŢFbd $pUW]^}DK/LD_UVdCQߓZ/?AKgX$tP\,'˯:B4ĐWȷz4fyo@~]NG(^g 0ZaO3#k{+.#Rzw޹aczqO>z?opWzX[X)w ?"H/VҋAw~F JnI'hrCs7OsϚa+ymE[x_aQG`b?Qx)`d"9.* 'ƌD_6<^x~ 7vvYқ#q}:&/fbߊ_K@6i}[?~ds8ZcbYR~m\?L)x¼>w71r#@]b >𭪾)Sub]iåd̀5W<7_:voN:$3o)^կ%Tr!t6k/Gii<7-Y8|8m߬H;˜]֛ A +Ѱ]j8. wiKTihcCAN2]ч 0Y?<{ټf'Z~ʋ l9tz6#?< Ws[q'oF3l2 4,`N& cFH2`H+e\-H`%KR%7C#Gl^ ֥0`ROWQFž/ sx:E^%v!,Z ̚=;loV=}M{aH|=yO;v*޼/z,s>φ!C NWØo|#D: ahWsy ƒ>xMFy+0(Yϩx| &I^裏qk…m=v#$6?S7|s5G9l Ompᮻ}*+O{Sl"$05@UBǟ !Ӫ z*fd??;O9P1Ϳ9FOvqDrު$hMoҶU,H릱Sc׮855]!H~(LH\ ~G+*a߾n{'V.3L52G}y#ܓO=;7[FPP| цlϵ1Q> WxC^n$ј"^2 3ON]YWY/lvvwU&54\ b[,?S~Y9#3Ëy^R#!%gFz<$#! 325ǢV'f%Y;/k wVD$ wC'9;ZtNgT)?N15b.5;Nq|h#BcR\sp**_07f|%\ VSŚmoaO p\?=z4֦mfex . |i[&֡94o7s]ƒag/4#ʳ<2[o30 <8<06b"|'N wŗ[ N?0ƛ<Ѐ=X|Eg?Yxg-s0d!$u,d7\~]vsd4N6-WKqÅ'>|FoiT'u#|{]veя|7TıOʯ,rGgm^1p##$7pCj]AnecĒ3Ii鉬Fg|O _c6+|$$OOj7Qe-ᄟdž\˽y`͝(u36>>slGj~3 'z0y̶~h#IY)_!w*dbeP̿uƙg)S`q8PYpG &0uN#}Z2W, gp[J DVwĻ/6HotϺ-z'AGov [aZ+X[o rk:d=\!]!xԑGphN ^77,[r?~ uig\WΨǎ{+W^~)yǍn!:9ص[zSY{if̘nefmK{رVGy4\~o,ކ_q?!P0[le8J>acwv+līQ[ehXIyӧ?aC CaDc?ꗿ 'O6,%~I'8%kfPo]ۮ"!_/<S>q6 X xad| 0ʫyn]jWI[EqE_~BgG_}Z1I/`XADgzd.闿 , R>kxrW~,ReHOvz@,13[kLȠ'#i~ʀo`q3ī#Y,(JGcG DINƟ4xr[/^8?{$8{1).cw i-8Om+"0&Ѩxc@/[[ɩZHy 'ai<)x$AncH㓨E/GL]hu^ՏɈ;AEIy^FVY/涧-"ߦ8b¿DH;(å0"4Pj15s.XI6G6 8p?90\.qaGAkA0a/HUFx ɳ>h%Pqp(_ Qֱɏ] y:R:^7N|UPӥNԏ2\#|? 5W טɛ4gO[z.2 +dƢ!JH,'!-_\w5WxbJ:S1ʪ*ēO  ['Ȧ2Ɋ ៧1h-.!Y5YE GfAur+Cp~IJJac% eSYףUX4Z KziS*qm{fr;8t R6_{ `T&7F#N]hQ#Ns[™ 1کKz+}o3p[YfJ_|k@x?1a_0#<_1\F8I!GNmLTZΧLM<F uK}V%ĺ^wu{~}m5Y=η? #/~Kydާp m8s tm]1Uz!L+? Ίn [g}⬰馛g~qU>{颯|8vm՗MzlRG> =`d_ׂ}o 1bos X_rqdiLh.xPԨѣGy;0 \!^!7go^?~-~_Z8)| ĞcB&埗|F8'x"?dY:GD7Tls-_=$(Dև G džGmm3whT%7DC.|m0$l2<TF8孎>~bgXf_/ sB$~ y2߀uv%~wGZb|VΌX]7up:"L:%ecax2㉟ȍHϊ"MK8JՅN}Q~S3ļ یPOc(oذaqhP\sfm+_o|ik/X_f~ix׻eF.^yo_}k@vͷ[o_TgfC4Mrh7S! kiii[rF|3_cb*[ļT[m-Y [⃕?#Zނr~xw}› yZ !R)d>Tn'q&O I-s }p«X;Yv0V 2@Y$haI~*\{͵1A>3 "=c*21 1s 7N.bcS`駟W~ni* o+ oiWx]w5(kSQ!%靐l/F~.j EW~v@?ۗ<ɷ^q,( dVWEc Xs< Xv e8kbG7;X{\~  4 w>~GwnF;EԳ>^0l}ܺu*,ƾO`X;pd)~ޏ9/2Q_ȮHxD܈G'Xv))u׾^z97>+"nw?sOe-bnkJZkmN":qlɏASy¿9/RTse+MkJū'i1pȮ>[di2cH>tE 6|ddX `,)B vN ,bgLB YS ~/\y{y*A`&&%e@ۘ&DvU3Y1{{1вLfmqo獅|6zq/?p bf/WfD.%7G _ As,Zn1A~+w*>s`w&>8Ⅲ+U 6v3aFM2Y6?jhc1˷xߎZ|_OOO1p~kCkja'l_x?皺p#azy+2x?SEп XX|ۭf~ AE68!Ex+!։zBJ ⱄ=!ŵpܹ]Kwav/Rϟģ~nDIǎXycy<{zz^jQXh exX;fNń[4mQz@>X\puך|{ 埏h*ߩ}N;ۂ 93%6"fϞDn1uםp*DZ&C|е? /vg^V?tA_6!U˼0)by'ͿC| >i-~,aOssҿ7>76T6nZZxb>deiJ<mh,iI]zOkc { 50O \t(lv,wO€5-v' '̀mdF IYqd-a 5<,4!7x*њShΣT3 R>__qyxv3Ojm4y>pv@4?Ey,ݟ|Zn;1f>czXxqG̏OҼ$}Ǖ|G$Ag+?ҦZA l}ZU/Yľ0hyS~iJ|r%ѯzJ]|?,8q+[_Ɉ4*:PWB}UJD_ISkLmG'r63qR[{jM 컯_|%8Q?%/pȃ'|X@IDAT[`Zy |_a|*esb|^gWBտN`tP6f XqCg/c+cn0/] 6o - |v܄#?)7 h4i:Y e 3Ǝݥoڴi¨`DTr 'm+#Gn<0luePMyH놶:xI>d %UW]ex^69Tnaڰ|z,%=xɳZ_/N, rKɏ3U)v & a?A&8R8#LP4;fFB@WX4awo6>pI-*la@γbgI$73Dҟ|zbs@]?[dMJL=sӤTrY<%i7@t,J444cf5Ͽ<^ {Y4ڇdv%-Y$|{߳hwN;lw l Aƫp[o5xj=#rte2 rC x`6?Xgs˝s'&6}8x?yoQGe<0qb~W%NW෷\~ӀoMS^xBa2'?69ƶqqgI/Ԝ_8WҀv{~smK_xq3A]>я]wgetǟoc.O nax %^v G[q+Hz2dҍ+3/4ߋI>-q$t H/ƃCcͳ|iU1~M&?ֻ)xz&zK.Qnw?/E85JZEbhw"Gb|O+"̨ EK Yh_'_s `^!ZʹaFײϹ81n5XhQh|`pp'>:ܹ?^xܒ0"vԿ!򟻍6(\p&ҥnbxó<"0&Cxb'?SX6pk)^W?u.ưˮd}WU|WE0ܳφqeK/-tSO=5ma ,Yrzm&Wfp nҍSvgӀe, _T+]Ik?ǿlN9ֶWK RRֲySj7qƺW~llFYA]} |]u| ?! f^.P-*]} 'IVMWɑٸL0aB8`t)ɛX*Z4&ў`ƋD"TnYn'ΦY./[廲V  C >604Κ9TӦޙ勪E }x.#@*S*Kjj{;L|pb~~WPXppX8%,\nKeߨy%VޒпNWg;w7}0V/[%??ߨiiG}7"Pc?nb 'ܹᢋ. ܓO:9lՖ>1c QX[&hr9T7`O9T1 Npy |gpw{q2c8!e_z%8!'kL{Bܳυկ80 j/=+O:$\c8$ª'OveK8y^!w߽k&7ȯ+Knp}ڕsG֟8Ԡw_}hwm%i֏Iˈ,ׇҿX7ʧ|G?xMͱ,y2.0L1R"scM6-'vi0#\F^qƅ/?tdr%|jeF {Wv*t+#ʤ 5raM7 jDA`2<ӇJ. /b/16شњFr[8߮GlO+*˚OƋqF=H{ђ]Y&9)MKg)’IGC%- tLH %6ưJXX/Bt6PiA[4j *c㈏`^R?l(%Jο|2~scdiRyxm6l߲:C㱽QČvRg,WtPY_%*:d{zw8K͓HҝI\[,x'~wzk@5 טM~D!D=wxYt 2iE,GJ0/tl S? tq!f̳t+GJ0/ILGJeIaj̬/$.ҿ:>)ƟƟ_7D18lKssMFqk[9#N!~:؏|ϳsdy)\FKg)kf;Y࢟zFV*}EQP!AؾiӀqtٕṣߊ0L.ZUJ }= EF6E[xYx2<+ ҽQ>xz=:dhFCYdt<4/U/;wii&1_Oo'?J'=\^Bau$qv;n bCH#vF['<9c½5|b,T:'YMbeSNvf^|oU)ɷcVj1#7 E j.YMwodW֟4ZKZyZzb8NJ SV//C0e0v>̞*h+Ro^%8ƹe7gԏ'\ϷP>r77!_V#zإqQC\*\um_?~˪ J#{ RŘ$Qڏ(/"Gcж':XXv =3]1n4~2=LGCni6̈,Z6ߒz><Օ|^OgjO#KmCa0ZKc/ , /Wyx9Ҕb*i4 ?g{oA>A_BڜO=L,?VgYli l_f0uo^ɓ|/ƃ_1˃<¤aF YbhW>A'L)%.y_Y/?qWoh3/yPBOE? 6W'U q?f'+4G/7ϬNǾC20lp"#3\znv}qV'3004(|ƌ*+ øG[!F5+oKAS*fʸ't%CMmIʸ|%COG䓔΋1) í|U4+pˬ_wzy?NñpI? 1Pq1bĤrrRNc3vIbPq1pbRdJ9UyF:d#LjJ4Ҹi@^&=GO(Q>yUo|pg͞\_F3+F?'<9\%,vy9#`t%Ag |:Ó/- FX.$KYfwxyduZ;kVqμT0[wgR#TIJtb `N Gŝ)M-]?6uq6?K5ѰlGCwq=* ܸELG'39FON!B{J3]4N`-`ɱSߴIO=*?)fs pGw/|v;F=IYeѣ~R>e֜~F7 Z~ȋӯ9H 6,Іf/m\K~yv 3H~_o)S`A?9׍rKdFQke<⾯/|'_)/q(\vڥ וz045q@$c#z$XZi2XP'>6Kg)8^ie4jX`YGyp?enA(h#/N<#.C<ӊ9BڍGwBl|1fwI.1s@83H&?6`MƇȘfy:%DGYl,k|`@,zK%?feY&47XUX+ڿ6_ q 4x+Fkk_?}MH~gat:[;jk$:z$9/>35hkI'&RͿBO\J}jO?\m,g Qӡ5oԇlCok5e= c 2<tJ]cw1N=R'O Ώ?{ &! 'SXU 2ST Gk'vx 7"g1bE=>eN:1E9ZlɏgYہW :K2n;c27XŸ@a_J :2W .u͑a/GƟ~Gb0%P +JiK(#FG֕A2__#)+g[0Rw.5ٿ?Ns},xa&k ivqN AuXd]5H,0b\J )lKYF}Kl{Z۴ƨ(˔/z{}8/G/R ^.'%/ufTZEzB#jŁKv]9FY><]X#J5UpsNC̹Z1 kCo59h1}[ ZB5L,/|o~LKkyk׮j \-Z2':܊W_3z^?m~&% vh"-9cɀu8 2b̤E 򐖙xMb!dx2ilY;;'-*O^[Ӽ}\<[gYB0쐹[V7eD\yӓ6%[R"[ǟ5gRF4ߦ4.88Zq!dp|RJ1"uJv F_ vt|1'@Hfx\JP) Œ\AjUA|NjVŸ__gJTz%8^  #0 բlxjWjgJƣ!?P^_;60vGSe}T @HDǂ< l:, V岈*i9!mEElJ(=?Ku?Whvp)ҍ Ţ3LEz|_[Pс#+ /-K2-P?Ϳ @!ZFQJ7i _G8hi'My7%D0G~!h Gk1}JltDX?WF싶,RbsS{RfK+BWsss2Sx63? /e琈gf _!c?f9>2:ǍX<2[LO^q2ts> Ke.=n4xxZz*k=8ρ*R󛍮exn!wj[nάmfD/2l]qcm}jY_C9?0!k3qIsG^IķɃE|'߁H8YTkI/a}Nف _v 1&ZӰhs$ \QZoH0>j{2l3*T3x*+U$"yOm`:?A?l %wt^E3ysKQHEB0`o`M {=LB@! B@! B@! X$ ,4yUAB@! B@! B@! @F : It ! B@! B@! B@U@D>**^BB@! B@! B@! #еx|kD\!xlRB@! B@! B@! V!];ߧ 3`X85IWB%J! B@! B@! B XF8'V!EB@! B@! B@! V) q G4yV@„B@! B@! B@! @ c̀X%8 ! B@! B@! B@z 1 A}k$ ! B@! B@! B@y}OLK8N`X5xB@! B@! B@! Xtt3`V|lʪ ! B@! B@! B@@2` A}+B@! B@! B@! CW+'&Vj#B@! B@! B@! <];:oT~& u^)B@! B@! B@! V'ɀP88| o`l! B@! B@! B@7`8'Bk]_! B@! B@! q8uQB@! B@! B@!N# :jB@! B@! B@! Bpֱ:5zG5B@! B@! B@! :}kԿc]! uP d! B@! B@! B@ vXxLTO9B@! B@! B@! Ev'=9¤Ǧ8B@! B@! B@! ;5'KT ! B@! B@! B@t N`=O` }kV^! B@! B@! dzX'VwOHB@! B@! B@! !XR! B@! B@! B@@>Q| U{)B@! B@! B@! fFgxGWB@ \R?Uud|$ 6,k?o+ B@! B@! A,|񇃝G{k[81|~[]o(vWB@ \. F2,Y^x㏇K/tl GqIӟƶ5XQ(B@! B@! @:( X+KVooo~s;C(C!!0P Xc/&ݮK/?^/WGN<$Kkw:A(]JB@! B@! @k{=#XN`MZu' _OLgɀk,ՀuN9T; _7XʊajEʺB@! B@! @ux[,ʜ:uj 5`wi0hРg}oyᩧn!o:0tpo)ѣG=v=lr^}dPJB`B`wv5 <8L4)LoE X#Gnls[l͛|?th\Yi{_;z .SG~j+ƒœ[bZٵsEqZQozXrB@! B@!  p@ `ԯ5k9bwտ ?[d/πuLJw0mj^{ ?N _|iNqg?kQ1b0a.,?mڴ <3Np 60&\w ζo83:v0dМtRَ>0kpm ! :{O+1cƄnn___xߓ Ǐ ?p /ՁFu׾V?z\{w- >wQTk߀H;RDDEߵP:J"HEPA齥Qޤ|ed7ٔ I󘝙s̙߮:sfLlD~wvAjժ~SF,a@@@@ y -RW)V:}y٤iS2nX5i`t&Mޔ_~{;h#F_w j$ra9w ӛ #dvN@s r4/}ݣBCCUJr59vH֭ %wT\;u… {t:|Im= 2'K<"   $?a^D́e"Æ \)XZ&N))kӵkz:4cJn]z'̾_].3ft|TYfʕ9;txWj~lUsZ~^Y^g;*]:t]ԶuZF-a{/m˥\׳e"fϱHyVMuu0fĉig_~7o^/rƓO{iS6MOGWmۺUF~<u=:v&䬠VF29IbolJ>޾}[~'>X_KBGS'one+    @PG:RS~*9>Yk%M4DM:U~_釧VL,_>@oIv픡C;?:|L+ZD nsuλ.zú[Zv_[l!_|Qw͗ u=Vݺ̮j>.6P#.8GE@ oV@@@@!R} J3zUΜ9-} .,ˑ3d̘Q2`p>Yk2xPӶcS3YfQY2dK~r `#+V2w#ZM:_yqe Q`J:ud˖-J?>#}s#X>~8ɗ/\rEZl,s*EOYÊ7ݳI=SK'w>'onee@@@@ M("? iﭑKX njDQRlZ顢 `|4|,X̅e5?>|I+X޽F)F9Î`]?Fu~N3U?p)W.otM0N''1SǼ ķ?oY?wn?ߝXogy,Koi:    IWJ!nR=RH!hR]vӛ͝+eʖU*MLjٲ<6uܸq ϟߔ=pႳoV>T~@ Yx@ң1~X}׸qc=?=azo xS/|wj;ogy#O5@@@@M,BzYSy_2sW]tI8 >)F)E3CBeŲuvɑ#L4)~ׇ*\>}T 0gXARGG AȨM CȮ{"jիWE:}J[GlڼY͊7{ ۠7N荓}鍑7;u@@@@#BY[ˈ6hARLKJ&Yw7ը>}ʑ#&q7_7sV֭nz漋/yu @Jh߾ԩS\o9Q.b[SNeVV-~1K.ٳr|efp֭N VӦ;ˬjDo{uG,ZUU`&}6M=;0)ݩ++Ӧ}4xS/>ߗ:    @+R\ FM[>_Eo~9sfkytkС쬛/_^ٳ(PKӵjJNyu OnjPgJJU̾?Vs\Ԗ,Q\կ/.] i$e/Gjs`x KTmvmCDfӚ'*VtÌdN"]gWk-d2gr{_ˌ3eJ|mśzV *|wjJV,|d#^|/1'@@@@ ` MՃG5oSR`i—4iԤH<(󠄇[վrRZ5ɝ;u$fKԞJ̙3}6ٽg,PPJ.\=j^rY+uW2f'RPa5kdg]@aCdIG…w^ UBj>òR`Ag#tRN?^'Ku\xQƍ+{̶ׇ=rǏt Z}|`PWA3ݯ%JۡC'xǧ]V|;X 6|ƶoߦe^\/1eA@@@HjRK`h4ߧOV;Fۼah`ի4m̤r׈n?SL>}D` E٧._Lzn:փI]p@=쭂\, @RУ_ys嶫7oT^QTX#GN="uڴnuA4wt姟~4[1qAoYNK˛7i^WM&Oϛzqȥ3l    $;D`M6Ef&N@5j-ZL$mڴum.\HZj%%ԯ9t~h{ vEQdEdV"IZ@_r…#h#'e*h_|yyꢃY͚ZѳڷRjUڴi~1Y|Xҹ?JG1]tb:s \6hP_Zhi/[̜9yl䕸ћ}.o;*ߝNXg} `y[/.ߗY@@@@ y R&,_0_?裦=*eԑ#8M:YS'="7q^0r=@(PT *eV U?* n@Tɞ=ӁbՆ{cI9؜uݩm~@}N]M|_z<1    Z:|ṔD|B0Yky@@@@@@ F`4x>W @@@@@@ `DKEMS"A<      @Z~+"`~/=      "Ԩc&* F`w@@@@@@AҨIF`%7n#     )Ie,}aJIo/ׂ     $?7,?`R&#     )GIm%""B`w+A@@@@@@~*`H2       R#* s`@@@@@@3XtX      @Jbr     Lna@IDAT @#/#$@ar !    LW% IWÂDρE F3@A zP@@@ ) J}C LFK:F`1+%\      \,Jo%F@@@@@R9LOBRƕq      RR6n\#H@@@@@@#p?`J1*     $czVs`%7#     )A@EK#=VH`phJ.@@@@@@ 8 :J$F@@@@@R#uP `K2@@@@@@_aBPV"""$}      @0*ըG r!      @2l.!9I@@@@@@"pVS5+B"`Jyo@@@@@@ Y 8 Ss`5S#XH@@@@@@#`K!F`%0(4\       iSV{1      l) `7kA@@@@@z#I@@@@@@"תmLjY3>FM۪k `Jyg@@@@@@ }:ajD.MZKDHPph2      ~6=H#"`JxK@@@@@@ y }2wrԵ1+y@@@@@H~%>q d `aV~/=      " 8z(D5U)r      @0`j`R&w#     )@jܬDDDHr       @+R< "<,Ȍғ`$߫      ^1J!DT9+ٿ\      *#XJ'G@@@@@VSJ&     35.(084I^Vܹy%KeW;KԩPX,\إ  q>@@@@@ `ś)#3?Օ9rD&MlkF~IԩK|3- u)cýaC塇RdȏWR{+$wܕ!CիW=)T)7ٹszIe'N@@@@@\)UqS=XӧOSW>mBݺf=%U" 64׳n:YuD=edɞ=ܾ}[;ٳ碴P0@RJe͛+1uɖ-ܺuK:w=ٙ> @@@@@$ `RtK-AsL&џ]:u ,QR&`Ŀ^j(o_].3f̴_zOW`ͼyhQۺuKWsm܎*[ 8P GåkcⲒ>3GA@@@@@ 9 8F`7K uX#Ξ=+^+##@o6W_}Tۼy|(4%Kt?}tѹm4oQ\n7*kb$L 9@@@@@d(p/`4nt?F`=V٥|@̙SΝ?'{S^u啪Uȝ;wd߾}oWK&k,˗/~uj]_˘1cL(l#YdkuLXAɓ'dȐ!f[aoeD@@@@@,5(084bk1s`'էO/yL/N9۷o}DRrɥF8e]* 2ĤiV3rM0K(jƑ:ݫ… K"E^ʆe]f.i +*T4e. |YX1GPŋ2nX٫cԭ7np6vR@9W#F)E3 [?ʪi+9_/f|bTMK[ԣtp+_*)R8-[VҧOoc#G;vcco{(ʧPBRDI)Tictw (`{iҤ1}Fq-\ش@@@@@c,BqSRKb́uڶuxSz?kL3(<<\un鴅cUzANo׮jpj'u1埫k]ҿ Uh lh<])ׁ_0ȯ_].3f̴̫7>;˔ɓezQh?%UP/.oo+GoJ6 g^_֭[?q)g@@@@@|#Wx@đAҸ`VotUkVL'v4oY-V-[H_4||Y`MkMVRn=GpU$۵:?o;MU+t̚=') ȯR0s?PQ*Fc_ vsSƍ^3~,\X*V(/}5)׮]+O0&M\rIlBW#oV *W~m>zطs `km۶sVtJ/aҫwG2g.@@@@@H_: `֤J J1^8jѢZ\X+vtn]g#<"k?c6ׯ[FV^&ީA7PVFt?xF6 &;w: F>$_|riٲUA:Y| .9R޸qä'JΜp5JXHL DY't-ɑ3d̘Q2`nO֨aC{vViZUͫW>zmvӆ.85s L~DNSQ `E!a     @ HQBQ5/Ry&M-yܟ+,+D '}s٫S=S&b/;zΤ9Jj?{=whx(wm 8@ʕ 0E=h%5 nZ7nL2٤U ?<ׇ=r*r-ܹ꫎ZKǚOՐ/5 DZkk„q7o>cv `;u_L̙3@xE@@@@@  `O!V."M9… ѐ:>,ÆRd*ޫRDIsjY۷eӦ&c5AXV#5bpپcgLR>uɖ-;w3熚:mK iud裏9Zg:yY5<6X=K2w$o|Ʃ9t1B+     S XEXM5+R&Vk@~RB{YG '=^M*UzTkORƓO]ztՄ e۶.`޹mV(ro7+cFYZT5)S6z\YzMgϞR@S]jUH=]:5ßk9W>x+o-Y̙󵳜@@@@@#$u A>/9sFo&얂 JҥL*@=Օ+U_'L]Gj~l:uJ/[$k%w\RRKҥKgaܹg뢩=aLE}6 (wIshG<+!!/_>)]/nNuE7v ǏuK+QT>)8.]2>A_]Gɒ%x>vIt Z`Y"     l)SnK4X*\G}=RHzK… gܯ/^ܹ#z)SuY#9V&'3rw"_uPfڴj ݱ^ޭ<^9.}߿2rzu_͚K 0SL>}z,]~ w>Æ 5*NW e.E\8@@@@@@ X"Ts`X*XӦMYPkb2BF6nc>u9v޼&]da.ezCZj%%Ԩ4i8uĉ <BmڶRJu>sj/˲#z^u@Ee]F]%mڴt c|rYbs\Wi'}ڃ]+V*_|ج~Ѩȑ9ZKy߾P?;5_WWɘ1xzϼɑ#Ӿ.S%`WgΜV+mL}V*#t=\#;@@@@@@ l#ڪ#|JK]:T.d͒UN:1hՀreM˪U=U~ݧ      @4~EDImT5?B0$!      [C*`6r2oi@@@@@@ zg `#@@@@@@XM˷Դ      눚ˌRGzsu@@@@@@ `śE A(      @,)UqS=XQ@@@@@@ L pB`V       @z~XՁ~*      @B lZDޕ7#@@@@@@b/`Xv$ќ9b       ,鄩{/nXX Ls      pzoƮj#      &Kl4D+}i @@@@@@ ~'}w7[CT X6Au@@@@@@No)XJ"""$ KK      0)uF-,5V@@@@@@RP)_S#D"`%- !     Ea7+ $`\9@@@@@@ Lw-TK"@4      qGR-0V\9@@@@@@ Lii$P      O'L5#^O+s<0r       @B lzD.o) v@@@@@@&XTKlZ`9r      @ ?%WZ!@M       k֢}7_}!oX G      $_arGKmLÁAJPaC@@@@@VbV      X!jJ!F`%<2-"     -p:C lܹ-7o\ /L+<̑BP `R@< T,/{w15k⹢仯ٶXE@@@!1SVI*U]ݻw v>N8X "ŧ@U@Ŋb`=7S"  $ =*G/zt~9H,_I.D+R< "<,HTs`IB"a@ X+}X>q@@@ >#CiLK> 3 ۷տ}~>NhC*`6́@ Q+}bX@@@JRb/%t| `5V,bVܔ-[-Ydտ.+ҥ~ZE]@WځO%m(A@@IPJ%_JPT/]~09u=ھ@aY$gtsv^-5=ɟ3zYɖ9K VS~߾}$sG\>69"&M6ڵOU%7Mr\~\fxNrfMoq;uEBOcuO9ybf*"۷o˹s:&Gjo7)WGI!:륆o>r1cfD! kW:gΜv2K%2f 2HTgq$8$~A}81XA@@H ("{kG `Rrgw[[W%o޼Pׯ4-Ʈț.ݕP@@@ (Ŷ8glLWvc7ƶ[I~J "JMB^}th\tgͶ2pXw*`#R ,G ʑ#9sʹ$poE@)| `=[%ʓI.栳QG.Ōڸ>q%JO;g~X+] agiqwLb}- >|AB;E*!uE{1 J6ΖVU9Qri4_{b2|pO6m(cƌuNAخ}[)]<6֭[~>m=vܹ?;HZLu#""o&SN6p ۫;;w(Oլi/[&3g6zΖ=zKs^+W.u:}j=^C|e9wOgYA)]`}#$X*Hc_]q^?x )S9#Gmu/~Oh.ٴy4h@!^ƺθ߻yESՖ}#2fc*,Fs_X ?"{g~MwU_5vmZAʩOڇ?obpy;on:lӪ>ό>3WL:KM/RIf_3+n#J*!C3o͛OFy u)#GoQ ՎÇK=׹sGY2sժd䩦_HG1oѢc&LofZ}%aS'];K %u2;p`׾˹&=lHR `E5k%6ʘYeT'/Kĵ+qV\=Kժx^z/. ;(y5#].b#M*mp9~?x(x֜`.?>m@@@ ^;vT?v:Q?|8qXc{|RRtyXn]]i`}4WqxocSפ-kHܖ:Ւ7sQ ›/UFҦIms uJ理d>SVm=Rk)_>}zɣ>fZ:n߾MXgRUQnϥ~-ݻvɐ{A2#?? "EHzM ˮݎ_$V}*]^VqoF.Zl[*UO̓j4X>PVHHH4eʖQYu>;Ϊj^{gϒ>ԉe9[/ݐ_l*-؃Az Շ ;O>^@7H뢤sw]Uy՗?HUlީ9t^x>,%r=׺xPҟ*35Fm v;pRGSG 䱍#'.ˮ `?'\M*x([,׌f'tJzLa٢e\LͿܔkˊǤ\,f_c]p6+XM,u},MY0/cׯ_,kK?ҥyOѣtnjS lQVM`:+ÇI%̮m[ȏG9{モEtaÆJɒ%;o͋RR @@@ :E޴@K?K)/J| `w+ˌE/,dc t3cRr~'voɵe̜&(fo`zZ_󖅺!rGmU*r޶g+~m"=jėۢ/۸kns~>7Գ)͜ŁQ#_ϺI͈=˧"szzi\߬'r=:b*oڈkK[W#UҤ8e(8GJD|ogy7=NztVBY`?!L+Ky*q `jBi˂ mZ+mڴuM.b|恨nd$jFN#8a8>0\rYZlrN˗/Ko5 E^)̙-Kb$빼.^ k.l#@ &b2)dx5ORmI1*[Vȍkb]`R:ӵrU/vTc'R݈7k+GHmz#C 3̈Y=W>:Z_.u9|37Vxxty~qG0YAٸkfo8#  @ wCUx p8ܸ}sʿ+fzdח?=(/5 Wq ^sP&E ":EfIj*=tei>p>eiҴAioQ²_W\;Yɛ34sՖ3Wou[r\t>Gq/}vMq `ujP:>ӳUr/t7ɿj3gLo٭I4UzcڵM34h9Wz&o4jdV]&Ml}}n;Nx1]*șӧ⅋PW\Rܭr}Otj>ygϞ`^z/{sXSN̜9C2f$GT?>#ưaDp?N?Z~Y~߱} ttY+~ڌ̃e  +ct+Ji/J| `|4m{ttQBLHF:0v&b1H'gV@RL޺}W͗uԓ&s5\^6 הV b كAD~1Ӕyռc==}`@gO>G^&{|t~oڈkkTP=*Ec“[;CSm+.VsrMtۯH#q3S07n2IMisy"߿/n<)]J|IgO#ةS51&=ߔ)UZEh-eԨnPJM8Дɀu__ΰ@tgNꕫn,p$Uh~XyKs`u*=[ms:˼]տu`NzA"/qv_[=w!C#ltɜ9sunXsf.+ntSի?f@@@G[ٓ ,:}`J\|Pm]:UkuJυi@:}K0@kYmxۧ]KRg @=MQjXKP?Z̓dO#اJXkl̜lqb|ϺxoO^t7m5e]ct緗QFl֭iyyZIF=_Zgޞ7XmM"Ẃ6,=Ʌ ܵo;~?lk3WoZ5R-FzyyVUfte˨hd V̺վQV@ x `/?,U*Sö4^Y~n2ZRD,XuE+'G5kur/{sNrF .w4=,C@@H"U^>4?]J>DvዀRt2հFиd]u JݼuWsG d"dfmt݆ `ݹ!S92Ɯ8sUNNauW+SPSt/ 11R˛`P|XBiBstՑ*s?}zuEoY爫M|}&ExF|XK1Ž^'nvn٢ً%tl]2y 7j޸?V6z);R&Vk@~RB)~o7D6my3~߷_kR@3.)}}ֵIGS@hlݴE2zU.HmnY,?2u >p"6Vl{nÛu|XC ]Dٶ~X9U~;XnIى   PfMԩ9gM?3NዀRtW%d k{||auܦI[+H7pfYTtQʬJ.^qvɻoHM=c6D9&4w_/[]01 .&V+%4]csAFt~eӫ7}&>x|[i#,+-]P 9!@IDATwt]jTR5 l봄ceTl#`%rMVR^=,^H]+WWMccYfǛ!zgL;w4R{r˗wrTԛ|ݥZ5ǐ9sf˒%|}Q:xXk_-ǎG^yO$uC /^TRj1'T+.R9o `jB8,]Df*IV#̜_*`wg@@HB/͒%KP|_/Η>+ӧ].K%Yf_X(#~)e!3/3Pמ+f s? `Y>Y>nUGbxns:7.+3^t~]czrrmdBOޔY1]ӿ/޴m#X끤LOת);u2N!1.skޕʕ:^-D}h _jڔSjv _ʗ/o.ezs4x4{-3[>xg+!dR~}t,XG@ i x `-y\xI4S#?<׮ȭ#Ar/FsS*=ZCfrL>5\cH>Olo `x K}*s0al映T*%J8~=mV(H{XNV@@@"Rt%dkKiM'l]OR(wnU)1϶hdлd~ߖVy))Y8Y_rL!Y9 ||#Jtfray_۪T6e`RRX6Sgf]&>ϘGK ߐ3E z,Y_څu/X/:{+H yMnpIu}BΨRޕ+n^Q9nd F#q́,XjM!ߏ>/[ھm޳[ ((JJ59&\ rFzO]6upgaׯ$poSsKWX Һ fږ_.Y_?дPm@ֹst2ft -\~;ުj^{c|" 6m槙 @1fIޑZ_ŝs'c#?OIX\XL$];.Tٛu|XVV~y}WBBC̏J*-8?`7v UbTxE@@/J]/Ηj.ң:!!.JŒ٥F|7gE`Uq>̿x 9F5scUA"=^zKM._mw,[^"zU^W&kw6t9 =#*HTxv)UPӁ!ӷʎ}Gz69);CϚ4ަeK_bN*zNM{Nȶ3RDr&UUL???F-j O:y:Iy5.{k*5MsR:ABݸT2t0Ӎ͛7'vRt3dP4P q\q-Yl|\wӿϛM*ZL. 8p`7JQ|֬ЋVA>HXk~[%K25h%iܿf4CfI /^M#u-,{Kb_G]b%sGώ9BicziӦʺu9{Xv1@@@E@)2)^-gg#61M/s~9R)+,J0u \s9> /wȧswʪ'] %DKt$gڨ.fÛ>ۉF|j36]wXF}ڌLݞ=;0BdDǝ>Xϥ;we]>&#$u AXӦMY {v/&#Լ!:jiF=Sc͛kR޵K t+.\HZj%%J4i8tĉ 'jܣS y*izsKqU={V#e?fnSe]Gu\Eu)sG^mS{ޛ.6p%K&7o%^jTy3XulXzYa<=LN}QF*Ъ>Nx\^[3mKNqf-԰?]/Unm.W T:MbKr.7+V Ę[m3u)(   PDV@鯿ߎR(}nYL,5/)7 ⹼EiSIc::5mPA*/~?W'̇:~/I}Tyi)B7b6y]y:O^ &uG&fq~?_*yrnkXr|䔂jJOUs` , s_uUKyGMo~V_+ۃ؉Q-/Q'kXA?;4YwNI~ S'~~ k%7mly؉> `NM6i|XuΚFw,9ԟ2֗E#Ң3F@@Os+/ŊrgAB=ؑ wԕ'7$ʕ25~2oIJԞ* `ٕ,H3Y8u`~WHR>gZZ <4ʑ I Uզ ۬͞]JKM U=j%Ң/E@@oeeGdKtW)^^66y¶&~^a!ڥacT^/%Jcǎݻg J ԓ.6lwqB8Kf?YT;u?ɇkՍɣs.=UQ/yu>    ݢ ,.yRsz(ȕ $Xja\Q@Uv4\XwA=?#KTG.Zp @@@@(,3,2 ˜      d#5.GV6ZB@@@@@(pXi!rI)~Q.      ?O+S\.qgv@@@@@@BpԮHsJl|Ou9B0.%@@@@@@-ڥSj A V@@@@@@TKG*x      [L+9|@@@@@@0Xj,RZ !*PNB@@@ tSC2 jHsρԳ4RV   i${As` ,'XE\9X!%T@@@ ``P%3`B0VB+"F A     D(dE"u:K`%:"\@@@@@@-pzAuJ0"d`@@@@@@(Jڣ2b;2VQvF@@@@@p ` ,s`@@@@@@(R[Ke`DRB\@@@@@@ s`!UK-́Z     vWzS:CN\@@@@@@ lX rQq      @ r0`w 5@@@@@@[6`p1Vth=      Pg2UKe`1`w @@@@@ Rji)MَDو {$2Xh@@@@8#@k  ؆TXjIt~4%@@@@@l`e.@VSj!׊     KX8  !H    l @ &T5r-O;@@@@( X5@e`%ֹB0!    Pd #a-`X{v%Klr8RºAT@@@@BGV5AIK !ؑ!é+     @+z:"'p:+Ie`Zd`^7Q#@@@@SVxFZL     @ .A @x́HVs@@@@BVTt3D]=V{A:}bC2'D@@@@ J`Eil@ HO+S\.q @@@@@ eI G14T9ɹ9     _X~i؁ ;kBcOq1+-v!    @`劋 <8,2l     @~ /I΃DV_/.d{K@@ k  `5qeb3l7ZouTTI?*UJ~79tg9h95@/ÇIٲeYoNS#6m5k6kݤM?ӷHQͮ2_/==]f͚ܫWO~{7XhX[l1cy    ` e`@ P*RXN2:uhf,YүѣGeW-@ ϟwHF3g3)DzQGwjzW6;4ykO3Rw!CyNy=, +   Xy@@1Cf!cuK-n?U2*jR5TFAYɓYdE=`*@ ɟ)?vŤJҥXbf?,ӧMBUf?ErםwH.]L>xK:vsMCv骇,'_ZeA2qD{{)`ܪU*W6o.]dA@@ ~}F@PpL}~k~.kρu?n=de2tp}tpSK//RN?{woiب*Uʳ~U7ߔu7xWM zJ e޽YrRdNlR9|֦T\3$kr?ٻ~YϺ~ P]g{:uJvy {rCŗ=Dz'HK3Z{^4so|OR4nDʨ,YKLީ0as\[LΝgm5>u\r o\a&u8 p,zWن  @ tYΠ>pA#2@ ǔs]C=*z,maE5ʕqRfR^,ۭ :wd&IN:W͕5ku-'m7nK/L/Uz].޽[ z:׫ Z?h]G>{^"V ɓO+la/~W6{ZԪ]*s+{{gdܸ}_t{п_^o-+W~&gk{}u79ʫOH-=<ĚRWMc6@@jo}/]'/՗gΜi D @p 15q.NԗR= wN6 M۩du>M, "M^av!%%9Yjի/5k<\ȑMI8r8Vj _l=zTM*~k?쵶iC"6jH41Cu|L:*j^u?26lX2!7k.UO/[6o ,Xs9]jj 2Ծu"@'Ǎcǎ[4JO-J%kvwf h?>п>jՒ*`wmm_o.qhXfg;7K׫+cg@B/V}!חf͛I˖W`VMj@@t!C_!'Q1{B+E9 ĕ+I{':;uӍ{^Ci9v6әbȜٳ=X׳L/7|7zϘJ:M~X̳._[oLNN#+F:u,guc>JOz{NԃRk3 +T0߀1d߯ˍ=J7nlD|p^@  +%J0A^(i ~~)Vw {O?tsc2Wټu2:_+ݻJ۶Kp,drE3gت#  DbԨnX $]tcb__zIV~~f,0Vz7v}5|eNjNL>MU&Ǐ{xC<+/bcͦU>Yf#e]nuoz2eukiӳa ->=&5H*~~O[)}`5{W,ۭ[w;6qkuȳ<:))e_lhJ@@+,_7^ VA. Rs#+2c `=*uTVt!]Ww=rEff.bŊzn}y1cTZ<ҥg{ +V`J?Q2sYs]/Ӥ<=j٧ >{ݺσ|lܱc 66!gXY:uJt]YzV3y*ޕ{@u^1=1:|R1[N<)IX ֭w^ruec kՉW@@*PNybg#]eL01"C(29$tD#,+/,?N`=hzbS+%ՕvzKٲe0k~+ӦͰ+   pgȐgYՓ&=#7'z7\q 0Cۡ_Ib!بaL?tu~@F%76eymygѻf}ara׿dwg{N0XƎ[w{֭|?O!\zᾆ &+ @nV^'Z{Zv`5{`te^"-ZP[ f;5gl|#  @6_הxZtQX ’: @d Q5V7RXQ7v4h믽&ޞ8qԭ[Wåzn]}̝7?9tܹS[)ӣG7iӦY_l,Xjck\X]&LhVȐz;vL*AJ-W^Y K.7=!vY_=ẎXX;'@#X~_7?2u }:m   ;ҥK $?X.,]Dlsʾ>H@X 5={ԑ@  @i3=G !͔9ڷo'qaD}f!{RdI믿䩑#eW6N:Ι#ZcY?yf\|qvW^3^sKfXJd>֦QYhv @t ^PbA&x2,ݼ; {Ou ĵ}ڹrg2{\mAv.VVM3c gTw8Ӌf`  @~/vsW,_erE+bGpԮĕX:K= ,ͦaj֬RKxIlkx+V'|nd~{!5Rk)WUĤYo߾]ϛ3 !l1{ujt%'[6+ ]͑7ٜ<'׍C zIs\ yw 枨+=4@@@@ U`hO+Nt o    @ *Tn.DV?5x`?iס+CQQU@@@@@@ RM11Ŵ1IVv6B@@@@@A !8lcr_{rs`CQG@@@@@@ LkĐM3ަi      @8̘2U'`IbCAQE@@@@@@ b<vqrVv6 C@@@@@A1m|OR9Xp7uD@@@@@P3ְ'U,5C9"i      &)纆>R,"+Lzj"     *:Hb2Vv2B@@@@@Í䀾Ng`%1)%O]@@@@@@"L1&B0:      (U+#-I{rI@@@@@@HpԮXN,      @x 3vR)Xl F@@@@@C:%SiXѯ4@@@@@[ڣ2b;2V"G@@@@@"HL+^g`+:      (` ` ,HbRJ8:#     D{,5`\ `9"gi      uc\NXJӦPm@@@@@@He`%'CFB@@@@@@ ĺQe`9B0l{#     "`B0A5Xұ4@@@@@W3X*˥2B0\z#     !+-I: ,5W     mADgJ6#     ;8=Z`@@@@@@ lC 玤      @ &T5r)K;@@@@@@pe`%6B0\{z#     "`X{v%Klr8R"i4@@@@@@ XB0;:#     &p:+Ie`Zd`EZ'@@@@@@ o>_/.d{KcڒkwY      a,5nGAf`?N:x6o,F˯xcS@IDAT `mݲEƌ_- >Lʖ-z8pPv!W%GY#иq#;^zrJɳϖ'N?,Y_tߋgo؁     M=V{AڑTpCZmhuoT YKŕ˚'|ZUϓre[Fd׋9V. tvY(Y߲櫯d3} lD@@@@@ <N rYiӦ +&*U2~w_76_|)L;]~"i?姟~b+Uw^Olwy[>`W9'н[WMџwkWr)ST\{oOI#FU%Ϣ׉x     @ 8j׍qe9%6C !\C=\3q65zL^?4&SNRW kH~cC.$F|IB-f2UϞ=䦛!%J0>}yfg:     ;kBcOq1 r,_y `/_N.*TcǎɎ;%55l˱ K4ԶEK.-'?'+V=/֠e}j I&!}.._~UQO?%MU/<3ilWY_Kll{i߾ٵgy^|}+V \s?~\;3A~4hPOխgw۰aC;/FՕ뛬-j^]ff}ٛmN9pl{U*W6on>۷oCXE@@@@(dO+Nt+3.REz zY7h޽}1̱9S߾}UVT,vf4>s١ւ׉2 $2IiE䭷1GvJ\K7V.o~!=Ae˖k֬/_3>k5G؜9&ֹyͫ_A_2B=゗_?o͙1cTZ 5߮8=C@klݺE^|?~:;ǫ58wH6mڨf[=aԲŕҡCY? -_.^zhRN<):T^V@@@@@VL+d`M4Qԩ4fo&͕5kR&cA1ce9 rʕsvSGezhk&W_}sIllykh;CxLeKʂW^5W_ғILLyWW۶Wwݩ1M#A,`Q͚5M9eP>iirUM` fձrlTל0qױոk:7:VXNIN}[cР';pҢEKO@Ye[CO5 ؀     Pft5RNLJ) 9rиȑLtJrrT^Med5/v=*ӦNmjs>4ت; 4g2Mj׮-T囯[6j40rќϏ@XSN /Мe9VzΘ_,}“*fߛSӮB 5` (:efyO?Ik몫6sh[o)[b\uUK۷20k~+zbbfƍ9c2&M(6n3LR>S̔~? "M^aqP azٞ]կ'UF_em2vx! njkJC~U_a 5o&-[^e>{:gq      `5qeROqCrk>=QYVzXu7xM0do׭I\f;|[볨TS7U?|fۺu3˳֭{‹}zeML yŗY1j6ޣ9dꔩ*XX? 4W=WfP_ 2ij{Ҷzz ܙVtE]dΜ5K$k     E$BPX*zX=zt6m˖ ^¥(FJ*%a@S&cXA `GzX:IYu_|9LYFCW^ySLWk߾}c`J7> +X?{p&rI/oWF٘gQg&$rF?/!Sw!Ce)o%*kȑ&Hy&7nh0F}yXZx9/_s^kQO|ztW{o%-Cn6^g=޻W|-f=qʂ=sL~)[!Ny@@@@@CR3`B0V#?~l/ӤZjrq޽̱AAV]}Z,_tvZ7 `_I_7#}kv`ߐOJM>Zܧ볘]p꥗^s=OrU w3TTQ=WJSZYh̘f@}$)w|9|F&]vib;Kʨ l7_Y#SM7뭮V}!KJrJDYŗ:]`+     )UG !tJJt&Q s`Y?Ct~F=eR@W0Z[{ dcvuXzxÇ{Tɸƌ7_,)=?ӀLM~k~Vn0mi~V,|)[]+wyԨQ̅?͜TQS@Qn]Ra@}>{p;vlaFx:lT+:+qǎf^.uX9 @@@@E X*+S ,uU+&ƴc a& XH*5_X>ڗz[Y~"<3Q.S[o""/sx뮻n=|AO+@>X7&]J}Z'TGwI_; `M Ly*V&OԬYK|~uב#G\OW'LW^]5Le;W 9֢k~+ӦͰ6V(    ,=*+c΁e"nرyV]k=gzsu@V̯򣎙k]>F3u6f7^n]˖.j 2@/_A֯NM6`Ж#G={?.ST\z|:O@>X>;I.bs$Y+ȬYrB0YjÆW\a5W f.6_,k.[eV 4^KErMjժx}9gjM@@@@@p!M+^g`Br=I6mԲe*0ոL8 hvsu@V̯֧W{!sɏ:f9mCv,]>31Q5v{M[U=4o+ٖ5Sem `W, 6u ]JJ1Ҭqjo sҁqƉә)g__ҥKa .ꕸrf[~f`uYNs^(vh g+rms,Y,\YϏVpL~*sϏr@@@@@ l,ILJ t?49jyuv ű~FM0 7su@V̯ORR<әH~1Imr `l'%KB;g| "˗rYgG6H `Wv+q<`3'sӫ^^=iܸ+g_ UVU|IO-?X͚5ώ|}^(Ыg~*_Z]/}3.z)'gOYVy2Ge03g }npS%79     Pt&W,X^,ÇKm,ժU Juu19zL:U%:{#c t9_wg&xڰa{l߱k`6+pk,'I+4oMgΞoXy}vsf:yJկ'M^a ƯX~cǎ U믿D[INNՖ:j2{R_yg_˞i.Є;I&reREЬ%?X=wڴ1f+;O?IrRfB*Q99~z\Wͱ֯-[H kH pz^Ǐa?7eu`ueG)ׯT{+KZK+*U9@@@@@(:G1Tu, `%jm*4iD@6Κ7oY}Y@Y.h۠3Ƭyv+W~&gϵޚ`uLo `](Oe2t᝷ߖE.:SI74^;Nٵ+M9 ;W` uEرce^@>XOɓ?wyr9H~VA ikɓ'Q~Xz1*VڙA0z=jժiͪPg.^tPQgm͞;9@@@@@Be`%\8 yF*sb1A֭[e̘~kz'Hqug{+V'|n_ ؅ _7A-7q h]qצm[t^|cv7o|#}k@}`.73Ŋ3Cm޴Ij;=| " Re_ʌgu@Zϫ_ :e͚N8!{_z)xV@>V2}n8nV!6d/^lֿ?۷[o{B=@@@@@@ Ng`% ,RK p8     DXǴ @@@@@+9t       @ Js!PKLbe     ')A\.8 `QQU@@@@@@ Ƹ2ҜSΡLV"@@@@@@pg`RCv)5 s`MQQ@@@@@@ "<8,2"i      6gX́6FE@@@@@@0s`9LjhbRJ7!      `5qebP.     D{As` ,'X@@@@@]3`B0VB+t;!      u:K`%:ݴ@@@@@@ DN!蔸N d`hOQ-@@@@@@ JLkXQ4@@@@@i3xb.*     D-2\"I)jZ      9q* پb      @T8j׍q:%`9T+9*N#@@@@@@BS .K !=E@@@@@@(83VG`(w     mUIs`lWQ1@@@@@@ :d`ū ,byZ     {$2XB      %: i=L)Qt      i3+v5#D@@@@@@pL15d`?iʹVt>-E@@@@@BQ1yGvq nN @x w=ҰaCٸiXIx6Z#@ p.   !(h|镮m%6>AUE+;*!*0x iJb*9 @ \PtEʕ+/>\>~ Nw`]o'jז<(zi^Wyx~ffƍM7(*V%wl_0@@@h|Ic8:ԛwݩ~;;jiڳ+YKę. >Lʖ-N){M6ɚ5,F@3Ǐƍd„IgvY+{uÇˤgSK6G?~C~=lr9r䨯bmz[nyׯg+E/{WϘ1MV&۶ɨc2~QҸqc9X+}߾*USd1){V@@@hH o~ݻvmVƮ$ؓ!U7Ο?O}\?̙3Lʱ,@h{+I%\DXé?S~GN1G1)}Rti)VٮpN6M;k̓ D@]MV=p?K?^yKEuD;]6 yŗ YB|C<.>Y)ֿ}ɜٳ};A!إBb3܆ ~tD+޻vYTO<8xо;P`5hPOƎ'C~Tm3flږm#hg    @D C+q ^=TsHρe=Z}ST5x)))2bȈ%q @ _T[&Lh\jL-[6W5kJrrK5jd~Tí:uQ߃sWes;]Z\^zyWs y{]$7\~K&M$##C~}Wq:!=ד'N5^[}HVLV;n[NؼZ״;$Cm.ϳyU@,ǽ~^zQ=&s ߘo @x>,W+f@@h U;qkCn:K{8@N3dyf$Y-zc4Ȭ/[jeʔ12?C%4h5!-{WСԬUmo'.RiѲRt*/9^Tix6mYf{[+-ʖ-'M6e˚";vȂ_~Pr-<z#GŋOk{C?/\7 Pcǎ  ?5r6-sKHF}znS䩧FyU4{wϙ(F:rכ+>+gzf͚K=]Vs[͙3Ϻy̰v{^>3;Wi6 4klC >㶽":1av2y;%sd׏ k?Ի4Tّsm-z/=Lܛo7ޗ~˿uHMM >?]ۭEz&$LgKE]wXn[|\wҸqc:kvmhso*c_/ `o7 ==2f6 ^bՇW@@\VVy)U+#-I "* UzJDx6 QBE"EzU Mh )*.w-$?Cvʝ{rm,“߿[x]g~݇wS̅ T`AljA"' ͟?t,_<4dP1:1!؆Ŋ9 xcg믿 j;n!}b`pAxo W/gŴjT"" b@ Q <*]B3+>C -b;۶m粹3>a`9tUwg,`>ב>1SճgzڶqEy~O!}=zDLv(/EE n;w pH~|m\O rsw8m,[z)9ct)({u=Fn8p&M,׽e'W6&;FNPO~|%mٺMJ~q[, Ͻ?]=UVp:uvOv F9ŧ{Zw||펧>׿oUӤS]g!}لo_3 X}`dd=u V@@@@@"+\^*XlQ$r`Ez]IJ:ߠoɨQA)?C'+&hm6aC!ym"OL$Γ[N(\.lݺuOFq2ax*\uC$fg&o޼Zq5kgjUlkYgΜ4Aiyȳyp9o=ilTKc/Rrs>GϝlˈɚUc: dj` ɲ 4x7ߒ@,?ϙ3g "wwoMݫWAk֬J'|Ws٣Z@X2ӇK_1{Y}bN(9r(·GI%d^]S=ow[i^m+<|bKA^1i;'mEp3܎?c̳&(Q\1]`p&`Kx_9vݹsbc3&{`EGPpbM! zݻ ESN]/m޴VB;VjX>KngΘg>tONF PݻwhH}I8:3Z!flmUK,\+}jժ}_}-Z׷K.;юwޕj~Ens?6o٪jgN^c2bGRc%.]86@o۴vڶm~Ӻ;vԭ{w)$ԇph,rvbݵիi;xr+y &O!}/4pvh6P_###󇼪-7-[=<h{r=ʐ@P? ,\_…"f,Ol"+|}Ȟs&(#'Oys8_3bFO,>|V]\#)&5qukҪ/Vnԫ[G Mg͢"if!xޡgB?Bo7߾lgL+ϷQ|<<BR&}.Z:И$y j*n㺕!3x`wBCխWjժ:~4VmcccW;_i߾-5lHڶuK+8'݄+7矯W/9?Ɓv:UjQ\Y19Lzbq!:pvBi ̯ .]: 5Xȃ:9onݺI@aaEsh?t ͙3xHng$3`d֬2DO01c M,kwߙqEM:N Xo'ٿ;XX~OvzUwB MReS:lee=|OmYeG qsw([;j`={~ghEXrEV'(#+V|7n, ^1/똽^+=ouk׆5j,ŧ={Py\:w !;KtCtY3nb>巣s}M&oL]8z<fXy=%\ kA#c/\EKu{]1JM1?nPVp 9l٩j,$Z8(Y/gM2Uw6T,_^3f ;f%PHl|tʝ[!2>\>>""]{͙̑e1kjO},lٳQ (S%kԐ'Ѫ9tJP+ŃFBN9d ?]X 8e*Kbcc|}s=w|4ugl^Ѿ^m}UNOe,w>3o\zg\ XVy&t[ϛ֗Mߘn4l)cf;{py,      $+\_+w.*j>/3wYoY Ao7o# GB\w)Obqh7o9g*)ZW}4\!}zLc*$'˓\/ՠ&5|PyܵsD.Ah^X.>A y0UT8ݿQ9;/ȩˏ?(DG);{^wY1w!(jߓ}yS#sÙnp0ٳgDN./{8qVʖ)-0~$EDhp|Wת*r=@P?ǏKE7n+VjEXrEV*o~hVp[}ƦYy &7}wIK,|;9sCc1a8ߴKlx&t[ϛ6SoL_ry6̔Q<n$.fp@@@@@$5ڽm#OKD N|uxs;+֛SR6hfBP US> ?{,}N•*g?? d|Ā//SNQRv)S&$Byڵk 8sݓ˓'lOE@ {7͚jHZ!.vIX}&NO &}A޽vu]uT'Źn޼` ,rfNP?,+|۵mC7oN7}xM9w߰Q+c_1{)|}5s̕m;wuMotruH,\9${iӟ}W;hѢ%ϛ6SV6L:ϗ߾̳a0ÈaF ReK{EWy.uh羛 KhtȄm1k7;82gƋ^d5<;CxQ;+.E}QڴiLM6: t|u,_,Ń=z4l;Կʛ7g1B x; /V4lH~֬Y'CIC@ I3~W1H d,mN]t I9L9w>|0*W|7Lܹ)66Fkݵo}հa}1Aǧ7E=!8ut޽+UXI\-O0cjK_=Zj%gX4K'([Cʟ;]~4z'o:XVV\''phys?6?*ooٲ/Lv:f3(so}9r(mf͗zIrQ0)9cG;ƍ){Xf6*O}]\ XV͔7z}d0Sva尀X#TV|y??ENWnޒXC(?J,2oܸANQH9ZDI-;N6N۸oV 8Mupn=G9~}yC2X2eS SKb X*K6c,e >A3<#[0xiҤ,¥;n\N{(ΨCX*'fXqʸk=/@IDAT@`|,d;sİ~*Έ܊PPcN?"wNoLY_,+}gLm@@@@@HݿOyL3Y7gp9nJ$n)`]X-q)_OϢJy.'߳g"*<ϟ7=*U@Y;y (eK͓իW)cƌ2LWb Xj`mAb  @`.˪TR.g~M ϛ7_^[}uW5kVE^C[ܵom>]߈`}1rGT`Amq£>Q֗ w_\ޕ_GʟMzܭ/XE;w~㲈,|+Ca/ot6.f76o{>ʻK1z[0.{} Ж1B+.?.ed֭)B<Nj NީsH^RQu?_;v_T]~6}-$͔)D3SClFѢEw,7nx_CWLxZ}׮]4LשV҃Wa3gЂ⥟nM[|/ףo0PCoZgy)-E!Ip}4yTY[|ٲe.; RUXĸv5uMf:`ۼ}/.[Fg\k (y거믿v!JsӪUIo-[O۾T7mjoFK,SU:|j7mw<a0S6HًO $`~tFߍp&_sRWP,,10(<1F[$L(WAsω2g-ә:u Iڵ^R9O0C11V *H**FE ~!D AC%C+Ƚ})mB ,<+WN)9z̭I\LҔ%s겓hxg̔gTθ'wEZ^ORskW;{lo#x=r00p  2 ,_!OTy0`PʼX pZH>}4{L 4׮[|`$Gr`XA" 9SGr"ʳBփD# |aj `%>Z2˗nܼAvNWA@@@@@@@@@@R3GX+Nx`!`jp            9"( `%yHt!X+UxHrv+F!BP,O`jR! <Ŝ9+X/0@@@@@@@@@@@R-VV@`H @qqDӁaHX"` Lw.@!PHxJ0@j's>ܯz`E,!`%,Fe  @JA@@@@@@@@ $D>4            X\X!q$`ρe!$Z @A@@@@@@@@@@@<aG6X(@*Xl\lC;Dm'vhBEG 1.Hh",VQ?'P @@@@@@@@@@@@́#r`I,fxi B*eρ.a@VH(X6x`%j12`!X `9@"{`E?.XD6́#C(e Gp            Jܡ}Qp J|h@@@@@@@@@@@@H Uq/_ r`Vr` a@@@@@@@@@@@@ Q 0&PdR-HY~4>n!&,9RVj@@@ E"         !'+Nx`!ix(   8 `%g       B@9FX%r`A    ,E        :D!BXb NWH6 `%       N {#gx[&!  PX!+!qn?:uj3t.%!`(2d@6[DI }N}:cFz:}_ON_.fD$ )sB@@B%sЮ]S$5rym[le?uy,L k7o> m/"E Ӌի[N8z( w?kɛ5)APeiО={}2udʗ/??-b7_D%      Et$`DEŜ{`U~Oʓ'<~zڻwb  @ , ҢΝu3zԻwBrI'P|EPeԁ7^/_R7oD~t~M>{N9s椿K^Nׯիר]~>}/̈g$9 `IDE܉g@X#)8"N;woߖ6l!uuҶZjպ4vТEKPX `j|/߽{w3ş*/ՠW_}J(MV?Oy]tV\tk׮?{Iyܯzk׮ofJEϕ8޽{ƍ=B`R X3\$C]b*ē49giEbE!Puy0+(HXg}J6mqU SH̋Kkذ!Tl9J&K?~>|ԡ^bWs#1\/` Zvå֮U:wBiӦ+ZpCm$eu!,xycbY)G. +s`%,;*AɁ-Fŋ24|ONΡΜ9Kǎw:xZ'N Necș#'=vL$duj5(H~ǝN;eJʖ߯߸Nlg2Sd *S ҁ}r~*W$9sΜ=P'6@@ҥ7pi$:nsywvIEPdcƎTT)ʝ;7=X~ۻo_p^͝7Zj%=ǞܹC/J_5\3/bF矧\r7tݻtCx /lxlgnR~e e̐/]x.;~VZ|un޼yl{Mz*m޼Y-[:~8*TrA˖.B Si .`Y_ }s wc[ X\C1x9z;^?oBŅZ > 3dt)[yԤ$o-~έ3aXH1Jesb,Ӌ5j.zQBdۧ[/Ou)O?-4c,U*SE,pE n߶{9R*=Z~M yGֳqZrVZ-B7o$VCd3Sr3SL̿={Ȼ/Աc{\ =7ou諯]Nb:Q%B|p^0SX&]^Ŋ(K,VΝ/mQx f͚T$bo˖Nlr.’ i* *;>)3ϐ X|?ן£(6=w3<_%`b7o>ʜ9u?3G2뚗ӑ4tpd+[֭4.FOϟsvV9zh`@@@@@@ HVgScO6Py^;EkWw'vP"eb!DՀ@mt&;t M8؛hʔғ^XyӧE 򦈎=?핧rX"ˍ"l,^~)J6xpm߾t1"-WJ. *{ݷOq28U L+WБ#3Tx1$g3F#'P/-pp*רaj׾ϳΧMPDK.qս{W]i_}FlծudvTWo3AAA"u?[1Cw !kC 56mJXpp].j{ {m9w'LmZy+^t˗=c      O XOnaPeC;=A"rI G|2[&7z*uC#ܜ6k&ڳMX;V'O݄@+4W@%hRڴi4 |E)\ ;V}]xuU>,/[c|ٗRF_К5!sjկ@nj]^qX&m\ " /ݼόչSyx ?1RZ"DMM4{ʁ@J$Ы❙Kw/!nOX=UzkΜOh׮U6o֔}'kǭڮ/[F[nV+͕`-nވ 㪔'\.]:y8cJ:rݘJ=unݺ%a~׳-Aښ̽9-`q ӦM&yb#Ҟ=viu\},ԍx-Yqfw~oWcEܹ>T XF;۹_`ܭm{{i'>\"`y3|$X"`b5r`qwh[gQ^ߨkn1`{hqϣij#"OC<9CN2͡]`,fwa&M(V{5t`*_k֭oܿNu?Ӛ^ ;z7Wjעnݻ*7iD-ވfM_"T!{κ[!faaÆsϕmgk[ӧUV=bd,>WVkP576.zױś؎7}-wnK?QČœ8礫+\_||yUVU>A@@@@@.`IDD~7n&:K5+DX`DGyx_CF2g/ʫ`\+25hɹڶmv;}+t+;;uȁ1Q e;㏧SܹUx?Ϟ=CH}]nF;wD/CV(pn"͛׫p$Œ(d|Gk[Jr `;&»՚j.p67Ͻ\=yx#fp\OK2OdS> _Q8,s~ѓj,c[֘7nBٳg'd]MFIܙ?|O-{/6/!{?pPsfWݳ973>M(p%v9ӵ+\Bd*A5]НP>*^(9R3g'NQ85# jYPP35{CdM6?+%o;+o"<-ɌV,9[[b.\</>[x+V\>9xw-8 KzJeg[NU?)"'y#`.] D#nW_ё4j|z6 X|Vu7hŷݹĄO75I'Ps`uNC4~D{͛5f͛NFK?~KUiV~Ҍ (}獀eff+[ǼuU$.(`z{'dZ{ٺuK쿲+/aʕ]۷K'B߅FK?I_R;a4lH~K&gO<, E 9 XW\8xv"<^{*]~⸧^Wڵb`zo8'ըQ] ˔(Q JO=l8C5*@_YvQlYiY$eի݆+R05RV;W2x]|YN@QpQ@~!Fl;v\!,o[ϕ+s͊e~?\ҥK˭+> 68l[3`@@@@@@ E$6Pc"`0 XbIb%_f5wʚ5+2tP-}R<@v 1xMeϑҤIpq^2g'b0uڷo/=/&=ĞՒèlry&έr ʗ7(YB{38~WY ,F#ȿ9LٳZ?V+M4ޱ}; D}_%1QxX+ hO^1޿o X\|(^+-Z^r*`);>)~Uhb(!,dŊe~ՌxM)pG?AƊM&܏'?yl}A;sn @!hGD=/uK`"5l… (SL2or ͙3O3#0UTYK[gQR倌QjmziSmvvt+TJlF 4m  G~ ,l+Xij9?jW/fɸ(F4l7Z< ¸C7F D e ̛7}7f(*^,OTy'˲\ܝ SCV,?ɡR<sGXVWCnx|YFo+%ǎK:#Vs6 (`1Y}NLaA בZ=5l@ڷ Ν;K _YhիtR_o;oQ%q^LUݺuN8)֩9LrXVS™:vwԯ|vO{-5__x“9} G3KMNqh\l&Ԫ=^ޯf\cXP!YŪ+tRԤqc*%B fȐ{1u֭[KVֶ>V*`1 bI nXL:'Ck<(TLi}t!ۓ Eeʖ,+D4Ε+:rۦ v,vݾs[xv[N(\ %9UC  ժ]ap"2OB,zἌ%KB<{HretR=Gvtu)9WV9s)W\ǟ _'OR9aUr{fDLWS>Ou%}fMY>(      .b=Byfy\΁e܄ޮTԹl+kQXN҃w@@ Yp%`% & %6+2E5{&Eu'.@@@@@,,b,%^GNv ={ȑC^H1$:tQWW&B1ر/^/@2q(X1 S9{&|#`ρ%B K,5VfoPtܖ# ݖI~K ka;wnڵ9P*=oH@A9o,` +2CծTVdNʸ X U@x B+  TpTJ@B0L CTz#A@@ @ ށm        <_ѝ;w("F[6o Ӱ \B X'OF {Rfʹg<IC L V{h̘Txq:z;@ysȐ)\쉉ٳhG:uHGn_|{vW'<.]Hǎ={:ؓ qob_3$`Mt!:0яq!n~Icjּ9Obֆ iݺĩR5iDڰ=׎KăNOH^CB {1/͝;f*M>/t93w^ fŕzuoAٲe\޽{tZk1gƒ*v ؁SNLˬW^YfϾ;+E        (]bN%ёځl̎f-J6&u9Kf*X60?жm[O?} jZW;hѢ%~o&;=]k '[|9f~_sJ(F[PP]rFEgrZvH/(pe-[6kHA޽T(MC"\eٷz_c$[A7B"VXm@ϹL*T MfE4lԻOzi{>Ce}iv`UJbŋ{ɓNŮu;k`ObE#GK?{l5Ζ-+bu{.wtb<γVR%b/3gЙkԨNywIFۑ={v~:y:o?!I95& ͙3 G*UQfB}ժ2۷m%K9P W]U`f~ 4HzcԱ#u@@@@@@@'X o,x`Iҋ/3JAaʕqf=РM۶2 {aٓn޼oRH;r{ b*ZyRPy&ԣGwYTPxoh޼rOoݺE:A 69rhs83b _mSPUj*Ĥ C;vfϞoA?~jܸlKrhbBL˖-5)s,B-W._ݻwӗk[9ZXTDIz'6ѥK;zM+a9sehO3fj`;:uŽtO`Bѿ H `%l:RkM)m$qέӽ{O6+ˮ]Xv^foSpp޴iWX@zw-[hOփ^Ey7իR}dѣGsϕî=w, 4ᐯ6r%Ш UhknoA GE\Ze>>` Qa9$B[Ǐ'Beq[5{Ο?붌p ,P(`gQG~:K+WѶ+Iv}KBٱ}\2xUxy.!ϙ=f WcФI)'O WYukiժ՚}.+        @JN!BsNTn]IܛpCaGZ*@l4h(Ot\B-ܜ~^Ο;'rD)%u)SVZӦ䫍\7vj YzժUT= ! JCeDҥKSO=%Ƌ<=Gx{ jOfcڵkT\YV:=㲝/Vu7hW\XQ1p>J wܡ;OV/N+UUI1+cZb "N>d $wܢT]غuM6Nfcqp RtT4. <Y!9rXx!/+UD9w/vCA͛3qI"yfmـc/CeKϟR> 0ol{w>ʜ9 ,-f Wીe^0kѣxXx%*gbrxw1o~>TFʪ)eΝcռ;_BZA5[*Y;5kء=o@=v(;N;ϊ8ĕca>lƍR˖dC>x-U~3vr9Oh6ժ][6ǂӧxW_E87{ J|AI,tG/:t&Lp˖}~}PBYgkϟ!rt}ЫfΘl1TX1YC4~xZ}tXrMr <\rzB{:N< !gf᠁w\[a/C|}#F|$Y9eyw@,{Ho۷i5mnu%wt\P XVe3Oڵ2uC,wE;,VWՃOHBUJ>-h,Z6.6FmA"`d٘h 6=loժn#OFԯ_:v EN ÆkŭCֳW qYEje"L6YĪ\;/aϸ]NxG w8\DŽX"fAɡH>=q.5mŞp tFuՓNG#FjǬgmaFm[= ;scR>}M?Oo'hdc/h͚uNmt(DvQ֮[:[a/C  (=VސFػ9sm%<^}mڴ{#e^P6[.ժU[p!@xw)        $'^(W(1wޥ{`E-:ԝk"[%ه;vl"RS;!o˲'O#GiYyjW7kJ̓ݻٳu6r%f씍y|cյkդlٳQ (S(bv5((N_dh7bO;T;(]Q;RBտJ}*(NRB@fTRD( %$&Q Xe{zdg7wvޙsF.NOhvK;sH/ /Zj|6T&%t%W,sA!E3L,v]ϫle45փ        xq"P!&1_޴^Mj#,Vnf0+޽{h„hShƛЃƅ{PFq'M=ܳTx Sռ?,:T.˗/bAra[3tQZzu~|xC1cF=8'X~}M10M4yx'gb];_R X!n;C u&~^b?>u| {J p͚9oW^C{6*`ٝ fSCy5baCf]L5'-ndt|=ȩBzڽ%:"nʋY?VGF/_,R3R/yoX!+$5z`=@3He˖7n@͛e+etޓSJewXi[RRFk6r%V씍y|S{Rx uǹ+B-*-0C娕| RҿXx;Ӵi3}c&wgELjr"W%,YBv[gӑ^]pƏG2zquhEZܩ#hR~\f -zdI/zϮlQA딛 <9J.#k?t(V\!f͚f"',GksiNL }C޷iJ1 fZ࿛$`渏tqOզKO.`v3Lܰ:Qzpa2t i<sMaO%eew.ر]_58@n ۨ,{_STjUՑzܹBQM&,YRRKIIAn_d2z*[L:s z?l͛7.Y,NȓiӗKAXe6ttDՓYN^۵+ы 씍yc .[>RR\]۴~^zeidB:զM47]\ډ7o! ]+E7RU &N 3I%6k&\j%-^T5o i޼YŮ54C32/Cw9p@ϓQgtΜ9~>.6|;̝).`pY%`UV+**tuxf?ϵ~jX`1c:޽ץ "eXC{.4%y7jH}|e֋ԩ+svb;cig1[جyst-[A<ሊ *HS}7o;N/׃@@@@@@@r /JO-_&`{_]{s;Yu"`kq X`#)ރg;СOJaTZ,*+11F*\'9ro]F+ZjS޼y2F#佋x7`mxT TE"SʕZt=ᅬSlF. |ۏ1cMU{7ЮwɽgV*ǫq3j,4i4O֋m…&Pƍg u߾}?[_t1ҡÇH"Tb%͛ŋ4mTp̮N4RF)$hݴ~*^8UTQS}38ΜYuڱXڙCl[oI%J8=E{+xg&uknT`Ai TH-|\囹b%PbNf Xܾ`fn;'N/׃@@@@@@@r v"^/V˟/ 8Y9v$ -#։< VB⡬%۴! @zȒM,^BlP#͎_jE^zI[59qFoIݲeGfX)^[59( 6\UySkͅ~4k k' &~WW]wEf;>*TH3Y|H*1sepaTlY7Ol\my'J*eV(,}G.yB+c3nrY&DrcOeUremNm/;7ETyA;6s9vaw|/Y%`1;*PGڅ4:sLT!Mn,^#m4 vkTbEb1M,ذWʺuDvV&گ$+R 0ӧOiM3{g aPPXA\9p˗hSΜb-XH^d_|Kshr?;fjJZ5tEV]?Ѥ2}ꩧ_Q6<*6oqkVl*P;m>eˈpcv.ټqu.رY]V>::p        9@V X%J{#r6 XT X'V3^X,! ,$FYX9z|]9[UҥK"^^[fb{kYȴuj o• %$$RJJVӅ^wvzj]lgF_[=WsK/і-Oڱbpp!^㐅ǒ }j*T /YS*`*kGRgeҥ+WL\2f!Ck{`͜5;ּݹ`\9ݥωl@@@@@@@r*J-0/;O?9V|C a,J.s'd#-[a7 0]6w o^<Oo̝; ,(E}mxszy`G: .].]GY{&ku2KQdTvmtE֭WC;w-Z|5k9}^U}[^Ճ3s,BuX\{`A,>DK?^f\F=$kXx1\YںtL͚9Ήw6֪R23T0`Pyׅ2ԡ#OlݚBE!ϟ!@xwӝwީ4'&v瞥:>mذ[?|sB:,,)S&Saercx'LEʕ/\֭[SޜL%KV9A@@@@@@@_>̱g&!`uub,khŹO9K.YN>zԪKΝ;7Ӈ;0*TZ_),,\.:vl/<`Ѵi3TRoօ2%`q^zcHEħF͚u\6dtys ,Vg!`٥#g;6;%w:nxXXV_wߓn g߫vgS,0 @@@@@@@ p X;VFQ/`qs؟*P[oӧNQ5`<{ׯ_QFG{|1zHV3lISjը9z4;R?FE#GjleBYJjժJ;edBrR b'~\]Fo*F5r|uhE*٧8+ .$<%LZ?WRSsb~Es/!y={S3܂sl2TR%HLL#Y# X/ 3Ps㏹iߏ=0aRg<5O޽{i1?>Pe_}'6gP"4W}-xgfxf͟tnd}U}Ice{̬<\.G.@@@@@@@@?AzmZx`epJ,.~?%+GV}ڴR z*uez)E!創/h.mh,.E>x2pnݻRŊdH;uŰSB8۰~i>d2ڵkj!͛W/zZ({W^n]ϵp|Y/O0A ;;vlTep|uԥ hL}/\3'V+g`/(nDɒZ9y)$=$8{&Ơ]{g[!oPׯ? ƁޣU}w-y\vMGXb /dу*U2Wx_tR%a}͛l7&sg[j hq$s>ԠaCyۇ?NwV'?KtҬY1}u᯹\VMjTttsyݺԠACR|BBک-32?x8Ə2V-?П}Qyzᇋ嵿'}ꤱ||VZ/_>?.SP׮gha㗪Z/]yfbɇ#"F2O>eV">}*)RT(D-?Yw]OsPK\Vˍ{|: ѻX ޷jN;ˏW UڵF Q~w2Y*Bl9       =9Ǿ "BA 3 \ǟ~.Xc/5ki5X(/i=}4ջ7!Çӈ#|Y4>2R.h5|-/' ='bcR\^8K'>֭W+/+Vj/##0Y`Q2;}MNŊ#.Q`QV|LY_h!u] b8(Ͳ8pe'ߐL ,Ç{bЗkUzCλWxcM/,:yy,,f+WY*Qde vU5^VhjYfOo!eZf>7fsNMSIx>ƃQuIM 73g>ەwit7bSX2t|.9H~3i}e|Tšiޗcܹ0r*@x X A^G?Z][f&6r8"FwDGX耀eq>^UA$YzM& lԩoSb… CxbM-yӨ$b0%KhJ9Wf 6l\N-"K[yG(<<\<1Ȃ-APU=XixJ㨢85ASx11HON`feO?^F__Wq׹iߪoGE28zU)Ny^?lNǒxWs`BL{ tp3>ӯ̞=KxF"UҫgqUľZ0u0Ό.߸f]=i2ߖ~՟NK~]ƅ2C)ϫ˄xeO?Z3v"ЊKD%}zͨshϡ=%3JʌE#fMg8SlwB?2eg1Px7:~XPY"MT9Yem̜M7aarGF >؏ZQ&Md6}oK7 i޼Z/?Kp,Ϛ9vQm'}*\,vx:XdY-5nԐ„0~!|ߏ!0>xnM;˳@6m$y&M~ᇴzZgI=z(rx2_ oi?|KIg̺{zvUp]`,L8#oSZyegnp;w-Zv8ENP5[E}DΕ+UQGK΢րkPjڬMsU!{wM=t6uJsשSZl)^+Ah7 o=ޙ3ߑxFpƌiB)b_/իȑ#DgUݹܣ{7zeu׋=/TUkg,*DI3 =\l^_v#FP̼9xOO'NC\)]Cƒg^.A]fdg}˗w/yl }<= m԰>M&VYO]OsФI[{ELֵ =ӴnP G nAi@w8{^$oCpI dA6I4}) _V;=8]Cb.j; +/IRH|ol_!& _7n ȡFEI̽cbf=,[կ~(<ÈTӻEbo(}\.|/{ż^{[̹ƆּaϋN2W}t=w-E`[ v(UuRBЇ'ԛ5w*X-39tlO-ײ~_$o500>p׮ŻMMscgn1=v(M!<{yy{'hx1eʛ2\)O*Kyc0}ay;fzzzF2>U߫fN~{Nj Z>U>37r {wgiʡ &oi8@@@@@@@@@O (=zR“Xz6 XWDX?䱼Dހ~XZ+gkCޠ:u}3`>aԨQcmٲ]L7lP1/!2$ne&M(ˌ#B=~x}9^ .},A_UfsnWVrدmpڝ<{ o7fݩcj2ˍs@FݻHW>,7龶e*~ a9l:TyPusH h }ޏ[pB<8ܼz@IDATxU{APқ ((J %H/*Rޫtl}QĂJIT |u{2&nB󐝝{g799͜ R+?B %G./@=]u^0ct+e޽K/I=%..N>\Lf̜=Jf_{M/&7xұcG駟}N8A#4mm[п.S.>l߰zk%gI]g_ye9s7Kݲx2dPh2zp-Kqܸ1?koر@nNϚ5Sef'yo̙3${vٴi+|ډs=>|kC L0^͛SOiԮ~2gs~:ǖQFȵ^'4o.volkSZh!\xoK߱c?5i2hv˓&MkI|Hf[3d)99Yl_8WrM7M4Q>l߾7Ι-]~y?[ >s9/H6mC4ȹիڵz3{nn6fhYzMzM֧:+E߾?ٱ}[?Ӈ mےeEvz&}ԩ\Pr}Lv_C7quq3uk}~i0sɴi3t3),{3}V4WU]Nm2: 8@J*%G;~w~;6VffC݃_v~ ƍeaNNh rח*#7`5v^Ӧ+> ܜ    p \j+7$Xa f\ʛraF?H0t$W\}6lC&ry&5~Dm[l.kױ˖,Y,s^ϗW n}&o>(+_F1:c4_L?CfzZv9٭aAÆ!yf>Vv)p{g:1b8uЎZj)5kղ}.ZP^~yfZ4o&uֵf9mK*)osNY-ge+3gәgϞdL5OJLQVf~k(00 + ۷oF  ڵAm%KorOL̼S,Ox晩UGUfr'̀l,>7ܛўw8.h9P߽z=tN>?reɁʓOvJ՝cYYa{0kU 6d]7ҥɳ>[>r _!fnIB@@@3X n<ڛ ,[B0ΔvnN `ܵW)w:}ef+e˖e5MKj߿j7 kPD3#۽{wɗ/+qrͻmؾԜaf=?_jY6w%=zt6{&{3h+p+w^.d8C8 ,@lYY=P`4׾߫:Vu7Kl^ 4v~Eso~4N46eB]ߡC{V[K/b   čGΦ`S01½%4iK,|}Spt@kwڇ;153ѣGܟ`ٚ"=X;iy)'5kf!jV60cǂ1ZIǭdi99}yG:z^w^8݇Mt?:| Y`ϹPAqpr\o26ol2Kmy'[M^z̅NStw)_+IE+&EjퟰA!ޓӾu,י}_.E=Ffޱ4;.]ZnXQ4LùCA:Ң)))fRҔU˟?˾N]3åpaoLZfΜaou819ZqAyS?&*ܬ&8v/)YTTSu |YTkup:ԌA *,J| /j;HjY$57Z'ceŽy>}ȑþdu|N Khė^m-g% ^,fkرc!^[^Tqc`yLaV}Kcq~Ng륁a-QiMhߑܛўw8 `E}qwBZ52ݯ~? i=M0>)?P`4׾߫[ke:tAm/Ui}m#7m8.,gf]kǮ@@@JGwS4y*/dKm޴I>g2p}vaZיgCU_}%cƎ gl>X`+q-FN-3gN73PЬ&kM;S=`tG_b+Sɚ2goF`^G۵%Kj4?UZMK[1Y}~XYq/:⼺?+IAgU_W* %gΜ Hmߞb҄K/A8iě(X_3 < KT=&M< u/vzLp=NO=iǾ}XXC ܽ},?7ܛўw8} QFF5q">&83:q٪m^5y76Ϭ4=#ݿu~k8:Nfn~={@+q&m79"7yiݦEq_ C]gW}/uӞW@@@8;gNjMV3+?Y'ʚCWعk)A7n-鈤K)%ٳe7o Zun{%muW_xoؘVYz/KʕNj/ o@bfCt㍶-bZˢG#bg|Ciel?'>Ș`NH*KNg6VϞ=R bm۶͔i5TFҥ.v7Y|EC    p ĕ\ӓ%̙#PM&T˻7U;W6mڇeĵS^@ˍ=6;PjS9@@@@@@L+ݞk?PId`s O`)+w̠[l.rXz-t{χ1[ <(y2eJJ|dd!*㬧tٳgٲG{ȨQcN@@@@@3]j'J֙~9Txϟ_>`c[@@@@@>d ( i      W A)^@@@@@@@ 5=W &#L+G      H6P]#YA@@@@@H BZ_. `ř"F      Bd``J7n-G(! W@@@@@@P |e3*3f 83Vc`EhI3@@@@@@J6y`.@@@@@@"%7^.  ,`䚴D@@@@@Z;)!`2X6@@@@@@(*TY2jea (4i      7Z@@@@@@+_gW5%      BDK3D(! V@@@@@@T `ҞQ6H%i      z6[BIJNIt      @$ =&qkJF"H@@@@@@ J6X&e&/!     dPD ұ9      @f; +3@@@@@@ \X%Lp۲      15i#G`@@@@@@+Tg$OhmZřo͖      XǔlZL A0!     dHjdX"+C~l      c-!     D `gX       0Ҟc`QB06       `5J1LVXs@@@@@@ Z[Bp)!,3      x32)XIۢ鏶      D%p`4jvDVT4F@@@@@R '䁆M+JL#     D/Wֽ>@ofz0V       `يz3<")QtGS@@@@@@|X&D+:RZ#     D#wW<|ogXۢ鏶      D%j.G(!(@@@@@@0{=i M5y@@@@@@ J[BpŇɃ0]y`E Js@@@@@@l W,>+I`tF@@@@@J>]oL@i      -!GKM)1)%i      @6@<(i      #SB^l A)X1@@@@@@{<0c`7#"@@@@@@b&WpiҨIө       @$6mJ<H       1f`1X[SB0&t       deXfJL"+P"     D'p"EVtF@@@@@X#X1q@@@@@@c`3$&SB0BK!     @jԤx<I"V@@@@@@T P2$>#Δi_C@@@@@@ joSBqk35+jS:@@@@@@BjdX"+ M"     D-p"XQk      @v }f ,eKLNS:OAR`!ˤSkV duΜ9 .FtlܹY-[vYe֮@@@@@ Vi1(!Ovyؕ ./\\:p)UmX M2ؚ3"KA,i+{Օ %\RNY- .ҕo ճW~R> uvU֭56"     %2,вes]o/"mڴf&VAhli &9,?ٷog>[lRеryᅗb{#M~̙\}hi3-!M>YX     p x32)XINc=)5nɟx<SaѤ_^"A%Dk]uҷoˬ? 3gΔ7 *UR}ɓ.LQFɖ-[M8hA{@@@@@NIҨI{ d`7PNz)s$%%IblMrrĆid}N"ڠJ:ŋJ"Esϕ|^TK(.K5kS F*[:ߗ9sm*ϒ5I`[Yֽ{nS(StE.r}Z-VTuL6o";7XKa]vw|+vs Yk۷o}F)!    q6o|uWuRpW~F_">wzI\] 4 B͟?_֮[,{Ւwm۵K gǎ2d< Ebt<le+[oEwKmR:v(bZԪ][rSRd֬Y~ܡC{Zo; 飏>3GyÆ ۮZmU۴mm%/|󍼿ti!IRu}w”=|7}      #p"X2)WTD$ɂEPDi4}4ɑ# <mIs3PzK{O} -E40з__m^Ӛ w'  `Ι-#&-[y& c)6tݗӸCrN5lUZI:wb%%&tмdɁÆJE\ 5%fĈRpT˝{nݺ;o}X: 1AŦ͚-4׾!>jq/_>7ސW_{λ7lX/__fɝhybn& oAǎv 6-_L6î !CիWcR5K\̙3d/ں!kRb%_0]SwAŋ o@@@@@XWJLGbrI9s9W<)R(C߹s8yOڥq͚wI޲k׬F۠رclvbMClN)f˟OJ,%y5ug`^={ȍ7dϛ7<hSӧ[*I /BuΉ;GJr~u@߈ep$cD{63&bŊ`O 80U^q=nJ4Ӿ@Rh1+]L0.0?m7Aי ^ x pM2xPiGvJ5&ԍ%y~2q$G.ܵs駟GK/_|!O*X*THjժ[iw,{Kz}{vr!IJLǐ'oU\LwլwsRh4x ie+Ė,_Tt> Wc@@@@@eJ6Jf1.|iѴsuXN ٨_>RmgIg+mC؇2lPٴyKnq)]etU $\s]Vv>le;wzJn5%uzeᢷ|Kzf͞c]h[_*s< `i_{ׯs޽_}hhɼӦ/vƌ%ME +_ iӘ1cd 8b+M6÷>Zkg^v,kOdʔ6kݪԬU˾߸a 6λtmҷoQnmi[PDžzɧJV3VVʷS:iIRvtgӭ[VF~iѢԩS%+}׷tŖ)S1u @@@@@,+Tg߮$iDXqɸ9ӥ^,[4Wfy?:,{Y#v̗gj3`~i߾F  /V&93ڶf,.5}λpC9a1{nݔ)mLVeN)[ M6r||So3PxvG}7V˖ͥv:v]`p|nݺ.a_}ͮj´mko,p^[6`Ыf7,baI'E,XdK,>DӷmG={ N9tUn}m}]D;٭5p;6V`ơ"_M /3     2lTB})4xժr٥ju:2WDX18Rzeo7]ni{l_>3fL+2V5ee;w\֔69ݥ|jU%U9K/K."*Wbڲy)6ķ; Yի6`JmތԞSfL݁XXYOw*إMۇj$ԯolbyݻ{\6xPu+V|,SNmmoS>\9PIqog&N y1ҢEKj9DrT:2IfS;Vog     u'jl2XG'[_6o/^p{efz~k3k9r:۽yY^E 4s 3~GvHXک~꛱ۊ-&W\q-oiwf~ZV}+=     /K{ޓ, &c2NnreKJE -M[ݫc>cwiղf},}\qE6dJWʔtgvn^kˌʸ-@@@@@I `ߝ, ,sr*SL$Zhʷ%oKIs?zoɮ[p|vZFyuΌ>i5{n ,x[6mGa)}I lz};ʃ Xg`hLԭkp21s߾f;Ν;eᢷ6[jE',?bebv՘1`v~߾}ҵkMF &8p@|)ϑ#Gg;3ݺu+ڷq+!lM)پuΌ2d}zj=D–-Ku%Kܹ/8|6|-y>X|;63NPSۧ7O+=!#    e` W5eViuyVʷS:f4i[w~ 6Ė֮#G9}`4;e{UwwE䥗_@@@@@3_;nf:X=.+x$K'>5 ci#G43"ŊM>$[l)P,UJ6TfM k 9 ,xSy{0WT;^d횵cl2y4L`in? %njDkujQhQGHO-?ud͒?_~)^-+'ڶWrAT$V־KʕIŵzjf4TD |2MRR?GwY\' dRd [K/b?ԾQCCwlu;є6 N:t^m)L`5(^—A4'+ۻ!$WCM3=KW.UU_znMP_@@@@@8;t ,o AXgϒ+B;f1O|.ڵTt&pL*x۷\uU4}۷f `ic'PG5~]SwKB&w~tKfto`޽4 oo,[YiLVNBߨ:^x%bp_ov,e>`Mo5x5~//`QisٳwZD az,m3bp)\<ի}5s YKus$xgv&JO' jݴc9     p ҦK3s5NZҢeK{C ܷ>ԩ}7yz-/IN99xg1e˖0 f5i򈻩߼$5d07իUf9sk: lߞbJٽ&]LK/͛6#6zLA>S4ywg9sȣڙ,~4 \&pF` j2נ:Ŷ$7o!f5{ČMU/ͽZSroΜ܋},L֑t&-}v5sf@]1cm9^VיRꫯdqN׫MV^vmLr=ҥK;3 mnݦwqѠ3iD{w:yE@@@@8\XZs2k&>7bI));2swoJSo߾O4ULa Z8(]dϖ݌Yi&6H-?^J&*ɖ_5Sm7lLs-t_/ 6ܵ3KSʔ.eqM/@@@@@+_gڕJLr$%g˙!p  `!      pJč8ӳR?Y]BY!@문̜$     @qc&L߰l+ C"!Xbc@@@@@P n1 L+N, 8q> _^\0k!     p OeKC%b      p ,\s$`Ibr~ʜ      ,`Xj!G`׋cC@@@@@xBEjfN6Ό?iN@@@@@@/!hj2֩{82@@@@@@l1$ s      ,p}96ef4xfN kuGrf{Ը\S:p    7k ,--YzF4`1r+v9jt^݊7Wz}b|egy@mZkִ:iN{'{YO&9r䰛;vL8 ;wige9nOcH{/ۅ~'뺅q"   dIҨI?2gVFT=fZl.k_~6mg'4mj~2gs<4ڴ&Xr;pȋ/*MT\\2h`μŅu1wltO5 3:D*G;@@@ d`7f 3#jqH_|A~{qCj]f?h%N{'WܺujK-_~)ƍϰTn1dB u?ui@@@BD+A3<B=3J!v7PNz)s$%%IbΓdׯCj]$g&e˔W#GȎ;e׮QGy\#7W l߾]3ivM"EDׯ6fQm3gksUWχ~ĭI2zA4.|,QWz9"wN9^p2Gs~5sg>*Un55;dMibeŊ}n6޳7+m}>HuO\V9z|j݇t>Vt1\z:߷_VYJTwi @@@@u<}' ,HbriRu{SOJ*U,Ϝٳ宻B^+t5h~Ql-_{#wydu,:U2FG}$3ftW-զmk)^\tEu|7ҥ6`IR-{vsd)io:^y$#Ǡ u%%wRti>T&MbӻFQ7KÆ @>\RR%یoYUѶ>Ȉxqv{zURn]}T?9 );H{϶/d~Í?\&3gzjvDɒ~;5Kϟ/k׭3s2C,;͓oР7~- P\=]db;رnC&}@8m #F 7A~oa̙3d/݋# 0*jZT۸C!IkL6ְ#V=;g\f7iy:uʕKbMol+ݺumљHׇ}5Iᅮ `i6!`իe1̸wDsi{r^WZ%nj˃98H];3e3cJҹs{`BΩWw+k?_^ݧ̙ӯuEM9mm}DzdA֔wmӻwF.䒴]=\ٙYƏ?foӁ.AS:֭ ZW/\eG;#Ç _Ku-=!@@@8zƏ*8vƝdNt`լyne"E֮Y##GƎcy/]LVZS,}[d"2kժmֿZߴ[k)唘ӕ}6 v$ٶmɛdd~0~l5#Dנm?-Qlز_+Jn3Qn, ^x-7Y4Zrs)yYF_|}9bpYa{!7t}?o\:qqq"}=Ѷi{ׯ_'SKbEB xhM2x@8^Q?d )n&qV/>)Y->wg` d9YWZJ|&ƔLJJM98:rtH=πsLifeg>#~%Do^?d= oY@WD>@>)#޹u^X@42e˘[m IU=`"7EsNҘqƉ Mg͚)ٲe4CK.~d0߃mڴE;##n =n{ߝ E?(`%s:!=1~    +Tޞ㆛ 6{%^9կo)kW)'ɧ9tÆ5-?Q8cxT1<թm)>yҤTanZ1r hLԩki鰹s_[..2u/Rzu>p5{_.]v/9sVvio_lxe;wzJn5e uzeᢷ;ƌ7>bH4uz{vG4UNSQۻ>:eZƷ7eРAr5e shp!=!s`+s3T}9̇s d`i@❷ߖW;LkTv-iѲ#2~xR.Z'9}4tF&i͇{Z'}̔ۛuܞo׮ԨQCgeZG#Gy:Jc';5G!2=T';Tg=\Yp^c lAh~ghyݟ)ӧM]cFe.\?}}s@@@.Nx&&MV%C]5֞yf-?H|(<ؠ}ʕYjٲԮ]ǻKsy-5d5X2mͷkFj1tҌ!ac]^d6mh k:ed[j7SFU@A|`_ɷlalxm^{Uy~}֭[J͚rVxsT[MnǥKMgOn('=LPM>Ў{xۜ7s98X:X~#s|8~wJkǤw[᭷K/b?J(.)smh=ƌ8)pܵsOgc۵s˷:uƌA'kZ'LI\3CDB[nm﹧4mλ[ioQz3thQ714cx9Nu};!f@@@@ҽg!0V{tʕ˄ |gnOc93Yǔ,-8dm-3:qɓ'=zTZh,ժJOkf K۫&A!jrr f%ʊO>(#p7U9K/K.*Wbm25eGe:w 6ӧ+wݵxr[z;`T/~f쫲eڞW2ѽ3˕W^}Ons2_9<9X}:p9|Zu/ W};݅|k4h@[&P'R:oM{Ǝ}4[Fs u瞛c[/3ϔ[: 23{,+J;ڎۯmORqo*-MFg.éc[tOu}   $SB0>d`yL6 %^5[%99Ys,R B-lG~"W~q̀t2;~ȗV;]fj5joRQ垎wIUP*Rz?;{>0ؠ!m{NʕTjrwm|8ӎۥW>ljmzmv zp,9pO㴍g8琞9r?m# hPS8{z>9?L&NPɬ̜[)9ct+8tm8(= $=_>ҚwNvBbʣjy^}U<(|~_7Nj8cg}7ngnIL|o]Zo+C OkUˢ?܇PpL:ǟ&h@桂#>Ch])o?9g{> g>dm ^ .e $s%=6K/|ӦM{cT#L"EH|FܠEf͚)ٲe+>S /̳<'L/;'QZNu}X   )!הjX&LIi>:O36G:Q;-5K+ٿ. oWBzwEfRn]|2w m{ˉ 4_[F  `Xqt |p*(mo;'ӛ bz2Gsw@zt]̇s y5kԫwYv9*SJ mg qHub q&iww2=_>;٥Zv7W\v2<(ގ ֭['V,GtwFz?qrJzdΝp[':3    pF u3a02¸[KxcGt߾}ҵkgyW;mG]vI=}<-Ch(qWm;F,wL׷3}<թ}ULC`Æ t`{8pͲҠmפCRZ5E䥗_N(}=dvy\#ݻw|mt_mܦ|`Vv[Jk qcǦ Do/_6d 80ďhy]Һ)?j&n'MH`CMbPA3{dcztXќ{Wׇs231dz{|jLɷ~/Ь%N۴mo/smh^qCMkh'ўowuY*UE=ݦ_Km;|ߝ6pueIʶmҧo? <+\Avfk)ӧgg:c-^̛Ou};!f@@@@lX5Xqd`[ݥ|oPcguz2}4ɑ#G@Z)gΜ͠_Ŋ̭MY,g `;CtIDپc_~:d*RM%fekb} T~m3>ܹsIjե ^ioG1Ot0U;d횵ÜGҥMv;*U ~ ){uUQ֛߼eϗ_(n]G1e{ˁڧ&"Ŋ8|lݲURRR@RT)ɟ?oCQ:~߉6s3T}½n2={eRvm=gf2aYq2(Ǐ " "IIL =)ޱ,xQOP̂zzPP@7( TT_o-=N虞eo?LOwuukf}*!r_ڿyo\ _k!PI4 ܲqq `iyͨrr=n?~ԭ[)U|fΜ!}o_<j׮eҀh?]tEf6МTP3K@&sw߉lٲQXZ ʿOAuh&H:uM^~|bmSf 7t|հy&=>VP@+wa/8dcAD?!g>Vyq i&aҨQ,==o<Z>]8sw_~;e_Iڴmkp3욵$oO pC3_|Ey.7    P"l9~de.VQX]T~ݺ2xИ$0֭[]\h3 4@oŗ_d+n{QtvK=E ``2zΐ&ꨣ}87[e; zf4jhNN]d[o[;m۴N6#MuE۷fjy\\r 5zLЕn}u.ѣdž zz0Yo Z@۫C;ҋ{~aRIڵmKPmV晠Zfp\:muO1C }n2၉ή׾S42ʕWnݺ%?dұf~O%z~]@ ^[A~cA 3.}Ly0W)=rH SV41j*; d%rcxm]է3M0yyI;Ku19gǶjy 8~ؾ /l~fx! o4CڨQ,4psw&pCQއdlD@@@b-d`ufXwJK;e;b_ϥs9E4jܤٳ E`x=/Ӈ'PM4/#5mj4p֯3ҭXz4gX[:<:3mUfȦ͛x:o!5lҴTXI+h-~, dNYmzɼg^p xiVymr=~LRSrz~wPXF߇hX\Ck`   ^3MV\Yd`EEG-'ժבfh{v HenaW8NJ@ώo7/?[%#@g޶fFtذrQ6ѫ͞y(@E     Pt[T!WT6Q߱5UQ @ LT7Yj,]T*[AZle:-?<A-{|PeA@@@@́CV컪Xe*k罈]::_&*1>B U 5\}ߤL2ϫ˗-ޗU@@@@@XL1|@"8VwbP ~3s6Y˼r7XG@@@@ Xi]2NY= E,ЪRfMٵ{¸[O    f)u5ݲ!KR{!s&@@@@@@?@^F3`fN'3|@@@@@84XmM      GP+9<+E@@@@@_;f32^}!     xjeCX !X7     yCVZ΁e2: D@@@@@JBpB0UXf!U6      3L VfVN1h6MD@@@@@(̒.*B@@@@@ `}m2R;3Ve@@@@@(X隁X%~su      a/ ` \Շ}i       @ț !nXfa{2@@@@@@8ԩ4w,I)      %T!%VsY      @84VgJ@@@@@@ 0WX%^se      @8n2rMCG#@@@@@@*7ֆlIb2XJ@@@@@@!Bd`%3kuh9D@@@@@(y YCV\      Pl\C*6w"     %Xd`5ݼ́X)d`{ͥ!     BaH@@@@@@ s$5^]r+C@@@@@826!;3ah       '8m2L,Yd`źG-'e*+))fҰZrM:__~W@gZ@@@@@@8"R+_SdڷG~dTE      @K[LVWժב!{2sI hRKˎo7U@T     @X2B0m:Rr5iNÎHŒ]ϥs9Y@@@@@@?@ %fex+WUw}gX0I; |Ij6      RkeC4!s|UX.Ra/{wBjr}Rb%Y;]ow rK?)mܸa\V [{Q✯09 Ӕs!   $_ /kBsO1.1V(=ߘ1#JGVX!>;'N^zʥ^fl߾]nk/ ̙3V\)#Gp'Q{DL:9;x    @ fX"+߀R{>a#o]dL01r{2z ]~}G+2W˳[HlE,w /X-=^Mr/+ Eqr?h'    @xCt Oth߀ҡ=ɓLA'+Uv?,{ گo/_&f=o{B:`E`B :Wuv~{~D[F ٫iC=m^xb*v Eqr?h'    @x;f32e2W/VP ׸q#1bmJW_=#F,)9xz5^cpIEGIyW8E.sA;@@@@ V-bHւ(9u{- `rI'ɚ5k/Wkrm ד ،f 7E,i:T,͚6h?A2ʒ;vD:]x}hΕ}şmWd\khaR^};D=WSJ*{_jeή۽H߹/}{kE<7:4iDlmƒ o./Qx??wI?uo}w̜˾^w] Нgq駟L :uپk{S5>:P3{N.[սHFsA_?d) Mw_oݺUޞ??Пc7oac3[hp?zU 'J_|L4N_᪫M7!C%~oE -o=rhgעL%rɲ.C ҥTt}tߎe?/@@@(Qv-fT `V[g+ֹ綒ئm޴IjשL'=zgO*WI2t0RJvz닮wqv^QLnЕM ٫AA!o+ M6#yaz9c s;XOZY'M#<aAFs noL@4hl-'z>tI`/'Zdc7>%^qc dEY/?ƛo){~羐XAhѧݎLt>#ypCA{}}>Ns]F 9'O9 .)_7A~xM"ݢ}w%GۨӿWlxXEJza]T^ | B?q F3M"   %F /k ,Sb..g `Wzjټyh e'{/r ~0CZ5jnQ: S<,{&fefߠ0%UMF+VȨ8}~#H6gu^qE;[LV~qF=PY CImfܹS6k*s $M˯ :uTI7'N5k٬=3J㏒+sZrɈS[!J&fƍKٲemkq/=4 XKo jղu*{~ >d8n޽K2$\nC 꽙p[4Vαpan@@@@ B0KҺ=&+-34(kɜ-վҽG{̾}{eI&p*P>3ft .}py_pq&L{K?dW  ~Ch u硲>wnz%\r=ҥZDX~rؽw_`([oA& 6ú}=9[Vf)S73bd FڴmkkGgʀـ|5q] ,L_~Ӧ٠k2مqvwyn6{eД)Jtg,[T{_'yye_N9gowDomۦ ٳК o׮!eniSD3PHߝqw<,w 3zTEw{}+kOuA'l>;E~IvR}s[sBM6+XrL­G^=Iĺ(!\zs`    P5+9?ck2၉aUFa l͜491m1l93y!PBCO-Ie=0̆X͝Cʡ)g>,)W 8l6A]~5PIG}D=X;am W[4=rݻWvEYC J3j_\[9916r`]L|F˖}zʨ^$ f<=o1cȗ_ [z>8YW.{v:S袶m>}xJVutڤ7:O?l zo{].~~zS)W3j<׺(|k[ Y&5S~@ @@@@$ ` \tщ^˟1t?$Rӭ[0fX~pGKҺh+rJ%B`F ` |yY<(~(7 `äiɁLfY~C6gm9CH,??)'ױ%.]ZtvNgt {4mL4JyC0￿0/'|ݖmȚEPJeC3>r˒>:7Eu/C5*|PjlB3%#Z%o>;o-z̽)]^ܴ1^EҿhW3zZߡ{O?l|iLOEoi ?w@@@@b+ʰY !f C˖.|yשұS'_ ` =+;=|mCHdžnw?4_2s棡E{/#\kƄ !-UVs{2m [i&KZl29d޽ң j{m֌fp :T6oT&-\۱++inK+ǿ`̘HPqF`u+s_ ){~ףG7i׮7|C|9+5kd:4ٖ-[p],"$v\١= iY|'M :͈Y.Kh,/1s^3zZ!>ҫp"v@@@@820`F+&Zy3eArG ^{5(g#\omZ_hVx@sxҼy {a,\(O>C`ڵ<]wʑX~5lcl8ju۷/0d۽e˾5kjʔ)rGUeuN ~Wڜ=ZeW?QnvQ.˴-dA68ӧGg{89唺o<36սHV;sͼc)& ҹOE;IC^-#ҫz֙gmݻwC.{Ͽz( #V./n/xߎZ)?׺(qͲauAޗ}H6m-zu\yvv   r `Eqdve;ͮ]sN;O&TR6JXɨNs/7+W5jJC3ä`5 oݦE==`=3ƞG$Z|d~)k֮ CH,mFl+.Ln7Wn)pgN3g㼷ޒǟxyx3Rl,K?_*kG&M-ZjX^Lvi"/8O?[T$S~}{},q^;me>{z9s6`P9Z+kuK~2f츠Iڴmk,Ç 3uAeһt dD3ʖ-+XZދ1m*~>_|d_^$߹:AxfΜ!}K;X*&z㍽/ѨY :~E_[~hA^ܼ[0gz'Ѭ?Skײ7V\ih]Ӧ (     Pڐ-i]LV"񶖄V#u4bHkժU2r|׫CE 2CчʔWF7ԭ{7ֵdbİ宽yMUXrHHj8zK+S>Gol/gնm䥗^߫SWWҭ]Rd8m `͞rq2jU:Cim޴ wVǍ#NҜٳ/qZ-}bf~ή|m۴Nv.ҥKZf< fNR\yT5wo9Q`v{mHQ݋dL&[d\iPYow8^L+צM!CL26cn (]|=cx\W/bҿk }-TQK-W|ڀnE~yd5o~FE@@@Jk+>Rr5iNÎGϥs9K2qRfM;_7$t^M6J+:yiN8hf_yõzmyu֙g0w7oz-T7kT4p/ׯukr.i\90ghۜ2:wPӮ6{DS*T4?سwXMb?    PX$M4 Z:r ٳw.;J@+'LcǷ䗟n,۞U6lx\Զde3֬Y#C *:*  qw(,"   Ip !Hˋmj5\^&̾?;&®dԇjժsrD,\tT8ls"N ̟?O{ [O2p: w(-"   0XMr77s`i ظU樲Ic\3_.vW+MM(W_7;_{{]se2ܛɨ#P+ )ߡ8(   $Ia%$X8VtbP K'߽{KҺh+rR1       ++Crss%!q@@@@@@$ Ja$R       @!3Ĺ $!     $GPV5X !YjA@@@@@HH o ْd`9`%A      Ip !h2̒:IUS        6dI!hXOnPUNYQJQ*vKlGٸk0@@@@@@x $>Oj׫hRd.*oY@@@@@(&If,J!+M̫ND+}fN#J,!     ,0e(w܋K(]˖O_[0S+      p s$51 ?0kӜ/k( {e@@@@@3e`m4CvfX7 Xv/09pҮRQ@s ժݺJŊd'wAͬ/p-tҲqyWI^$XTԱ5RvdCk3!I 7/H*W,G>B֬]#>;'1D@@@@ T`V2,df/֮SUTVnrsZI45fh9ҁzX^zʥ^fݾ}|s?\9sf֪+eaJ%}xgĈ{q `w(C>v_ڎ裏Y#Æx     ^UʳcٶewǚV$=,YX&LtF/`Ejyv С-E;3՜E+ǫS>WEbŻ<ݿӧIʕmm~+֭ig$$YM}t].A@@@ M h,=+X "lYڠtֲV`[X'O2XGѕRJoϲgϞfe2kn!^CV.dPzUkWG%h0fa{ቩ"[ٰa}eGSRR;dQݼJׇ   DțkC)Tf6CFJvk˺JnnnV]E཮$ dĈ]W_=#F+ƶ$ x}I;WS5^{wslNun+OeIɩ0˧y    @ Xi]2l %VDd~7ybB/UxtV.{d`.?WN:$Yf|加6zӠ~=iР[ixڰqSC[G*YӦ6]?HWYqg$bfΕ}şs !ނڦշs-_\MSy5u*TN&[6o>_ z݋D] U~3o?=Ezʒ5k*Wrv5FDsnٻw5VoW4-e(+u}QNiw5:4iDl}u'Ǻxq*Hsv:OQ/wYG@@@)u5ݲ!KR{R)fȥ{z饒ۀ\UJ`R,cAoTZMy*T>Câǎkyx [<[:u$j׶jqe[oi&-[\)]fϞ:t^3ԆJ ok呙3L* ҷoiݺqv2{=1cf`;h%rW`[0ZRf=X["h@]W霞n{2}WOd~_w] 3g>첯]nttv8p@~'$BNlC{S5>:Pu{N.[սHFkg4~5u'{護ޔ .P7n,Wwi ]uYZ_Z*VhֿcvYhfϋ/ξO6d) 3˭[.vҲeu>No5뻪ÕaZ23#f\p\͓ǟx2gEyRu?tO~ׯ*]Z~7vUUzI6m( M}wrN;zl牧9m@@@ _ /kBsO9݂dV~EˠSsq}iӾQ6Ma=?~MRN) `/Gc}grլq 6LTӂXW~ɨcq&V7m7L ݽI_ry0iрдi36N8Q9>MD5pp}8qD9#mVСÂJnԍߘ۷?hA̓6h9K;w ~l/L6q57v_ BV4#oVОwz!&F.kevUv!?2٣X~4?fp 3M5*&ֵgfhѧn}}vЕϖ,'<9DYJy# m_n=eʃr'#ypC>J;ߡzҵkȐ!O9wIe?hhX;Gs=_<Ѳ/}"   @ Xi&,2"߀p;VH\iۡ4m^3rfϮMʝXtf(Xjղy&_oܰQ}~ֽSN9^ʮ̐}֣F!a5>'R: S<,{&fefߠ0z+d`v?t t_E}j̳:z-YJ+V a;亮M!sNiڬsι67C/2WD5PA'H͚lVTϞAydTXn m2M$sG~9o߾rɈS[aJ&fG͆)[ |7N]lZR~eۣBE2^T<87g4~55Rau~sF{4DrCV~:Q찙:D߂~`Zmޢju fDs1CMVݻ$+3KrgI}C}CuF{5>egγ3rH}v?u~oV/:TO>mם  '|}ԩP9Fz=̵wmݚД)tg4.[T{_Pu>A#Iڶi-7c5{FA K/(Ͽڏ W>[s82g|\ {AfG ]^or{wPH൜7[^t-sg:ZS]wmx o=wv۳/kCϞ=˯5饗_a Ufz<4[ɩRB=6>\n9xK\k+mZ_(}o]_+5]]QWQUW]iw`>>|}bŗ :"i޷˻`ZYgyRLf9Y EzQD^w K)ϟg>?O;fi =8!46 sx5ҵ!\U]ufzMyɧ"ǚH'*be ls}:n)S&@euTt~C~7ؠ|`5^k߽޻JuGz>}Զ6h?~Ivx^ >`~-?}Wv 齖OrW@@@(<jeCXi1CFZC^{v#,Sdb&Z )eOmSӪ׵pF|-֒ŋe8>-5r;Lfm 6هpcذ!rgئ]F3gPEdmðh* M!yhp]ϒrʛǛM`qP`k TeGc=9lp[iiO{ /ݻwK^~'*Ђ *W1VN){}@~`]h@eJTYSF}_T"Yk=~hMxƻW͚57vl`(Θ1];X~Z_xkg \ggglLY6N}qro> xzcƌ/]F|pT}&#hwePo\_>72; Q{N/jfwyt["O_s NOiO?󫟾'qE鵜<~g\ "   @4td`e4$Ÿp,מY&;kC nwҺu4}4yS{G:3.xDϒ[n5_BS˫s2y-'<ڦD˂   +`bLY`EXzo! ^Y%>ߒ>B~v)׹R^h+1R<az,p<,0 l޴Iƌg0,V^nFzH?V+kuik.rUǛ!ի'iiuxݻG)+V 'SN3YOܙmNb]ʸWN2CdHkIz'Ez|FgO?r^Wnݸ3atH l}p^\dL<xM@!+εs `5 `fy 9uط^}O>%hAv)7'|^Q2X~` 9xplڸQ~;W$ѿ^ܴA^9Iy=hr@@@ G /k ,S8g.gr.gүM ke!zMFMQ׋ck;̳βДÏf=*0~X~k0ivirY5D֯ 60,\6uM\h\}R:~zåifw}W^Ltgr˒>Sy^^$yo\'Ƈf ;CCsAO?l3|M'5}4B'z\mQTr ]>x/=_Vssd횵2dP].{K?JFO_we罾ܛ<^ܴ!^^'x!   @ B0KҺdسKK6*IMH҇~ey{c믓lR}.SkcNf굔,u\~s"g? 6yZ3 -b ְ fΖUڹ=6m-ϴTIrZl{yw^|=6@3iڎq2™vvJ{ ȌkCƍ2֭W(ST"Yk=~Ϩ~uK.W' cgE^6n Ά kHӆpNAEtk8u-[aqi{֬ǃ'M,eˊ#v?1W*^Q<pGzB?}]v}WC]5s- Y'M -}׋6k9ybHx#   @rlkڙ9bz `i=4qarb*hq `5o~ 8H8kt"^sy{p췎6/)!8$;'x>}-͛,\(O>6Yôv.q4s]w!\!\hVΑ}a0͚2ey6pU|yY#ÆߕZgAG@Oo]jԨa `2myv 8h ӧGg:Ixuygm({~gO?r_vqf =deYb7n(2qͲau>9g@ٷoiӦʫsʳDG5sfK9|Ǝm:~V2﮻Kf0:of\~+H姯ur[ݻd!r5;믷HD"Zx^ۓɔ    |Ct}ȗXQ `9L3t ^I:]cKGnrEvgc.9HtMORlƃ<"=8[{1=rVJ5OD }aʷn0/R=f=|2*S֬]4asD `iD]+.Ln7W)gN3g㼷ޒǟxyx3NytϗZѤI9eKVZl;] NO+)'`f/ԫ_ߞC}>7ۇI^$S~3x1cFVe2pV^-jՒFK͚5{(ZHOm^dϽc?o ~l~Uiۦ6+t~ypǝ}|_g&gu TkEBUfVP`%B2X,/G~ Kڵ͙unsg{/K}WO9-ymO2@@@ G2sE23T ,qe5~E2_^U5j:4bt]N6F 㧟y6Pð1V4)1CrhDթ˫3 /dqAsinӹ{fu}ޥK /w}'À `iy/E94;_]T"Y.zo^Q3?j=v߿ afE Gծ]Wre)6[U.:fN7LnK8=+Y3gΐE>ͷ;d]ım&f/(VK+}h_^ Y :j2}zxl$'Yi/    Pys`!2++2xI`52YG#FHWZ%#Gw: 3.}Ly|etCݺٺ,^,ܵCZ:=ɺJ_rKil "=8v*S>Gon4Y۶m^z1XN]^uxvK=!45{vŕ+VȨcV Cy&3D^=rLZZ7vL siفLV-ϖ>cv3c?gW׶mZKT;?֬Y-?_+W^"yx1a o[Nm((Xw{mGQ݋dxIFI3h?lkLf~Y۶}+LZ15եuvX!QLaόҰaCuΩM~A2o͛l&z\(+ZwMST?|Uϟ/Sz[vۼy&ٗADxh?Zs=Cvvgҏ}o_oڤL26Ct{: MQ.b{m %hbZι9xΓ崗W@@@(X:n^%i]4b)3ϽnߨЯyk q‰Cx0nݻߐ)֡4m,*V; Z%t86pB5р/hkZ͐93mUfȲ͛x=ԖbfMڌ¾^T[|F/a6XƫMt~`c[rlDugF֯KW6Doذ!rgS}wԎr#Ң,_L{_"ɨ#llDM #$A੧ nÆrP3U     ted%:Sjj'3ӉA~Mvm2>yIFXG M'%&裏!i+6mXE    pX́![Һ , C밸/%Z-5k֔]w/LQGB' $g@@@@@ kAe!n@@@@@@ `mȒ4B, ԛ@@@@@@b$Ê       rY.x 2 S      DH{S_Һdb !]      /2r ]Ro\V΀      @=3$!#0@@@@@@RjmleXf"9      @~C,2@@@@@@rXzv2 pB@@@@@@ `mِ7K2B*      @! c7_.+Y p:@@@@@@@ʈ1rG !lO1C      @ dD#$sO,$tN      Y ̳/r',EVd.      @JzMslȒT `VAS?      @4un٘eR2rg      *2Xi]2*Po*G@@@@@!`X_ ́Ê       p(X́U@@@@@@ +e2rE2WG.@@@@@@ X o,3`Z `9 X@@@@@@ ԩ4w,Iʉz;@@@@@@(HWVJC>l9)sTYIIIY6z/D8@@@@@@X 2~T!W^({Q߱55R    ٮ@IDAT  px 0-%~iUu=lZvivW IҥeǷ      Pe` \߱JOwvD,z.=Kɂ     ڐ-i]LwV^񸪢;_SKN@@@@@'Bd`9"v@wudM/=ƍ]$T#J!k֮gc[~Bjr}Rb%Y;][n'7l_^+^3ckv:oɔ3Sp    Vy YCVS3'|~R\9)sQ믿/;vl+Vo #p ʵ;Gh6.Wr饗۷7 3g `ZRF^X+^XgĈ{q `wsp    6! `ź+(:r ^)S&i8 >f!֭rΝu%%J;v[z":znK%;tJ }˗OɌ/J&oQ3]ք zZS^ңgOhBrZ?~oUsg۞AT"W]}}sΩ@N:eπT˖1rwWQ<=?m޼IeMΝ;IPrzow؇Bi]I:+϶ʪyK>߿_Xc(;]i$:)SˬgY+>41 |nge㱣zI=^:uj{tiGnRvV1~_NFvgcMY~}VV<'=}$>s u ((vPCE?vL;m?eܹ~ |h\׫/ 9:$+԰73ڍe@@@@082TK-qd`-I-Mw< ]k~Btf[g=%kuF!,S}^I9r/_ޣscg^]fyG_4)vlR>b}KRO,_L}8ضYW3gdʔwe횚5k*QQfSO=m,ZPfgiF[Lʮ];O~=2.N;=.jW&jf[g :̬曣rXgؾ@O{9/UTt6__k:wz^i/˿]a d%u@O ڌ<:4RL}{[= >z@@@@ ,2z+6v 8o}pe0_*K'9y'i۷TuaٸqaU=2y-2|pA:*\2t:cF%+"QgI͛d1B6nleʔ*̾NluiWam4.z1)^Ȫ,78qBƍ+q[ݻv6RZU^=dcqqz,}WSm,Y_ 5oN 6UImO}iu뾗})$F szsg֙gzu : :|ٙGd!&\+YbTTIj֪)uiY:}쉦ҠaCSnM{wU2b4 mQ]QC0r.# +]~IFYeneQo'$ˀU{iҤh'/=ojݘtVo0~Ntyv8sV˖}Sa{ͺ~ᇲ`"Ay?!{l|Y]ڴyF7nbaIgpgZ 'Nd%??]1uM'M(Q2jfT#u+TP+>@@@@OXI! 8 !Vg֫Ko6ݙ2w|_];%%EwmZKƍͱO?D̙G4?=[>;/Mo;jԈT&8ef54pb^fetCڶU}oEdTp=r:[>꬗>)}ZC͜9C Xڑw &ω;9I3r^g=ҳ+\Y˝;B<ԩS`rIi=mB^vt|wΌ8( Q~RgK.ߟn[ _qiXچ[ ξ    @ Qq|J/5W-ӟ%˴3|jQx=r-c:4ogu|v7>MaR"r5HLuX۶nUÏ 8 lDžo 识5l0ټ=aǏŋӧMp/u%8cRRJo;d'ɨ( 7ɝwڙA^v`XYk>{ p^G8zXcYYݻw`Ocz709R`կWW^ٞM+Wb罪澪A5;\wuiXچ[ n    @ DJI/# aPv `b'_s`5Y6pٯ ^XL /;o0,>z]aRv} 3u֮];O~z|KWԱr=ളiZ`Aa8u9~uټ37]!=Y_?J }aA@@@@ \X{۫٘+X-[?bw^ٳWzKX /uK_%ǏON}0YkOM6rW^d<%JciݠtV{a>545Y!֕y `UVU dXy.V;ͅ|PXYnk>eJ3u2Pbi濫X(P E~:7n{O~؄ VqR|4XCG}@@@@U`E`VgV7 =n@oϾ;X˖}%SPQm[KFETigI(U2aLKII{= m۶3k֬Ql'sn&,ou8Sҳg]7s YpYXh@^Dw_۝)V(ϜnG ױ~Y1n $_/[&Njuzw9G7}L|kE@B0TP+    +Ýe2TcñҧjcS\|Δ4:xBZjX\l=ȋn]AzBpLuԬY˔YbI4u)X`.XҫW/;-|a\H}6}ܵ)&L/6l8ef49/YBߖܹsKmoNM7-[_K/AN6_,ޛ`XYFk> :Y4hɲN8vsNҠ{Ο/}@}?}{|kN[1b4u+TP+a@@@@0Pꮔ$X !Ύ,ݣ!CI*UMt@bǎ~:IHHJK;뮻.3IP^f ;:rlܰAn۪JI*`jXSj~rAsjnRR'5|Ν;վr*l-:l yo6n ce0ضY9v옺vIHLP/ɋKU| 'Nȸce{l]oK"E̶G}/Ξ*mn2Xyv):zTZ v5ט}Ve񍷊N ҧes^K5g.dUK6WwA,K9kH4lPUe;GRbE]͛;W,\Q6 :aʮ(S+ͷ~Y_XGĴha^Z;+\{7o^Eu֓5wuߨ`fȑ#yļ S+k~qMO'7Ik0o,]_Q K*mg祾!.F;S۶ >'VV?'OX>s\'Xeʔ6.\Xw,z+Euwܿ_g]ә/<^?{tɊ[ :,    B0E !Xj!6gգNnY gSӦO}G-驆Ud[ͺr]۶*{\yv1Xgspt ˓QQ&IKR{ǎDO՜Qk[ȐqQRAe:Yut`Nzz8g;vP*rWEu?~y,]Tr߹ҪU zʕCmټY֬Y-/Zp{:?u豣jτ:ez&~vz\F&u6%eڴiJ/&n Qef }=KYk{Fo>s\`L>*c2#M`~;>nwo L[߉c*#pŲxRkǷg*.SFߔRDQԩs`V-`m BGZ|#    N =3T Vl\B8/SRRp19~p2}-}M}-}Z7z iwmk0~%Q %j3:zd5)TGנ̉oڦKNNV>}n(&:qfmʼԙo5"#Eg! wޥ/d]zjիɓ'PuB~=?S`     d bĺf^UDgww2$_|ɰ@@@@@rhƮ~!Q1*Kx9 @& yZlejܸq|6w^&NU     d@3^p͜DZ+s`@@@@@@ 8nƾ9L[V `%Wg!      /?f zޥ8V@@@@@@V kKTsBr      @Dtk̛CT.WjA@@@@@R !8A !2\*!4@@@@@@7Ź3X2:@@@@@@PCPCS,ظ`<@@@@@@BpFBP-B6@@@@@@"7j8X! r*      @ DtS:3"63C     oXIBe`9`qoCW\ru%O}A@@@@ 44G͟_蒵k oy빰^xy)_?o(Cpƒ /<#^Pv)sUXa!@@@@PX]ɻX:K% [+ 0< ^a˗Il<~-`8FBFd:uJv寏*.ng{SS sCk*z꒒/~Q_c١Yig۵s￞豣=N@@nmk3̺fݺwI%NټyW v"    X+G?sϙgb׮ҧO?MԐ:1m rK&MTΝ۔'۶m` ŊIgKU$OױTZգgΜ_Teӆ^wԒ={:-Zh[f-)Tʕ֦09׫/C+VY^ݺ32xPNkElRE-ΝSA̝2uٷg{sEǣV;wEXG5WuƌҴ'VFl\K2dXݺu;[sYv^:1ߨbEI5\ϯ|z*DxnٲRU)/~ka'!ٺeɚ9rDֈT.dAOTfy=j0|-f$.6Nx*#t;u 7vlWǭe̘QRTiߤ}{Uwa^&=oݻwxI;NJ,)p}{mnJƪ`Rjoi5_XŊtm Η:ӏ?+,`\& _J4cXJ++˖}%+WJ*IZ5N;͹,Cinx `9=쵍7ȎRRE2̊@^s5dg6Ɍ%o޼ݻwoRSXX:uVRt `G7j*(HK$&&Jduソ>7(ON+:;tOY݅)ۻwoVo=ȑ#MsP@@@@ { WXҷ66mz vqqz NP|S=Y̹?v=u2Ծ:r2o=;lP;?5x6HMLyoC]ʕ+gO4ɾ硶[Wh=#XzQFIҞ r]e]W Q/ (ժU3y2}L\VXzaÆL'ݖjΉ޶^+FIe֭*jn]_۴w"kRZ?9xE;PemA >~@@@@SB ,~ۭOK2eL>p~EK̑6>l'|e2`gڽk׮mҨQcx"R%K#L-%%EeR]b/j;9W_./vz˱cL0ʴھ}Jb>Pi l|8KհҔY63XJڱcSSyn0}uM.şO}ZڴiQ?*  k7P~ed˖rrOzrqyَݻ -Qy Pzznsڤ*j_/7mܨz#=6l /td~Xg;H_ǩ XYr PݺN_-j.z6ΏKÆ]߭]+cǍwfF[K7 K{5;s鹇L5֯['F1S@tK-o^      @pρB0B;6>1>=}G5+#y>pf~':5x Z=KOxOP]^z 37fYnXsg°u&o,ntJ* ΗéSi=ח^9!S'Y;w 1vzFu@;w)J.nTPe`or1\׮/w55֮0'U o?a_{U{?DCAjH[̂    Vt^,Ony"TN@pVrKi1|PZzϑj *HTt蠌^Pg:X/WV&M,|0SjhFϸqcDY@^ `iJ+aJŊL暞'ZtFdܸ .^j 2h3bխOr{eqK"`_vt.z|ʛ7mGIUzM&33~>Rn&a']l[( [Tqޯ۪ù>    e+DR$*lB0!;UT6 U4hTVͬߧ.+8aP+X"[nsf[/sϜ9#[땛ʕ5sd]qC a#={bʾ!:_Ka'O~[/_Q_zuy77e?WX} p ,gnԮS[6Yv9;'n]g7}UYI汎@H/;ҲE<âz)=mn:G3O>M~=|cG ou-/9[e|"   8w5`%bΞkRJ|| F2*̜) . NԊV۶QƦE eƌR%K.%%E #Mkhx)Z)RD.}*:Je=a6l w}:uJڶmvi4nz2yw4\b>_fzA=oMP^^}ξn]g}"2<5kY(/d yϝ;'W^y^m˗O~{o=SuEg?M8k7tzl/\c?    9K`E` f͚JTTy<慦>x?a2R:`ع~r|'jWw9:jFkÆMSef;Ӛ;ƣѣԩs>}ڼYV]EVuO0/ukĄ?U]ZңgOAwޑkz#԰]; ̙PX:NgYzxvwI4h`w|hǙn]gAzfG]F!8fhO𜗮SfZ\Bf̼!>بf9!s p `~*xT@f`۰*:KZL WݻU/     x\`0yL0Gκᅲ]wI nܢlZER=GGbbtq{ _,]z׏;&۷KBb J*R^i'Nȸce{lv~<]{}J̓GP%r85lxÇe%*F.* 4*xK.],PڭHw9"Um[TR*CP='E⼧W j..@ ,ݶG~HH\즦`6x~i߾iLfc5e]o@@@@0s`%9L:Y<5z@5{2M6ڹS&M$q8U0.3r)_ޝd֙5S+k~xCeeM2l; 64RrE3TҥXc. ˧B'e5#u2t1kzwA klRYe\]}~`tRe~o+#yg?}̻gn]ϛ'sק pILyG Ntc/ۆB ʰa̜}.]]} ŻJsa :y8z'ͅ؁    {As` e˜_ǽ 5Gԁ?˖[Iց&[@p;3ZvQ繗ի{}IY|&zݗAդPBS5shA g'X Fݽ ͜    @v0C!tK-뭤      @x3R)Xq 3z      -!'-:-#F@@@@@F ́6w      XB+Fg`+L     #2\"/      Ms`!cTK-́M$F@@@Do@IDAT@@D lHW8nX*&]      QA\.1`v@@@@@ s`5WX !6w      T1`s`eI@@@@@@p2\*!@@@@@@ [ J*ḰE+[G     cAظİA^L̙d3ݱ٬4@@@ow+)Nj!7!  Tr @+"}@@@@pp !H+n,}AB  g'XnV@@@ *+yKg`En:}D eEVI   "bp@@@@@Ȧ&oODŴK$.>1vf#     ;kB9C       @v82TK-qd`eJ@@@@@@,p!EVv@@@@@9t {KG@@@@@@l)+=`BlX.d@@@@@@ XvQ+n,]A@@@@@ `ߛ O6oRC&f׾n@@@@@@00}{%*F\"@@@@@@ V `EZʶ#     a!QBkDitաJ @@@@@@)nMǣZ8̞wV#     a"1z;^ǟlB0Ln,@@@@@]/EK-o \T~CbLVR`!YW<.K]+=II2w|cl    LA,X9 p V/I*2`wxꑒ@ɓ'9ȩS$>>N.X(I{<,O4kf{WM>^_fСC]̺dzLk֭2xPk7    9\ p}"Mխ `=cx5R47fO>)K:5<;7k2}jQK6n|gQ33Y .lZY9xڵK&ס};2K!   d@\RR+F+ ;@K*2uE"e݉f*qɝ;Oѣf`Rl9/Yx|GVCZb޴L;3+ʬvV\Qaߓkf?֭p: K.r MϗY϶Y+>41 |.~8K:w$7CY2|uѳY_h<ب-Z>_#1QN*lZQvZݣG7]^l,%7|pSֹ:[+>۱T\Ejj9wܹSԎCŤZvsU2ᭉv]zER^})Xʕ>v!YG9+=\ڹu@@@@@z+"e\&3bکH\|ѲKЊDͭ􌹲Sjرcdɒgs'}^fM%**lMp>x~EzihB1,T`ײ|72y]wՑnݺM67"W\qSw)}8lu%iESmCv6X^5d :̬曣.TkXmȑ#|:7+={r}HV˔-qz7 PGe`5oYp[nq;f?~}K  M2e <`#S}'[ڻg=̜3pb]x] TUuUi&u=cƎJmd9J,᳿UX\\93VFٷoo͵@ƍT3c̱[Ȑ4#}&HړG~9R 2xh>%$9RNe6&MZu`^r @ک['u]kˮ]5j$`kwWn.,:COq}C6.sx'֩m<#713f|qgR\9oIvN4hTVd`-YXϘi     @\`%/5nٳ?stx~;K&Qe=}^!}!gDgQ9T%ojxŦHmԕH=Yjۦ4j4O?9sif*`;`y>H=Ҝklf2p`P@z'˪k*O!@@@@@X99+m[tt駢MMm۶CSB qNWzw< y2*Z[4iY@i.#z2rE4-Ο?4lxڵ2vx׭QǤ9Q^]ySg{'NH||n+Wə3g|#    +W(XP;@i \9CF!;w$ 40lӦ2|ȀsԁPCtAxcvGV& h`ɫCm0v  `?N/ܹCov8Úիe.\{$O<GܹS,X ?mq @@@@ \_z9rUreGz-N@#`[t0sVL裏o8-9r/_/?o58H@8I/UZU5 ӶxrϓjuFIg%z.FrN޽gHK3TXA>T*V$ 0soYm6sޥX˖}%Sǣ~ :uJ#NSgRvmpbju%vpK9#Xپ][y5ϟ'fNs}_;!^+}l;e0    @X:uȯd^ǁ#@X*#X99K&MIVH\OoH/$w׭k!>d̝;l[ ~duΟ/>|Q#2g\9s}@@@@ \,G+7]_ .bzQ:09$A2tj*rd*W芋^hU=yRHoǎnr9Xv?~|V 6̍ƍ$v{p=3pbUKׁGJU$2\s5w߭c[Ew(mN V |K7ȑ#qٺm*YJ*WlPӧOaɁMo_K6לwXe-]DV bŊJ ~'OK.j>=M)]ڝ%z*D+    @ `~_Hڹۼk ~/@ 9X&K){fD|Iݤ7};vGyb'J'0O=I*vL;'Loωd|72yf8IIIRJUkNӧsYLsQ/;:H;2ʔ-륅]_~dq,=ߚcpz+EKuw*8}4;cO*      X9݀!9I@Rs`79~A晖jΨ:*+s~/O?y.t~Ur=`v֖>nͲfjyЂ >#(O 6jX*tZ^|t豣@ekY:e;6b>6[1G,Y,ӦHS|dĈ&г~:5zGY>2­[Ȑ<@کLڶUYrls}\ ?l]e/%={2ہdFݠKʕ%o޼v=Tŋe~k[f.3]ny2w簕V9@@@@p}$Nw n*RXqd`~86/U@uMD߮H]®ZjrI5&Y[*n(&W\qlܴ.+VU6V)')9魄F]vwKq,v2#IԜa2ݑʓNre?/̬vP    p m&u9څ'`LQCFZ`y)m    dA.@ 3R)Xq E\=2L       p B0N[t0$2[>F @@@@@@ [ >՜9T\j4ƍyٵ+@@@@@ b, @@@@@@K*` ,Hl|%mG@@@@@91*G@@@@@.@D qB"T+R#     `GVq\9q      0Vs@@@@@@ g 8$\́z     \r X1*˥2B     dXIBe`9`ǁ#     ^1RKl\o-@@@@@@ȱVRD!B+> t@@@@@,C,@.32_f-9   \zݱcUݕ[́3" {M@`I   a.@+o0CK.bK~Kh      Lkߞi'.H\|b      T zȵfWgB .      "î*ؓHlX<      NtT+B       "{Pg`-G4 d`\      \JZKy<% !x)oF@@@@@r `=Z iZ\.G@@@@@.@qKyI5K       9[ !`!́z     \j;Ϩ,2.       @N0s` ǣa4@@@@@@2Ps`=Z|4XjO E@@@@@@r `=ZWX !S     \Ff˿'Z#2?4@@)P $~gJNżZ}׺áMCϻGnq/CǞTXj!nmD(~CaXa9w/2JXDʖA~;yF' :Uڮ\z_ϘJF[l=Q S\{M>bKZp.׺\b?k*pΝ[=@&_:ϴW {sn(z=orWo ]KkqrѬەsP3 eWw쉗j*W  B@U*뷭Co[.3 TXZʗ+)='_)}է%~O|o<8t]s9`KNr6N@xX]%eޒ؅%&.a [e˒%+K34#r<_=oݞ;{z_+~UݷNSi$X˕FݘF ǢJR'M'ic#бC;ӯOO3yH[3*ͦmm|Si*͚E|E?\@v]+W͑c̡#'aӾ][s|Y!7_͔ Qr^.2 &pݬs=i`qZ\q–=hJijfNK,nnj\.N$&]:֭[\j. cpޜeaڊK8}ٳ9|zGeOF$DƏ&OJ-~ZNP[ ^ZuK|jӆn:|Ҹfİb+n\X 4j ӡC{ӮmSPP`0.^l8"ϤQxnsp9E+G"nmo]Va.I۝/` wt]q]vℰq9,m Ԟ'*)4.QP 40:it9/Xyc $vTz31ՙW R\i֯kj*77Q},jĭYtƋ[>ocFڴ&XDp n|SD8+VmB {{&xt s+I@ȷ{A7!iX?q>LJΜ:.+NMn:2yX0,`l!m`qZ\q{X֬64pe4rsaQ.nƹM^2 :NGaoD`_&w7;KgvԷ*x_:sAQgY5y^֛WNnn*ǵ^~4jӆ^ omXqyv"611HD=w,Xp8T-8.r9X1$XXhrϺ q9,/IHR"`XDkbX)ae2$@$<a_!??vҞΕ^=*\uV 7r(2o51a},XS$l 0Əf o%LܶUpIvN:cVf'9Y}𰧥 NJg8qt޹{VMdcfa5J &fnsg7}EMqv Go#LJ (s kRB,p䙕\`%i`^'+N=Iۢj`wIV|jaq#l6#ZN~?02q}@HuRФ?,٣ճiժ߼mw_-jۈ ims93&‡\, pa iCo.+ /GgC{AZiAy7kfQ^E4MSӻ jO!bJ0 pB.1Nnz \mm{NǾ'>5V"nݚtу=Ξ;/$ؗ${\ +rnjT ,l&P񬀶J<\/s-sL4w` Zh|Uzޞ}}fw :k"h8s7L׭2 V7ISU$q Ѯ6 4՞{)3iwXv,& d E5Xc} B$|"}8=yƢj´XI[I!`R.(qֵ?g(O7{xwC wicE#ܸu_Q>i 0NY~B<a:$g o1{}]{a]Dd'Y=pA*vͧojb|zcR|cB:b.i<Ǹ"`MczdA6~w~.8@v3 @=Ą%"=RHHIHя95&cC Nmc gft'lF!ah=N'ar؄؍@e%JZ/f_MkʄVPI/#O϶n*+ !Q_}jz'.(sQ˜tq)^]?ɭLF/xk;&\V0^'yO劓<&L#:znM dP^(+eFA-շ+IG&$y|sqt٣  c.`%n^'k,׷},lAϰ`#ǼEfR^6>n8Wm|&`2l]t>;yhr 3䙴 u9 F fc}'BNоTOjLa(1uuLM/H mRҹ+T Fq8!|vF<:   =rBh)X"n2E  @臆&%܉Sǘ=SO%{2 Bb\ dl])Fىol4aW2v"~Cdσ=C˿Hum ĊARwh6Xހ,昦Lm !8CS+hV?Q6Vb2H~߄1|3M{'jq۱Z `5`,U6#dS}L@[+b DJx Ncb;eM+CIXPsfha"^'@ǢJ5-!?d3{0XBbJFNjvռÎnEھs9+]C<׶ e0Iĸl?s-s8óf) +hHAڤxs BecuWk"jSt/]?[VP*y"֭Z{uXp羧'׽Bq+sݴ7o$07hbа~|6pD)@0"A9zR4w&i`^'++{oa6w$_4/}Oʣrfź`̶BGkZƟ-*sj_a 1Զ$ w=([$ i$mCčrqߋBz~w ->,Շ7أʬIxh;@vX^m۴SZ/ʣ\x^wQ1yG8>I;# @ ev&IHip?sՠTTv~0BhjLow5L`_Yb]-,F;veko7`wuwHoܲw~!~b]' QN'VCղ\֘z?x*@&T G<&ێ2.M& DG_fQꚋĦ3_IB9aREM ]pJ!0H꒰@-hZm+.1qGT+qt3_ .>A2) դj ;&o?ݮ6f^Cs'AAq>qZf5._"Z2iuAC):h>zB6o~$y`vh,nuڌc*j76 6L>tʌ0J/^qҹk7ʰ0A?7`Y: 8Ƣ*g֢"<^'`&&J!I$",\q{,^{dGVqV~^cҼt4]7ߍu)q %m֓fAl|w*#ϐ_6Բ[xį} "To8"w:&} 5XYh 3aeCIE0Y05]H¹~a&IH#kh`5L.S" NdJGЄY0lmLce^=ȗx+\aŚH؀>u6ܟ]QݴLJX:'WЌ$ֺw֙; dD<_4cTGcB_e/W;JGyENf ӭ#2پj-j%'& a"w2M'cMͰW\eFPFk7\yh:5Vt?3K,ZL˪xYPs \lp_ 9kJSj3u :g!렅W&qrúLs's\iCcvֲbT/YLVs ˘ S!qIsuԭO"U M00!ⴿIz~8=`5VP8k::쎛gIbZp]6 jӮay3E5PXGPͩ+\U|=\m))N( X +e2d,D}EMB4\Sn-=7MBp&:h`½dG]qL" tù%81ܶZ\q{*B<}7^it2w-M4O>+߼T#*(7jxpFT}A4.`AYU2͢ =N `A,Wm^w4߉n|}Iylڎ'n ,ldo\cxʄv;-4|hmU8rʧ @2XKDKm )3E h~9ȌM b pXw ?7DQbQu>*LpACko2Nz]q1*$A/ ;1$/n~qF<)kjFZN#Ek:$v1A>\%M㙂t8LuI-bl{bWUwr*W\ʞ$NTZA7mT $SyK7i)دzjpw[k PV kİv,ԥ1fMxh]Wsu5~]mw|Y)G٢bThMF- 4Ot EY}`!|]>%'xX-LU;oi4X5l|fVE:b犗]' 5|0vb] +wܨ-4}w &URd- ,@+70% D G?V4VBX[6w~Ԭ+HQSFþ,IlX*p'_qQ…`:Xͪ-1Qy5M<$a5~jne/eHZ]0]M1+&4nԱuИ $$ @IDAT>6|nڛB Dپ,HVm&`%a$y.*3Tw+S1hlM#jud!B5N!jd𒼧ru/IDMjWx̫ ޯOV9y%i\q{A9SDN˼l0+ia S`wr|OF9Lc4_uOV7nPe?=&Akm|&n0{V^^];@ ?gW%<4OW&aMa?0<Yܱjq~E Đv~V6Da.{~ ];O+ʬ~Iye7WaQ\(i<]a4z+RSv$@$@pL8XB$@́@bzB5@v˄W W5G6Y',cfm21s YIu֭)G'է.sQU{aQEM'!+ 1 ՊՃD~@Q!1qau1-i{yvQ$l`8̅ [dX'&41;mh&&`"!|gL-e+}֚T~rʑ$>6V|6LνRVjVOڮ^nqXIؠ4ڿ<siW0!BQMOoBZ'sv`xfh,5;wM&yO劓^TٓĉJ+4mFzeouZ>/K1eOVײ9&+I[ P܇6LcKCZ8gD:9vL] w8>3ŵi[~C]^Qߺܹrm9uzFqu+=` La!ͪW36 jӆH?%y/~S>[X]wq;;]۶վʬ~Iye7Q]s|IuA4E~a&iڼ hƙVSa|i@"Ϝ3S)i;k< K`E0!(t35 h~i7X' g' 0A:q;I8;ؽcpA={^L.{̮҃$]mj 85M<$a3/+Pj-B?LC¤0`% mxM0ҶN;UCh㢐=ܰaݻg7kjIЇ^\o{'nQ},jM_APfhb=&0ၱuP$lŽlҎq1s1WJ S=kGȳm{=gDg 4m!-;pƾr ϭsUs:=+N{Z1I`QIVm\`MO,ԧ %iċEx$y%i\q{a,S(. IRă˻6yaa sfv/OSl4~`> ϾΆ?,f?hb`^aho,}׆GnA# \*xCcG M{h NiuTnq3\צ ?}/~w+r5xw`kζhİ~Ixe7Q5?'OR;p 7s?uM"C0'%+"#W *w^yFS ᓴ3ё O1!HVx" 4mG͕&&+=#ĩ@j[וA&8m?Q :gش+wL3yEu5(:m%BL&e`I1^4pI`r~A rӻIgWdOjK<_אSF߰eh"[e7^EXZ4cMydZ.nQ},jM_ӹ|m?wLIY.ʍ;|xac?n"(jӾӕ|6lLqʬij=u2vuo)M {VH kAVdB8#\`mXH/^mi]bM0-Ve<@߁71þm?IxeFz?.`L`.8,~B1w¦|I.r+s7 4'\창?]ANҧ? @֤ʽe,h`uj`(c @s"*&Ja0IH\݀^~( Ǯ9a-L}t p?P9aIaǏjWA晀pt W[xQNG?K$d;ą6 pCI=KpNʅ8XU~f-E㒴cX58IؠBkN,Vh?l251o^ĝ.NA-;ys&[@PWtk]ڰ(GV2L^!qcQc!Gfa0 -h'#4^Ϸ]׹,V(@qjOHu%>M 8\evJ^| xcRfrsaԞy 2&-Y6X\7r}pƞ{U{~/oZIo8Z&h']iM;hn"yf}^X%̩l}48eT3TQ{ 8q.N^n=5~fqރ9UKB;{~]kzIy'K3^~8]wV19 m(w$ Š*5QϗXs Z=kWeASdo۰H%!$-@mMGkzڽ؟`zy|{iisvvUưMiN4  ?ϤzlЦ2!Ki!j^i}^G8i8qx$ HLR+]LHZ<LqGbP}h9[ÔL/1UWp'մVM0cMi溟Ւ2j5&=`v({auy~``ŪVXpu]4}}#"yskjW7l}׆ME}j0&q)I8*'RdU\yyOwy] Nh%3c?*'x)]&3`^D+@ ImR-\ĒaTvIߋQEC cCm1<]D; {sɢ|\x+ ~vjc+^r;QA$@ I KaMmo0o   ȋL  fۻ0NTdB+b}5ʍ1c38 @TM@E 1=֖iYgm>y5vȷi7̻׍m݂ͣ^k˲^$@MU*&ӄ`Sh0HHH#j`\E09jZ6oU+m=55 @!M\a pL"֜X gm-y/oF$@$@$@$ ^ 5rb,q݂HHH)^>kͬ0Åa1V  HLE$DHHHH2,j`# 41Plcϝv͸O:k;PEb$@$@$@$@$@$@$@$@$ҢV-2: @x{`x& $mEk # @mEKJSHVmx2. @-  =,\$U &k$ @rV\)+9L$           =_HX`QP @r#c EWX궢itHHHHHHHHHHHHH 6`M,+ HHHHHHHHHHHH e A`-Z=DX)3fr$@$@$@$@$@$@$@$@$@$@$@$@1XebBp!X(AAIHHHHHHHHHHHR'i`Vi` ֶ3a$@$@$@$@$@$@$@$@$@$@$@$@$/*fe65Ep$@$@$@$@$@$@$@$@$@$@$@$@uA .3M            x2%X14 @h`Uh{Y09            ȟ\DXVHHHHHHHHHHHH }GOܻ,Z Vυ) @ e҄`HHHHHHHHHHHH.dZ,X4!X& @$V%ʟC  ,LHHHHHHHHHHHH _X%EfR=(Ñ DŽh`V.a$@$@$@$@$@$@$@$@$@$@$@$@$OURh8 @$@$@$@$@$@$@$@$@$@$@$@$@uD1!HV1f$@$@$@$@$@$@$@$@$@$@$@$@1֤ʽe,h`P+;%           2I&2$        hZԴ Ғ@ {woXZ|+ `+-6 TVSX=*,IHHHHHHHHHE}h[ɸUU:,QS'wޛ㡩x?n*]ƄƱVҁг{3nP3d`ӥsG[s/};O tLxHHHHHHHHHQH:O٨*@ J n MD< }bBph`U6XIAV̼̄LAAAh=+++MюfիE"4L.ϴ`ʃHHHHHHHHH$s, &wΛ!7O&e\9,4f[z>; zms*CR-"        K \eS"5l gT+I]/?_^ncuS<+`h!~3]t2;Jx'+*Y+* EKD%J2n;L7tɴmz9{ٳC`G}r,1!LT6[jEϚrTaSǿzڿ$-p$@$@$@$@$@$@$@$@$@ C >5}ɂc.?>)[Ef 5Nv1ӋaC9nݺN:/@쟵߬߸UeǍiƎAz3d3}d9 uk܋;n179fJe y:͠%~g+(//7v^ FV`}&KJcs^<ڻ=K\QQi~_+K.fԉ~3+~'߯9%Kh`- Jju2Աn~ƿ$-p$@$@$@$@$@$@$@$@$@ C,VǍ2}i@s˫ew͌9pħ_g7>`FX:půoN K$@$@$@$@$@$@$@$@$P _gflr?/šΞn}kΚe&3{iWPuIզM$DƷ~O0ALr=벂AxL $b}'N(j^G醹~?e5*7A2s?i7 `]z,o6S"ZVv1!(XX6h==5eϮXoJ7piJ$@$@$@$@$@$@$@$@$@@S`o3GyiT78KLswfD9s9wѣ5%&',3j0&Z~/gW˧2(\w~·ͦ{nߥKV E 0_a)j^w 3~'밓-f̩-_P[-Tw\:tz5cZrOURh``AySS=hQ^bY3 @=y˺-2ZE_7'K4q M=`~;w6~4c-LuQ-bJpYOtƷ=mo|}f v3~&枻;o_L?0{י93iΝ7?oM-7^oM oXK~6tm[9,}cv^cr}?46^Agk ~џUk7qIZrcicڿQ D_"-qdX         %PWQL޽vo~Gu'{mM~AYP׾ٷ?c%n2dPh^V3fHޮ%[fL^~EL zZ`Z-+K^߽faߟ,e{MV`}5z_uu-YϮ,sR}4Ӵ(4dB2>y3#"y(o^\TT$ӼBpiR_         H 9?3??|uғ.;&[AS.T۶mMNL^=dk`:}|+ϿhOʏӟhAh}]z5+FΝkpM_x& )J? ~=_|NgU&&#?,}wbrK^`?m~x$-9JFtplݺWEg^Z%@M$@$@$@$@$@$@$@$@$@@S`Ξ=g>/ ̚9t!VM&~+J޻o3w~ >2m۶W3ko mfrk[ C ?^>>i:dz8?ca>/a,ImڴÕ_`?4;J}v@ĕJ%1EU/N6eT=?m˩&- M 9˼3M)~ۛdϪ~j09st;K)ɧ{@=pf-7dH{RnO[Сfi~͋+5gI&Bs}wS%z{jfN7?<O6ǎ/_lq`_0gvᬬ؟~?EZrO,Z[*[+RУ}ZS) !        3MYuי=xom?E^{M7Kن|_3''>ӽ[7GVIo" jO- D|ij?<'5={v; >di2+Wꇭ%c<e5{a>70ۼuKJ̧>pAO.7_/|#}%O_sHKRU$X"_o;Ko1/^NӂJa @h,@'?bM) hAϮP/{>uy7JW7oO==Zjͣ?)޾ۿ~̈́cXAcǎwKyߏ~oM}2,Nz'n3`@-̏~k''~6E@GV`M4ݺvuP{~'ǡ;E5N鍠Wy93'wcjps̬]k.'iRDkReYP ^iAo.         J)Wb7/fdz9tN0o6p`3~Ӯ]pS'CYa6mj͕+W=wj;+K m}~Ye]Rj֬`MN&LG4O~^s0CY9w̙5ͼ{~><ij6XMcoUs%\. ޾!挦#>j'fҒXV%{`5m /t$@$@$@$@$@$@$@$@$@@SG>ӿe߿h74pe]fGR mVΝ;yK2h~#tވ.?0P+Wk}~Xhiߣ̈CD!Uʬ\ZXO~ 5ͷs6,kw5!X xmoHiޱ$۶lLQQA$@$@$@$@$@$@$@$@$PwҚ̛֜&*f>Y/umY{iY~?=zg4YF ;w;vI9jowYpˍa%>nssa?a>8@ ϐ!^<;`~MƗ F6-s%o?|Bɺ7Yox;{_7œef葲ES}C*"=}wTӸʕ0jp.x]{U+Jwλ9`8o*ztfڶkk >jn4GuqWTRŏCv P$y$        HJIe<q *'+k jH , @G5IHHHHHHHH6ODM@yo_+֒-`c iAvs         OQ6~R,aS$w)rÕ9n*Tl3VKBm+^ÀH rcf$@$@$@$@$@$@$@$@$@&'k] &@}s큒H<'        hҞl4XF 8Csu[+4#Ą [H*x&-Qe? @$cӬ9Kĝxh ":_Q9[ `-Y&a*XQDi k         G \c-kĝ/xhi=vۿrRLm K         h8a"V2vDۿ2q4XQ? Eid:@\Ccl[+&=EK *QaO$@$@$@$@$@$@$@$@$@-'[D3J0p<خqWT&2SYYi iB0IHHHHHHHHZNطnՌ+`xh%alDkJ%[X4 dB$@$@$@$@$@$@$@$@$@-'[F;Z0p<ԞqWT. ]bBp2 SIV) o1M"+W"IJ_Q  ݅fw@~eф`+ o! BWB;Jj_QX{Ą%%{`Q$@$@$@$@$@$@$@$@$@-'[JKz0p<~qWT>ք[EK=PџHHHHHHHHHHHH>;rEV}f$@$@$@$@$@$@$@$@$@$@$@$@5X{Dk5jb$@$@$@$@$@$@$@$@$@$@$@$@@j` Eń`MsfA$@$@$@$@$@$@$@$@$@$@$@$LUrxHHHHHHHHHHHH kJ݅;Le1wϙ9 @kBLX-m۶fsʕ"            '`MܷzkL}޲ܹu3s             Vu~3~$3f$s1s) E/            '`M?{t̻6s9sqsҥϝ9 @@ȱS+/^,7Ο63g3z3'Eڵk$            % i׮sյkgºݔ[!y9^J!V6S'           p X<.^WA=p)}>##_)cKa).x*.>1~2`OT}Mxs~7M$D <]Pn.],ӣGOӧoӻw?ӥ[w+^U=I$ླྀێMg\j9o]6Ua(l=d 72UBcpg@?||}W?2k3わ{_҉-~oL ~ Ї?C8}S!|OпGwx@ؾ?3̉Gͱ#Lqw=weEUsesa֕KիWa=󁶁#xQJ'J|{I˹olNrƱ^ lxmQuϿ3~d Tޖ)Bid*Qˁ2k?%ߋ_򵞨Gܫw EϨ? |o3#!w_>'kV^(>cWz$_'Q_"B`zC[>o% w32 w2md﫶`Bs˦WIfbOz+>\BnB"gRL`$八 pq>MK#F0C5:t0;f%cIDATw1^^~ޜ9sӧO٣p^9%?o!f!6>6 ūW>aLUih#VI^F#½:4\K7y$ӫwo^3Eر(eW)>@~mڶAm4έ[VZ]֬Y]--kU<-`9dm*_Z!fW _d@¡NV0$pk?;~)*.q1vc"G1G{kl;&]aݱʿk^8a_}Nޓ?)=qJ#|!O??}ֱ?&wlx1o_*c|x}xlG?oڲA@SrLn})+lM Ĺq6HU\õ{U_aGղlNn8\-p`B8q/x>(/]fSv8 ?'Oܭ@W9W6{/quoYXUGMx倧457-y`Z;oV^喖i#C9klTp*d埉Ǻ,d_įf\)'Y!*7b3cts5k.eV{@8'\* qV .+- ӧO3#GS{(PWK.-={H I'QF"L&VZaB>L۶m)+ghH;`Gs3xe083w۷ qo0M/w{םw"C@"e+ ̸F=/K_3k:@A] 6={ʭ4rHr&h |=b^YFJ_^ y}j'ڶhd^]]}׈Kt^߯sҽp4Оw&qr4D*DM`=r7|]?u:ø==jv##o36|s_|?dG!_}.8\zOz^P}dPQ/(c 73GmU>~}c^CU9hNdp{2"?!?6axW"mhz^X$*qJTÆ}nG:i9eJΚDC&+q[o=4-vf 7XAʖ[ /J^"\TU*rUa5ߪ<0͙=ی@ΞrsQLotݴi `Gq횵fLpGА?@4vjڋ@յḱ<Uu]V3zPXGᯟw߿Nlذw9s; ڶkg}Qf6]n%Xry+C=^zi hz65?G2ƏMiIٲXj`\ aptiX0.,q 4E hFj=?n^z npQ"@;U5y[:зZGԚܪ{zG k|/Al/{Z`m%ܺ5o5|0n{$ ,GYA yz_ˁ[W=ף5\Ztqts=j8O w;Fspds5G}C#^kqG7QM #uйq\Vc7>Ϩ{t5ܴ09i`\Wa5=}C#^kqG7QM #uйq\Vc7>Ϩ{t5ܴ09i`\Wa5=}C#^kqG7QM #G4oNG"i:e7Cp˜ 8{?˞z- hrC=gl=?Ueܯ &'e#IENDB`docformatter-1.7.8/docs/make.bat000066400000000000000000000014011517155121300165520ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=source set BUILDDIR=build %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.https://www.sphinx-doc.org/ exit /b 1 ) if "%1" == "" goto help %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd docformatter-1.7.8/docs/source/000077500000000000000000000000001517155121300164515ustar00rootroot00000000000000docformatter-1.7.8/docs/source/authors.rst000066400000000000000000000000601517155121300206640ustar00rootroot00000000000000Authors ======= .. include:: ../../AUTHORS.rst docformatter-1.7.8/docs/source/conf.py000066400000000000000000000017651517155121300177610ustar00rootroot00000000000000# type: ignore # Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information """Configuration file for the Sphinx documentation builder.""" project = "docformatter" copyright = "2022-2023, Steven Myint" author = "Steven Myint" release = "1.7.8" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [] templates_path = ["_templates"] exclude_patterns = [] # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = "alabaster" html_static_path = ["_static"] docformatter-1.7.8/docs/source/configuration.rst000066400000000000000000000106541517155121300220600ustar00rootroot00000000000000How to Configure docformatter ============================= The command line options for ``docformatter`` can also be stored in a configuration file. Currently only ``pyproject.toml``, ``setup.cfg``, and ``tox.ini`` are supported. The configuration file can be passed with a full path. For example: .. code-block:: console $ docformatter --config ~/.secret/path/to/pyproject.toml If no configuration file is explicitly passed, ``docformatter`` will search the current directory for the supported files and use the first one found. The order of precedence is ``pyproject.toml``, ``setup.cfg``, then ``tox.ini``. In ``pyproject.toml``, add a section ``[tool.docformatter]`` with options listed using the same name as command line argument. For example: .. code-block:: yaml [tool.docformatter] recursive = true wrap-summaries = 82 blank = true In ``setup.cfg`` or ``tox.ini``, add a ``[docformatter]`` section. .. code-block:: yaml [docformatter] recursive = true wrap-summaries = 82 blank = true Command line arguments will take precedence over configuration file settings. For example, if the following is in your ``pyproject.toml`` .. code-block:: yaml [tool.docformatter] recursive = true wrap-summaries = 82 wrap-descriptions = 81 blank = true And you invoke docformatter as follows: .. code-block:: console $ docformatter --config ~/.secret/path/to/pyproject.toml --wrap-summaries 68 Summaries will be wrapped at 68, not 82. A Note on Options to Control Styles ----------------------------------- There are various ``docformatter`` options that can be used to control the style of the docstring. These options can be passed on the command line or set in a configuration file. Currently, the style options are: * ``--black`` * ``-s`` or ``--style`` When passing the ``--black`` option, the following arguments are set automatically: * ``--pre-summary-space`` is set to True * ``--wrap-descriptions`` is set to 88 * ``--wrap-summaries`` is set to 88 All of these options can be overridden from the command line or in the configuration file. Further, the ``--pre-summary-space`` option only inserts a space before the summary when the summary begins with a double quote ("). For example: ``"""This summary gets no space."""`` becomes ``"""This summary gets no space."""`` and ``""""This" summary does get a space."""`` becomes ``""" "This" summary does get a space."""`` The ``--style`` argument takes a string which is the name of the field list style you are using. Currently, only ``sphinx`` and ``epytext`` are recognized, but ``numpy`` and ``google`` are future styles. For the selected style, each line in the field lists will be wrapped at the ``--wrap-descriptions`` length as well as any portion of the elaborate description preceding the parameter list. Field lists that don't follow the passed style will cause the entire elaborate description to be ignored and remain unwrapped. A Note on reST Header Adornments Regex -------------------------------------- ``docformatter-1.7.2`` added a new option ``--rest-section-adorns``. This allows for setting the characters used as overline and underline adornments for reST section headers. Per the `ReStructuredText Markup Specification `_, the following are all valid adornment characters, .. code-block:: ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ~ Thus, the default regular expression ``[!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~]{4,}`` looks for any of these characters appearing at least four times in a row. Note that the list of valid adornment characters includes the double quote (") and the greater-than sign (>). Four repetitions was selected because: * Docstrings open and close with triple double quotes. * Doctests begin with >>>. * It would be rare for a section header to consist of fewer than four characters. The user can override this default list of characters by passing a regex from the command line or setting the ``rest-section-adorns`` option in the configuration file. It may be usefule to set this regex to only include the subset of characters you actually use in your docstrings. For example, to only recognize the recommended list in the ReStructuredText Markup Specification, the following regular expression would be used: .. code-block:: [=-`:.'"~^_*+#]{4,} docformatter-1.7.8/docs/source/faq.rst000066400000000000000000000013551517155121300177560ustar00rootroot00000000000000 Known Issues and Idiosyncrasies =============================== There are some know issues or idiosyncrasies when using ``docformatter``. These are stylistic issues and are in the process of being addressed. Wrapping Descriptions --------------------- ``docformatter`` will wrap descriptions, but only in simple cases. If there is text that seems like a bulleted/numbered list, ``docformatter`` will leave the description as is: .. code-block:: rest - Item one. - Item two. - Item three. This prevents the risk of the wrapping turning things into a mess. To force even these instances to get wrapped use ``--force-wrap``. This is being addressed by the constellation of issues related to the various syntaxes used in docstrings. docformatter-1.7.8/docs/source/index.rst000066400000000000000000000010651517155121300203140ustar00rootroot00000000000000.. docformatter documentation master file, created by sphinx-quickstart on Thu Aug 11 18:58:56 2022. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to docformatter! ======================== .. toctree:: :maxdepth: 2 :caption: Contents: installation usage configuration .. toctree:: :maxdepth: 2 :caption: Miscellaneous: requirements faq authors license Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` docformatter-1.7.8/docs/source/installation.rst000066400000000000000000000016611517155121300217100ustar00rootroot00000000000000How to Install docformatter =========================== Install from PyPI ----------------- The latest released version of ``docformatter`` is available from PyPI. To install it using pip: .. code-block:: console $ pip install --upgrade docformatter Extras `````` If you want to use pyproject.toml to configure ``docformatter``, you'll need to install with TOML support: .. code-block:: console $ pip install --upgrade docformatter[tomli] This is only necessary if you are using Python < 3.11. Beginning with Python 3.11, docformatter will utilize ``tomllib`` from the standard library. Install from GitHub ------------------- If you'd like to use an unreleased version, you can also use pip to install ``docformatter`` from GitHub. .. code-block:: console $ python -m pip install git+https://github.com/PyCQA/docformatter.git@v1.5.0-rc1 Replace the tag ``v1.5.0-rc1`` with a commit SHA to install an untagged version. docformatter-1.7.8/docs/source/license.rst000066400000000000000000000000631517155121300206240ustar00rootroot00000000000000License ======= .. literalinclude:: ../../LICENSE docformatter-1.7.8/docs/source/requirements.rst000066400000000000000000000533371517155121300217410ustar00rootroot00000000000000========================= docformatter Requirements ========================= The goal of ``docformatter`` is to be an autoformatting tool for producing PEP 257 compliant docstrings. This document provides a discussion of the requirements from various sources for ``docformatter``. Every effor will be made to keep this document up to date, but this is not a formal requirements document and shouldn't be construed as such. PEP 257 Requirements -------------------- PEP 257 provides conventions for docstrings. Conventions are general agreements or customs of usage rather than strict engineering requirements. This is appropriate for providing guidance to a broad community. In order to provide a tool for automatically formatting or style checking docstrings, however, some objective criteria is needed. Fortunately, the language of PEP 257 lends itself to defining objective criteria, or requirements, for such tools. The conventions in PEP 257 define the high-level structure of docstrings: * How the docstring needs to be formatted. * What information needs to be in a docstring. PEP 257 explicitly ignores markup syntax in the docstring; these are style choices left to the individual or organization to enforce. This gives us two categories of requirements in PEP 257. Let's call them *convention* requirements and *methodology* requirements to be consistent with PEP 257 terminology. An autoformatter should produce docstrings with the proper *convention* so tools such as ``Docutils`` or ``pydocstyle`` can process them properly. The contents of a docstring are irrelevant to tools like ``Docutils`` or ``pydocstyle``. An autoformatter may be able to produce some content, but much of the content requirements would be difficult at best to satisfy automatically. Requirements take one of three types, **shall**, **should**, and **may**. Various sources provide definitions of, and synonyms for, these words. But generally: * **Shall** represents an absolute. * **Should** represents a goal. * **May** represents an option. Thus, an autoformatting tool: * Must produce output that satisfies all the *convention* **shall** requirements. * Ought to provide arguments to allow the user to dictate how each *convention* **should** or **may** requirement is interpreted. * Would be nice to produce as much output that satisfies the *methodology* requirements. * Would be nice to provide arguments to allow the user to turn on/off each *methodology* requirement the tool supports. Docstring Style --------------- There are at least four "flavors" of docstrings in common use today; Epytext, Sphinx, NumPy, and Google. Each of these docstring flavors follow the PEP 257 *convention* requirements. What differs between the three docstring flavors is the reST syntax used in the field list of the multi-line docstring. For example, here is how each syntax documents function arguments. Epytext syntax: .. code-block:: @type num_dogs: int @param num_dogs: the number of dogs Sphinx syntax: .. code-block:: :param param1: The first parameter, defaults to 1. :type: int Google syntax: .. code-block:: Args: param1 (int): The first parameter. NumPy syntax: .. code-block:: Parameters ---------- param1 : int The first parameter. Syntax is also important to ``Docutils``. An autoformatter should be aware of syntactical directives so they can be placed properly in the structure of the docstring. To accommodate the various syntax flavors used in docstrings, a third requirement category is introduced, *style*. Another consideration in the *style* category is line wrapping. According to PEP 257, splitting a one-line docstring is to allow "Emacs’ ``fill-paragraph`` command" to be used. The ``fill-paragraph`` command is a line-wrapping command. Additionally, it would be desirable to wrap docstrings for visual continuity with the code. NumPy makes a stylistic decision to place a blank line after the long description. Some code formatting tools also format docstrings. For example, black places a space before a one-line or the summary line when that line begins with a double quote ("). It would be desirable to provide the user an option to have docformatter also insert this space for compatibility. Thus, an autoformatting tool: * Ought to provide arguments to allow the user to select the *style* or "flavor" of their choice. * Ought to provide arguments to allow the user to, as seamlessly as possible, produce output of a compatible *style* with other formatting tools in the eco-system. * Would be nice to to provide short cut arguments that represent aliases for a commonly used group of *style* arguments. Program Control --------------- Finally, how the ``docformatter`` tool is used should have some user-defined options to accommodate various use-cases. These could best be described as *stakeholder* requirements. An autoformatting tool: * Ought to provide arguments to allow the user to integrate it into their existing workflow. Exceptions and Interpretations `````````````````````````````` As anyone who's ever been involved with turning a set of engineering requirements into a real world product knows, they're never crystal clear and they're always revised along the way. Interpreting and taking exception to the requirements for an aerospace vehicle would be frowned upon without involving the people who wrote the requirements. However, the consequences for a PEP 257 autoformatting tool doing this are slightly less dire. We have confidence the GitHub issue system is the appropriate mechanism if there's a misinterpretation or inappropriate exception taken. The following items are exceptions or interpretations of the PEP 257 requirements: * One-line and summary lines can end with any punctuation. ``docformatter`` will recognize any of [. ! ?]. Exception to requirement PEP_257_4.5; consistent with Google style. See also #56 for situations when this is not desired. * One-line and summary lines will have the first word capitalized. ``docformatter`` will capitalize the first word for grammatical correctness. Interpretation of requirement PEP_257_4.5. Some proper nouns are explicitly spelled using a lowercase letter (e.g., ``docformatter``). A user option is provided for a list of words to maintain lower case. * PEP 257 discusses placing closing quotes on a new line in the multi-line section. However, it really makes no sense here as there is no way this condition could be met for a multi-line docstring. Given the basis provided in PEP 257, this requirement really applies to wrapped one-liners. Thus, this is assumed to apply to wrapped one-liners and the closing quotes will be placed on a line by themselves in this case. However, an argument will be provided to allow the user to select their desired behavior. Interpretation of requirement PEP_257_5.5. These give rise to the *derived* requirement category which would also cover any requirements that must be met for a higher level requirement to be met. The table below summarizes the requirements for ``docformatter``. It includes an ID for reference, the description from PEP 257, which category the requirement falls in, the type of requirement, and whether ``docformatter`` has implemented the requirement. .. csv-table:: **PEP 257 Requirements Summary** :align: left :header: " ID", " Requirement", " Category", " Type", " Implemented" :quote: ' :widths: auto ' PEP_257_1','Always use """triple double quotes"""',' Convention',' Shall',' Yes' ' PEP_257_2','Use r"""raw triple double quotes""" if you use backslashes.',' Convention',' Shall',' Yes' ' PEP_257_3','Use u"""unicode triple double quotes""" for unicode docstrings.',' Convention',' Shall',' Yes' ' PEP_257_4','**One-line docstrings:**' ' PEP_257_4.1',' Should fit on a single line.',' Convention',' Should',' Yes' ' PEP_257_4.2',' Use triple quotes.',' Convention',' Shall',' Yes' ' PEP_257_4.3',' Closing quotes are on the same line as opening quotes.',' Convention',' Shall',' Yes' ' PEP_257_4.4',' No blank line before or after the docstring.',' Convention',' Shall',' Yes' ' PEP_257_4.5',' Is a phrase ending in a period.',' Convention',' Shall',' Yes' ' docformatter_4.5.1', ' One-line docstrings may end in any of the following punctuation marks [. ! ?]', ' Derived', ' May', ' Yes' ' docformatter_4.5.2', ' One-line docstrings will have the first word capitalized.', ' Derived', ' Shall', ' Yes' ' docformatter_4.5.2.1', ' First words in one-line docstrings that are variables or filenames shall remain unchanged.', ' Derived', ' Shall', ' Yes [PR #185, #188]' ' docformatter_4.5.2.2', ' First words in one-line docstrings that are user-specified to not be capitalized shall remain unchanged.', ' Derived', ' Shall', ' Yes [PR #194]' ' docformatter_4.5.3', ' Shall not place a newline after the first line of a wrapped one-line docstring.', ' Derived', ' Shall', ' Yes [PR #179]' ' PEP_257_5','**Multi-line docstrings:**' ' PEP_257_5.1',' A summary is just like a one-line docstring.',' Convention',' Shall',' Yes' ' docformatter_5.1.1', ' The summary line shall satisfy all the requirements of a one-line docstring.', ' Derived', ' Shall', ' Yes' ' PEP_257_5.2',' The summary line may be on the same line as the opening quotes or the next line.',' Convention',' May',' Yes, with option' ' PEP_257_5.3',' A blank line.', ' Convention', ' Shall',' Yes' ' PEP_257_5.4',' A more elaborate description.',' Convention',' Shall',' Yes' ' PEP_257_5.5',' Place the closing quotes on a line by themselves unless the entire docstring fits on a line.',' Convention',' Shall',' Yes, with option' ' docformatter_5.5.1', ' An argument should be provided to allow the user to choose where the closing quotes are placed for one-line docstrings.', ' Derived', ' Should', ' Yes [*PR #104*]' ' PEP_257_5.6',' Indented the same as the quotes at its first line.',' Convention',' Shall',' Yes' ' PEP_257_6','**Class docstrings:**' ' PEP_257_6.1',' Insert blank line after.',' Convention',' Shall',' Yes' ' PEP_257_6.2',' Summarize its behavior.',' Methodology',' Should',' No' ' PEP_257_6.3',' List the public methods and instance variables.',' Methodology',' Should',' No' ' PEP_257_6.4',' List subclass interfaces separately.',' Methodology',' Should',' No' ' PEP_257_6.5',' Class constructor should be documented in the __init__ method docstring.',' Methodology',' Should',' No' ' PEP_257_6.6',' Use the verb "override" to indicate that a subclass method replaces a superclass method.',' Methodology',' Should',' No' ' PEP_257_6.7',' Use the verb "extend" to indicate that a subclass method calls the superclass method and then has additional behavior.', ' Methodology',' Should',' No' ' PEP_257_7','**Script docstring:**' ' PEP_257_7.1',' Should be usable as its "usage" message.',' Methodology',' Should',' No' ' PEP_257_7.2',' Should document the scripts function and command line syntax, environment variables, and files.',' Methodology',' Should',' No' ' PEP_257_8','**Module and Package docstrings:**' ' PEP_257_8.1',' List classes, exceptions, and functions that are exported by the module with a one-line summary of each.',' Methodology',' Should',' No' ' PEP_257_9','**Function and Method docstrings:**' ' PEP_257_9.1',' Summarize its behavior.',' Methodology',' Should',' No' ' PEP_257_9.2',' Document its arguments, return values(s), side effects, exceptions raised, and restrictions on when it can be called.',' Methodology',' Should',' No' ' PEP_257_9.3',' Optional arguments should be indicated.',' Methodology',' Should',' No' ' PEP_257_9.4',' Should be documented whether keyword arguments are part of the interface.',' Methodology',' Should',' No' ' docformatter_10', '**docstring Syntax**' ' docformatter_10.1', ' Should wrap docstrings at n characters.', ' Style', ' Should', ' Yes' ' docformatter_10.1.1', ' Shall not wrap lists, syntax directive statements, or literal blocks', ' Derived', ' Shall', ' Yes' ' docformatter_10.1.1.1', ' Should allow wrapping of lists and syntax directive statements.', ' Stakeholder', ' Should', ' Yes [*PR #5*, *PR #93*]' ' docformatter_10.1.2', ' Should allow/disallow wrapping of one-line docstrings.', ' Derived', ' Should', ' No' ' docformatter_10.1.3', ' Shall not wrap links that exceed the wrap length.', ' Derived', ' Shall', ' Yes [*PR #114*]' ' docformatter_10.1.3.1', ' Shall maintain in-line links on one line even if the resulting line exceeds wrap length.', ' Derived', ' Shall', ' Yes [*PR #152*]' ' docformatter_10.1.3.2', ' Shall not place a newline between description text and a wrapped link.', ' Derived', ' Shall', ' Yes [PR #182]' ' docformatter_10.2', ' Should format docstrings using NumPy style.', ' Style', ' Should', ' No' ' docformatter_10.2.1', ' Shall ignore docstrings in other styles when using NumPy style.', ' Style', ' Shall', ' Yes' ' docformatter_10.2.2', ' Shall wrap NumPy-style parameter descriptions that exceed wrap length when using NumPy style.', ' Shall', ' No' ' docformatter_10.3', ' Should format docstrings using Google style.', ' Style', ' Should', ' No' ' docformatter_10.3.1', ' Shall ignore docstrings in other styles when using Google style.', ' Style', ' Shall', ' Yes' ' docformatter_10.3.2', ' Shall wrap Google-style parameter descriptions that exceed wrap length when using Google style.', ' Shall', ' No' ' docformatter_10.4', ' Should format docstrings using Sphinx style.', ' Style', ' Should', ' Yes' ' docformatter_10.4.1', ' Shall ignore docstrings in other styles when using Sphinx style.', ' Style', ' Shall', ' Yes' ' docformatter_10.4.2', ' Shall wrap Sphinx-style parameter descriptions that exceed wrap length when using Sphinx style.', ' Shall', ' Yes' ' docformatter_10.4.3', ' Shall ensure one blank space between a field name and field body.', ' Style', ' Shall', ' Yes [PR #220]' ' docformatter_10.4.3.1', ' Shall NOT add a blank space after a field name when the field body is a link.', ' Style', ' Shall', ' Yes [PR #220]' ' docformatter_10.4.3.2', ' Shall NOT add a blank space after a field name when there is no field body.', ' Style', ' Shall', ' Yes [PR #220]' ' docformatter_10.5', ' Should format docstrings compatible with black.', ' Style', ' Should', ' Yes [PR #192]' ' docformatter_10.5.1', ' Should wrap summaries at 88 characters by default in black mode.', ' Style', ' Should', ' Yes' ' docformatter_10.5.2', ' Should wrap descriptions at 88 characters by default in black mode.', ' Style', ' Should', ' Yes' ' docformatter_10.5.3', ' Should insert a space before the first word in the summary if that word is quoted when in black mode.', ' Style', ' Should', ' Yes' ' docformatter_10.5.4', ' Default black mode options should be over-rideable by passing arguments or using configuration files.', ' Style', ' Should', ' Yes' ' docformatter_10.6', ' Should format docstrings using Epytext style.', ' Style', ' Should', ' Yes' ' docformatter_10.6.1', ' Shall ignore docstrings in other styles when using Epytext style.', ' Style', ' Shall', ' Yes' ' docformatter_10.6.2', ' Shall wrap Epytext-style parameter descriptions that exceed wrap length when using Epytext style.', ' Shall', ' Yes' ' docformatter_10.7', ' Should format docstrings using ReStructured Text (reST) directives.', ' Style', ' Should', ' No' ' docformatter_10.7.1', ' Shall NOT wrap section headers or their adornments.', ' Style', 'Shall', ' Yes [PR #220]' ' docformatter_10.7.2', ' Shall NOT wrap literal blocks.', ' Style', ' Shall', ' Yes [PR #211]' ' docformatter_11', '**Program Control**' ' docformatter_11.1', ' Should check formatting and report incorrectly documented docstrings.', ' Stakeholder', ' Should', ' Yes [*PR #32*]' ' docformatter_11.2', ' Should fix formatting and save changes to file.', ' Stakeholder', ' Should', ' Yes' ' docformatter_11.3', ' Should only format docstrings that are [minimum, maximum] lines long.', ' Stakeholder', ' Should', ' Yes [*PR #63*]' ' docformatter_11.4', ' Should only format docstrings found between [start, end] lines in the file.', ' Stakeholder', ' Should', ' Yes [*PR #7*}' ' docformatter_11.5', ' Should exclude processing directories and files by name.', ' Stakeholder', ' Should', ' Yes' ' docformatter_11.6', ' Should recursively search directories for files to check and format.', ' Stakeholder', ' Should', ' Yes [*PR #44*]' ' docformatter_11.7', ' Should be able to store configuration options in a configuration file.', ' Stakeholder', ' Should', ' Yes [*PR #77*]' ' docformatter_11.7.1', ' Command line options shall take precedence over configuration file options.', ' Derived', ' Shall', ' Yes' ' docformatter_11.8',' Should read docstrings from stdin and report results to stdout.', ' Stakeholder', ' Should', ' Yes [*PR #8*]' Requirement ID's that begin with PEP_257 are taken from PEP 257. Those prefaced with docformatter are un-related to PEP 257. Test Suite ---------- Each requirement in the table above should have one or more test in the test suite to verify compliance. Ideally the test docstring will reference the requirement(s) it is verifying to provide traceability. Current Implementation ---------------------- ``docformatter`` currently provides the following arguments for interacting with *convention* requirements. :: --pre-summary-newline [boolean, default False] Boolean to indicate whether to place the summary line on the line after the opening quotes in a multi-line docstring. See requirement PEP_257_5.2. ``docformatter`` currently provides these arguments for *style* requirements. :: -s, --style [string, default sphinx] name of the docstring syntax style to use for formatting parameter lists. --rest-section-adorns [REGEX, default [!\"#$%&'()*+,-./\\:;<=>?@[]^_`{|}~]{4,}] regular expression for identifying reST section adornments -n, --non-cap [string, default []] list of words not to capitalize when they appear as the first word in the summary --black [boolean, default False] Boolean to indicate whether to format docstrings to be compatible with black. --blank [boolean, default False] Boolean to indicate whether to add a blank line after the elaborate description. --close-quotes-on-newline [boolean, default False] Boolean to indicate whether to place closing triple quotes on new line for wrapped one-line docstrings. --make-summary-multi-line [boolean, default False] Boolean to indicate whether to add a newline before and after a one-line docstring. This option results in non-conventional docstrings; violates requirements PEP_257_4.1 and PEP_257_4.3. --non-strict [boolean, default False] Boolean to indicate whether to ignore strict compliance with reST list syntax (see issue #67). --pre-summary-space [boolean, default False] Boolean to indicate whether to add a space between the opening triple quotes and the first word in a one-line or summary line of a multi-line docstring. --tab-width [integer, defaults to 1] Sets the number of characters represented by a tab when line wrapping, for Richard Hendricks and others who use tabs instead of spaces. --wrap-descriptions length [integer, default 79] Wrap long descriptions at this length. --wrap-summaries length [integer, default 72] Wrap long one-line docstrings and summary lines in multi-line docstrings at this length. ``docformatter`` currently provides these arguments for *stakeholder* requirements. :: --check Only check and report incorrectly formatted files. --config CONFIG Path to the file containing docformatter options. --docstring-length min_length max_length Only format docstrings that are [min_length, max_length] rows long. --exclude Exclude directories and files by names. --force-wrap Force descriptions to be wrapped even if it may result in a mess. This should likely be removed after implementing the syntax option. --in-place Make changes to files instead of printing diffs. --range start end Only format docstrings that are between [start, end] rows in the file. --recursive Drill down directories recursively. Arguments Needed for Future Releases ------------------------------------ The following are new arguments that are needed to implement **should** or **may** *convention* requirements: :: --wrap-one-line [boolean, default False] Boolean to indicate whether to wrap one-line docstrings. Provides option for requirement PEP_257_4.1. Issue and Version Management ---------------------------- As bug reports and feature requests arise in the GitHub issue system, these will need to be prioritized. The requirement categories, coupled with the urgency of the issue reported can be used to provide the general prioritization scheme: * Priority 1: *convention* **bug** * Priority 2: *style* **bug** * Priority 3: *stakeholder* **bug** * Priority 4: *convention* **enhancement** * Priority 5: *style* **enhancement** * Priority 6: *stakeholder* **enhancement** * Priority 7: **chore** Integration of a bug fix will result in a patch version bump (i.e., 1.5.0 -> 1.5.1). Integration of one or more enhancements will result in a minor version bump (i.e., 1.5.0 -> 1.6.0). One or more release candidates will be provided for each minor or major version bump. These will be indicated by appending `-rcX` to the version number, where the X is the release candidate number beginning with 1. Release candidates will not be uploaded to PyPi, but will be made available via GitHub Releases. docformatter-1.7.8/docs/source/usage.rst000066400000000000000000000225241517155121300203140ustar00rootroot00000000000000How to Use docformatter ======================= There are several ways you can use ``docformatter``. You can use it from the command line, as a file watcher in PyCharm, in your pre-commit checks, and as a GitHub action. However, before you can use ``docformatter``, you'll need to install it. Use from the Command Line ------------------------- To use ``docformatter`` from the command line, simply: .. code-block:: console $ docformatter name_of_python_file.py ``docformatter`` recognizes a number of options for controlling how the tool runs as well as how it will treat various patterns in the docstrings. The help output provides a summary of these options: .. code-block:: console usage: docformatter [-h] [-i | -c] [-d] [-r] [-e [EXCLUDE ...]] [-n [NON-CAP ...]] [-s [style]] [--rest-section-adorns REGEX] [--black] [--wrap-summaries length] [--wrap-descriptions length] [--force-wrap] [--tab-width width] [--blank] [--pre-summary-newline] [--pre-summary-space] [--make-summary-multi-line] [--close-quotes-on-newline] [--range line line] [--docstring-length length length] [--non-strict] [--config CONFIG] [--version] files [files ...] Formats docstrings to follow PEP 257. positional arguments: files files to format or '-' for standard in optional arguments: -h, --help show this help message and exit -i, --in-place make changes to files instead of printing diffs -c, --check only check and report incorrectly formatted files -r, --recursive drill down directories recursively -e, --exclude in recursive mode, exclude directories and files by names -n, --non-cap list of words not to capitalize when they appear as the first word in the summary -s style, --style style the docstring style to use when formatting parameter lists. One of epytext, sphinx. (default: sphinx) --rest-section-adorns REGEX regular expression for identifying reST section adornments (default: [!\"#$%&'()*+,-./\\:;<=>?@[]^_`{|}~]{4,}) --black make formatting compatible with standard black options (default: False) --wrap-summaries length wrap long summary lines at this length; set to 0 to disable wrapping (default: 79, 88 with --black option) --wrap-descriptions length wrap descriptions at this length; set to 0 to disable wrapping (default: 72, 88 with --black option) --force-wrap force descriptions to be wrapped even if it may result in a mess (default: False) --tab_width width tabs in indentation are this many characters when wrapping lines (default: 1) --blank add blank line after elaborate description (default: False) --pre-summary-newline add a newline before one-line or the summary of a multi-line docstring (default: False) --pre-summary-space add a space between the opening triple quotes and the first word in a one-line or summary line of a multi-line docstring (default: False) --make-summary-multi-line add a newline before and after a one-line docstring (default: False) --close-quotes-on-newline place closing triple quotes on a new-line when a one-line docstring wraps to two or more lines (default: False) --range start_line end_line apply docformatter to docstrings between these lines; line numbers are indexed at 1 --docstring-length min_length max_length apply docformatter to docstrings of given length range --non-strict do not strictly follow reST syntax to identify lists (see issue #67) (default: False) --config CONFIG path to file containing docformatter options (default: ./pyproject.toml) --version show program's version number and exit Possible exit codes from ``docformatter``: - **1** - if any error encountered - **2** - if it was interrupted - **3** - if any file needs to be formatted (in ``--check`` or ``--in-place`` mode) Use as a PyCharm File Watcher ----------------------------- ``docformatter`` can be configured as a PyCharm file watcher to automatically format docstrings on saving python files. Head over to ``Preferences > Tools > File Watchers``, click the ``+`` icon and configure ``docformatter`` as shown below: .. image:: https://github.com/PyCQA/docformatter/blob/master/docs/images/pycharm-file-watcher-configurations.png?raw=true :alt: PyCharm file watcher configurations Use with pre-commit ------------------- ``docformatter`` is configured for `pre-commit`_ and can be set up as a hook with the following ``.pre-commit-config.yaml`` configuration: .. _`pre-commit`: https://pre-commit.com/ .. code-block:: yaml - repo: https://github.com/PyCQA/docformatter rev: v1.7.5 hooks: - id: docformatter additional_dependencies: [tomli] args: [--in-place, --config, ./pyproject.toml] You will need to install ``pre-commit`` and run ``pre-commit install``. Whether you use ``args: [--check]`` or ``args: [--in-place]``, the commit will fail if ``docformatter`` processes a change. The ``--in-place`` option fails because pre-commit does a diff check and fails if it detects a hook changed a file. The ``--check`` option fails because ``docformatter`` returns a non-zero exit code. The ``additional_dependencies: [tomli]`` is only required if you are using ``pyproject.toml`` for ``docformatter``'s configuration. Use with GitHub Actions ----------------------- ``docformatter`` is one of the tools included in the `python-lint-plus`_ action. .. _`python-lint-plus`: https://github.com/marketplace/actions/python-code-style-quality-and-lint Dostring Text Patterns ====================== ``docformatter`` began as a simple tool to format docstrings to follow PEP257. It was originally a single Python script of 118 lines containing seven functions. That's no longer the case as an inspection of the codebase will show. Over time, ``docformatter`` has grown to include a number of features that have been requested by its most fantastic user base. In the early days, ``docformatter`` only formatted simple docstrings. "Complex" text patterns like lists, parameter descriptions, and reStructuredText (reST) sections caused ``docformatter`` to simply skip formatting the docstring. As feature requests have been and will be incorporated, ``docformatter`` has gained the ability to recognize and format more complex text patterns. As a result, it is necessary for the user to properly format their docstrings to follow the patterns documented in the various specifications. These specifications would include: - PEP 257 - Docstring Conventions https://www.python.org/dev/peps/pep-0257/ - reStructuredText (reST) Markup Specification https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html - Sphinx Documentation Style https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html - Epydoc Documentation Style http://epydoc.sourceforge.net/manual-fields.html Any docstring that does not follow these specifications may not be formatted properly as these patterns may be recognized by ``docformatter`` as simple text that needs to formatted. For example, if a user writes a docstring that contains a list but does not format the list according to reST specifications, ``docformatter`` may not recognize the list and may format the list items as simple text. This could result in a list that is not properly indented or wrapped. The user is encouraged to read and follow these specifications when writing docstrings to ensure that ``docformatter`` can properly format them. Issues reported to the ``docformatter`` project that are the result of docstrings not following these specifications will be closed as ``S:wontfix`` with a request for the user to update their docstrings to follow the specifications. Additionally, as ``docformatter`` continues to add support for more text patterns (e.g., Numpy or Google style docstrings), new releases may result in significant docstring formatting changes in your code base. While we hate to see this happen to our users, it is the result of our desire to make ``docformatter`` the best tool it can be for formatting docstrings and the best way to achieve that is to strigently comply with the various specifications. We appreciate your understanding and patience as we continue to improve ``docformatter``. docformatter-1.7.8/poetry.lock000066400000000000000000004426131517155121300164270ustar00rootroot00000000000000# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "alabaster" version = "0.7.16" description = "A light, configurable Sphinx theme" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] [[package]] name = "annotated-types" version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" groups = ["linting"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] [[package]] name = "astroid" version = "3.3.11" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.9.0" groups = ["linting"] files = [ {file = "astroid-3.3.11-py3-none-any.whl", hash = "sha256:54c760ae8322ece1abd213057c4b5bba7c49818853fc901ef09719a60dbf9dec"}, {file = "astroid-3.3.11.tar.gz", hash = "sha256:1e5a5011af2920c7c67a53f65d536d65bfa7116feeaf2354d8b94f29573bb0ce"}, ] [package.dependencies] typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} [[package]] name = "autopep8" version = "2.3.2" description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" optional = false python-versions = ">=3.9" groups = ["linting"] files = [ {file = "autopep8-2.3.2-py2.py3-none-any.whl", hash = "sha256:ce8ad498672c845a0c3de2629c15b635ec2b05ef8177a6e7c91c74f3e9b51128"}, {file = "autopep8-2.3.2.tar.gz", hash = "sha256:89440a4f969197b69a995e4ce0661b031f455a9f776d2c5ba3dbd83466931758"}, ] [package.dependencies] pycodestyle = ">=2.12.0" tomli = {version = "*", markers = "python_version < \"3.11\""} [[package]] name = "babel" version = "2.17.0" description = "Internationalization utilities" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, ] [package.extras] dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] [[package]] name = "backports-tarfile" version = "1.2.0" description = "Backport of CPython tarfile module" optional = false python-versions = ">=3.8" groups = ["dev"] markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and python_version <= \"3.11\"" files = [ {file = "backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34"}, {file = "backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["jaraco.test", "pytest (!=8.0.*)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)"] [[package]] name = "black" version = "25.1.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.9" groups = ["linting"] files = [ {file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"}, {file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"}, {file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"}, {file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"}, {file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"}, {file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"}, {file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"}, {file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"}, {file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"}, {file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"}, {file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"}, {file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"}, {file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"}, {file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"}, {file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"}, {file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"}, {file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"}, {file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"}, {file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"}, {file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"}, {file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"}, {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"}, ] [package.dependencies] click = ">=8.0.0" mypy-extensions = ">=0.4.3" packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.10)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "cachetools" version = "6.1.0" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "cachetools-6.1.0-py3-none-any.whl", hash = "sha256:1c7bb3cf9193deaf3508b7c5f2a79986c13ea38965c5adcff1f84519cf39163e"}, {file = "cachetools-6.1.0.tar.gz", hash = "sha256:b4c4f404392848db3ce7aac34950d17be4d864da4b8b66911008e430bc544587"}, ] [[package]] name = "certifi" version = "2025.7.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ {file = "certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2"}, {file = "certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995"}, ] [[package]] name = "cffi" version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" groups = ["dev"] markers = "sys_platform == \"linux\" and platform_python_implementation != \"PyPy\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] [package.dependencies] pycparser = "*" [[package]] name = "chardet" version = "5.2.0" description = "Universal encoding detector for Python 3" optional = false python-versions = ">=3.7" groups = ["dev"] files = [ {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, ] [[package]] name = "charset-normalizer" version = "3.4.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" groups = ["main", "dev"] files = [ {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, ] [[package]] name = "click" version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" groups = ["linting"] markers = "python_version == \"3.9\"" files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "click" version = "8.2.1" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" groups = ["linting"] markers = "python_version >= \"3.10\" and python_version < \"3.12\" or python_version >= \"3.12\"" files = [ {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["dev", "linting", "testing"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] markers = {linting = "platform_system == \"Windows\" or sys_platform == \"win32\"", testing = "sys_platform == \"win32\""} [[package]] name = "coverage" version = "7.9.2" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" groups = ["testing"] files = [ {file = "coverage-7.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:66283a192a14a3854b2e7f3418d7db05cdf411012ab7ff5db98ff3b181e1f912"}, {file = "coverage-7.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4e01d138540ef34fcf35c1aa24d06c3de2a4cffa349e29a10056544f35cca15f"}, {file = "coverage-7.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f22627c1fe2745ee98d3ab87679ca73a97e75ca75eb5faee48660d060875465f"}, {file = "coverage-7.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b1c2d8363247b46bd51f393f86c94096e64a1cf6906803fa8d5a9d03784bdbf"}, {file = "coverage-7.9.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c10c882b114faf82dbd33e876d0cbd5e1d1ebc0d2a74ceef642c6152f3f4d547"}, {file = "coverage-7.9.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:de3c0378bdf7066c3988d66cd5232d161e933b87103b014ab1b0b4676098fa45"}, {file = "coverage-7.9.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1e2f097eae0e5991e7623958a24ced3282676c93c013dde41399ff63e230fcf2"}, {file = "coverage-7.9.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28dc1f67e83a14e7079b6cea4d314bc8b24d1aed42d3582ff89c0295f09b181e"}, {file = "coverage-7.9.2-cp310-cp310-win32.whl", hash = "sha256:bf7d773da6af9e10dbddacbf4e5cab13d06d0ed93561d44dae0188a42c65be7e"}, {file = "coverage-7.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:0c0378ba787681ab1897f7c89b415bd56b0b2d9a47e5a3d8dc0ea55aac118d6c"}, {file = "coverage-7.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a7a56a2964a9687b6aba5b5ced6971af308ef6f79a91043c05dd4ee3ebc3e9ba"}, {file = "coverage-7.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:123d589f32c11d9be7fe2e66d823a236fe759b0096f5db3fb1b75b2fa414a4fa"}, {file = "coverage-7.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:333b2e0ca576a7dbd66e85ab402e35c03b0b22f525eed82681c4b866e2e2653a"}, {file = "coverage-7.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:326802760da234baf9f2f85a39e4a4b5861b94f6c8d95251f699e4f73b1835dc"}, {file = "coverage-7.9.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19e7be4cfec248df38ce40968c95d3952fbffd57b400d4b9bb580f28179556d2"}, {file = "coverage-7.9.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0b4a4cb73b9f2b891c1788711408ef9707666501ba23684387277ededab1097c"}, {file = "coverage-7.9.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2c8937fa16c8c9fbbd9f118588756e7bcdc7e16a470766a9aef912dd3f117dbd"}, {file = "coverage-7.9.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:42da2280c4d30c57a9b578bafd1d4494fa6c056d4c419d9689e66d775539be74"}, {file = "coverage-7.9.2-cp311-cp311-win32.whl", hash = "sha256:14fa8d3da147f5fdf9d298cacc18791818f3f1a9f542c8958b80c228320e90c6"}, {file = "coverage-7.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:549cab4892fc82004f9739963163fd3aac7a7b0df430669b75b86d293d2df2a7"}, {file = "coverage-7.9.2-cp311-cp311-win_arm64.whl", hash = "sha256:c2667a2b913e307f06aa4e5677f01a9746cd08e4b35e14ebcde6420a9ebb4c62"}, {file = "coverage-7.9.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae9eb07f1cfacd9cfe8eaee6f4ff4b8a289a668c39c165cd0c8548484920ffc0"}, {file = "coverage-7.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ce85551f9a1119f02adc46d3014b5ee3f765deac166acf20dbb851ceb79b6f3"}, {file = "coverage-7.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8f6389ac977c5fb322e0e38885fbbf901743f79d47f50db706e7644dcdcb6e1"}, {file = "coverage-7.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff0d9eae8cdfcd58fe7893b88993723583a6ce4dfbfd9f29e001922544f95615"}, {file = "coverage-7.9.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae939811e14e53ed8a9818dad51d434a41ee09df9305663735f2e2d2d7d959b"}, {file = "coverage-7.9.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:31991156251ec202c798501e0a42bbdf2169dcb0f137b1f5c0f4267f3fc68ef9"}, {file = "coverage-7.9.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d0d67963f9cbfc7c7f96d4ac74ed60ecbebd2ea6eeb51887af0f8dce205e545f"}, {file = "coverage-7.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49b752a2858b10580969ec6af6f090a9a440a64a301ac1528d7ca5f7ed497f4d"}, {file = "coverage-7.9.2-cp312-cp312-win32.whl", hash = "sha256:88d7598b8ee130f32f8a43198ee02edd16d7f77692fa056cb779616bbea1b355"}, {file = "coverage-7.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:9dfb070f830739ee49d7c83e4941cc767e503e4394fdecb3b54bfdac1d7662c0"}, {file = "coverage-7.9.2-cp312-cp312-win_arm64.whl", hash = "sha256:4e2c058aef613e79df00e86b6d42a641c877211384ce5bd07585ed7ba71ab31b"}, {file = "coverage-7.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:985abe7f242e0d7bba228ab01070fde1d6c8fa12f142e43debe9ed1dde686038"}, {file = "coverage-7.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c3939264a76d44fde7f213924021ed31f55ef28111a19649fec90c0f109e6d"}, {file = "coverage-7.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae5d563e970dbe04382f736ec214ef48103d1b875967c89d83c6e3f21706d5b3"}, {file = "coverage-7.9.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdd612e59baed2a93c8843c9a7cb902260f181370f1d772f4842987535071d14"}, {file = "coverage-7.9.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:256ea87cb2a1ed992bcdfc349d8042dcea1b80436f4ddf6e246d6bee4b5d73b6"}, {file = "coverage-7.9.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f44ae036b63c8ea432f610534a2668b0c3aee810e7037ab9d8ff6883de480f5b"}, {file = "coverage-7.9.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82d76ad87c932935417a19b10cfe7abb15fd3f923cfe47dbdaa74ef4e503752d"}, {file = "coverage-7.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:619317bb86de4193debc712b9e59d5cffd91dc1d178627ab2a77b9870deb2868"}, {file = "coverage-7.9.2-cp313-cp313-win32.whl", hash = "sha256:0a07757de9feb1dfafd16ab651e0f628fd7ce551604d1bf23e47e1ddca93f08a"}, {file = "coverage-7.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:115db3d1f4d3f35f5bb021e270edd85011934ff97c8797216b62f461dd69374b"}, {file = "coverage-7.9.2-cp313-cp313-win_arm64.whl", hash = "sha256:48f82f889c80af8b2a7bb6e158d95a3fbec6a3453a1004d04e4f3b5945a02694"}, {file = "coverage-7.9.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:55a28954545f9d2f96870b40f6c3386a59ba8ed50caf2d949676dac3ecab99f5"}, {file = "coverage-7.9.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cdef6504637731a63c133bb2e6f0f0214e2748495ec15fe42d1e219d1b133f0b"}, {file = "coverage-7.9.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcd5ebe66c7a97273d5d2ddd4ad0ed2e706b39630ed4b53e713d360626c3dbb3"}, {file = "coverage-7.9.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9303aed20872d7a3c9cb39c5d2b9bdbe44e3a9a1aecb52920f7e7495410dfab8"}, {file = "coverage-7.9.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc18ea9e417a04d1920a9a76fe9ebd2f43ca505b81994598482f938d5c315f46"}, {file = "coverage-7.9.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6406cff19880aaaadc932152242523e892faff224da29e241ce2fca329866584"}, {file = "coverage-7.9.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d0d4f6ecdf37fcc19c88fec3e2277d5dee740fb51ffdd69b9579b8c31e4232e"}, {file = "coverage-7.9.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c33624f50cf8de418ab2b4d6ca9eda96dc45b2c4231336bac91454520e8d1fac"}, {file = "coverage-7.9.2-cp313-cp313t-win32.whl", hash = "sha256:1df6b76e737c6a92210eebcb2390af59a141f9e9430210595251fbaf02d46926"}, {file = "coverage-7.9.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f5fd54310b92741ebe00d9c0d1d7b2b27463952c022da6d47c175d246a98d1bd"}, {file = "coverage-7.9.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c48c2375287108c887ee87d13b4070a381c6537d30e8487b24ec721bf2a781cb"}, {file = "coverage-7.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ddc39510ac922a5c4c27849b739f875d3e1d9e590d1e7b64c98dadf037a16cce"}, {file = "coverage-7.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a535c0c7364acd55229749c2b3e5eebf141865de3a8f697076a3291985f02d30"}, {file = "coverage-7.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df0f9ef28e0f20c767ccdccfc5ae5f83a6f4a2fbdfbcbcc8487a8a78771168c8"}, {file = "coverage-7.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f3da12e0ccbcb348969221d29441ac714bbddc4d74e13923d3d5a7a0bebef7a"}, {file = "coverage-7.9.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a17eaf46f56ae0f870f14a3cbc2e4632fe3771eab7f687eda1ee59b73d09fe4"}, {file = "coverage-7.9.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:669135a9d25df55d1ed56a11bf555f37c922cf08d80799d4f65d77d7d6123fcf"}, {file = "coverage-7.9.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9d3a700304d01a627df9db4322dc082a0ce1e8fc74ac238e2af39ced4c083193"}, {file = "coverage-7.9.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:71ae8b53855644a0b1579d4041304ddc9995c7b21c8a1f16753c4d8903b4dfed"}, {file = "coverage-7.9.2-cp39-cp39-win32.whl", hash = "sha256:dd7a57b33b5cf27acb491e890720af45db05589a80c1ffc798462a765be6d4d7"}, {file = "coverage-7.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:f65bb452e579d5540c8b37ec105dd54d8b9307b07bcaa186818c104ffda22441"}, {file = "coverage-7.9.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:8a1166db2fb62473285bcb092f586e081e92656c7dfa8e9f62b4d39d7e6b5050"}, {file = "coverage-7.9.2-py3-none-any.whl", hash = "sha256:e425cd5b00f6fc0ed7cdbd766c70be8baab4b7839e4d4fe5fac48581dd968ea4"}, {file = "coverage-7.9.2.tar.gz", hash = "sha256:997024fa51e3290264ffd7492ec97d0690293ccd2b45a6cd7d82d945a4a80c8b"}, ] [package.dependencies] tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "cryptography" version = "43.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" groups = ["dev"] markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\" and python_version == \"3.9\"" files = [ {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, ] [package.dependencies] cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} [package.extras] docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] nox = ["nox"] pep8test = ["check-sdist", "click", "mypy", "ruff"] sdist = ["build"] ssh = ["bcrypt (>=3.1.5)"] test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] [[package]] name = "cryptography" version = "45.0.5" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.7" groups = ["dev"] markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\" and python_version >= \"3.10\"" files = [ {file = "cryptography-45.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:101ee65078f6dd3e5a028d4f19c07ffa4dd22cce6a20eaa160f8b5219911e7d8"}, {file = "cryptography-45.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a264aae5f7fbb089dbc01e0242d3b67dffe3e6292e1f5182122bdf58e65215d"}, {file = "cryptography-45.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e74d30ec9c7cb2f404af331d5b4099a9b322a8a6b25c4632755c8757345baac5"}, {file = "cryptography-45.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3af26738f2db354aafe492fb3869e955b12b2ef2e16908c8b9cb928128d42c57"}, {file = "cryptography-45.0.5-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e6c00130ed423201c5bc5544c23359141660b07999ad82e34e7bb8f882bb78e0"}, {file = "cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:dd420e577921c8c2d31289536c386aaa30140b473835e97f83bc71ea9d2baf2d"}, {file = "cryptography-45.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d05a38884db2ba215218745f0781775806bde4f32e07b135348355fe8e4991d9"}, {file = "cryptography-45.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ad0caded895a00261a5b4aa9af828baede54638754b51955a0ac75576b831b27"}, {file = "cryptography-45.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9024beb59aca9d31d36fcdc1604dd9bbeed0a55bface9f1908df19178e2f116e"}, {file = "cryptography-45.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91098f02ca81579c85f66df8a588c78f331ca19089763d733e34ad359f474174"}, {file = "cryptography-45.0.5-cp311-abi3-win32.whl", hash = "sha256:926c3ea71a6043921050eaa639137e13dbe7b4ab25800932a8498364fc1abec9"}, {file = "cryptography-45.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:b85980d1e345fe769cfc57c57db2b59cff5464ee0c045d52c0df087e926fbe63"}, {file = "cryptography-45.0.5-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3562c2f23c612f2e4a6964a61d942f891d29ee320edb62ff48ffb99f3de9ae8"}, {file = "cryptography-45.0.5-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3fcfbefc4a7f332dece7272a88e410f611e79458fab97b5efe14e54fe476f4fd"}, {file = "cryptography-45.0.5-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:460f8c39ba66af7db0545a8c6f2eabcbc5a5528fc1cf6c3fa9a1e44cec33385e"}, {file = "cryptography-45.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9b4cf6318915dccfe218e69bbec417fdd7c7185aa7aab139a2c0beb7468c89f0"}, {file = "cryptography-45.0.5-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2089cc8f70a6e454601525e5bf2779e665d7865af002a5dec8d14e561002e135"}, {file = "cryptography-45.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0027d566d65a38497bc37e0dd7c2f8ceda73597d2ac9ba93810204f56f52ebc7"}, {file = "cryptography-45.0.5-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:be97d3a19c16a9be00edf79dca949c8fa7eff621763666a145f9f9535a5d7f42"}, {file = "cryptography-45.0.5-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:7760c1c2e1a7084153a0f68fab76e754083b126a47d0117c9ed15e69e2103492"}, {file = "cryptography-45.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ff8728d8d890b3dda5765276d1bc6fb099252915a2cd3aff960c4c195745dd0"}, {file = "cryptography-45.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7259038202a47fdecee7e62e0fd0b0738b6daa335354396c6ddebdbe1206af2a"}, {file = "cryptography-45.0.5-cp37-abi3-win32.whl", hash = "sha256:1e1da5accc0c750056c556a93c3e9cb828970206c68867712ca5805e46dc806f"}, {file = "cryptography-45.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:90cb0a7bb35959f37e23303b7eed0a32280510030daba3f7fdfbb65defde6a97"}, {file = "cryptography-45.0.5-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:206210d03c1193f4e1ff681d22885181d47efa1ab3018766a7b32a7b3d6e6afd"}, {file = "cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c648025b6840fe62e57107e0a25f604db740e728bd67da4f6f060f03017d5097"}, {file = "cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b8fa8b0a35a9982a3c60ec79905ba5bb090fc0b9addcfd3dc2dd04267e45f25e"}, {file = "cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:14d96584701a887763384f3c47f0ca7c1cce322aa1c31172680eb596b890ec30"}, {file = "cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57c816dfbd1659a367831baca4b775b2a5b43c003daf52e9d57e1d30bc2e1b0e"}, {file = "cryptography-45.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b9e38e0a83cd51e07f5a48ff9691cae95a79bea28fe4ded168a8e5c6c77e819d"}, {file = "cryptography-45.0.5-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8c4a6ff8a30e9e3d38ac0539e9a9e02540ab3f827a3394f8852432f6b0ea152e"}, {file = "cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bd4c45986472694e5121084c6ebbd112aa919a25e783b87eb95953c9573906d6"}, {file = "cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:982518cd64c54fcada9d7e5cf28eabd3ee76bd03ab18e08a48cad7e8b6f31b18"}, {file = "cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:12e55281d993a793b0e883066f590c1ae1e802e3acb67f8b442e721e475e6463"}, {file = "cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:5aa1e32983d4443e310f726ee4b071ab7569f58eedfdd65e9675484a4eb67bd1"}, {file = "cryptography-45.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e357286c1b76403dd384d938f93c46b2b058ed4dfcdce64a770f0537ed3feb6f"}, {file = "cryptography-45.0.5.tar.gz", hash = "sha256:72e76caa004ab63accdf26023fccd1d087f6d90ec6048ff33ad0445abf7f605a"}, ] [package.dependencies] cffi = {version = ">=1.14", markers = "platform_python_implementation != \"PyPy\""} [package.extras] docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs ; python_full_version >= \"3.8.0\"", "sphinx-rtd-theme (>=3.0.0) ; python_full_version >= \"3.8.0\""] docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_full_version >= \"3.8.0\""] pep8test = ["check-sdist ; python_full_version >= \"3.8.0\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] test = ["certifi (>=2024)", "cryptography-vectors (==45.0.5)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] [[package]] name = "dill" version = "0.4.0" description = "serialize all of Python" optional = false python-versions = ">=3.8" groups = ["linting"] files = [ {file = "dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049"}, {file = "dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0"}, ] [package.extras] graph = ["objgraph (>=1.7.2)"] profile = ["gprof2dot (>=2022.7.29)"] [[package]] name = "distlib" version = "0.4.0" description = "Distribution utilities" optional = false python-versions = "*" groups = ["dev"] files = [ {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, ] [[package]] name = "docutils" version = "0.19" description = "Docutils -- Python Documentation Utilities" optional = false python-versions = ">=3.7" groups = ["dev", "linting"] files = [ {file = "docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"}, {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"}, ] [[package]] name = "exceptiongroup" version = "1.3.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["testing"] markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, ] [package.dependencies] typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} [package.extras] test = ["pytest (>=6)"] [[package]] name = "filelock" version = "3.18.0" description = "A platform independent file lock." optional = false python-versions = ">=3.9" groups = ["dev"] markers = "python_version == \"3.9\"" files = [ {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, ] [package.extras] docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] [[package]] name = "filelock" version = "3.20.3" description = "A platform independent file lock." optional = false python-versions = ">=3.10" groups = ["dev"] markers = "python_version >= \"3.10\" and python_version < \"3.12\" or python_version >= \"3.12\"" files = [ {file = "filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1"}, {file = "filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1"}, ] [[package]] name = "id" version = "1.5.0" description = "A tool for generating OIDC identities" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658"}, {file = "id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d"}, ] [package.dependencies] requests = "*" [package.extras] dev = ["build", "bump (>=1.3.2)", "id[lint,test]"] lint = ["bandit", "interrogate", "mypy", "ruff (<0.8.2)", "types-requests"] test = ["coverage[toml]", "pretend", "pytest", "pytest-cov"] [[package]] name = "idna" version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" groups = ["dev"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] name = "imagesize" version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" groups = ["dev"] files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] [[package]] name = "importlib-metadata" version = "8.7.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.9" groups = ["dev"] markers = "python_version <= \"3.11\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\" or python_version == \"3.9\"" files = [ {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, ] [package.dependencies] zipp = ">=3.20" [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] type = ["pytest-mypy"] [[package]] name = "iniconfig" version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" groups = ["testing"] files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] [[package]] name = "isort" version = "6.0.1" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.9.0" groups = ["linting"] files = [ {file = "isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615"}, {file = "isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450"}, ] [package.extras] colors = ["colorama"] plugins = ["setuptools"] [[package]] name = "jaraco-classes" version = "3.4.0" description = "Utility functions for Python class constructs" optional = false python-versions = ">=3.8" groups = ["dev"] markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" files = [ {file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"}, {file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"}, ] [package.dependencies] more-itertools = "*" [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [[package]] name = "jaraco-context" version = "6.0.1" description = "Useful decorators and context managers" optional = false python-versions = ">=3.8" groups = ["dev"] markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" files = [ {file = "jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4"}, {file = "jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3"}, ] [package.dependencies] "backports.tarfile" = {version = "*", markers = "python_version < \"3.12\""} [package.extras] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] test = ["portend", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] [[package]] name = "jaraco-functools" version = "4.2.1" description = "Functools like those found in stdlib" optional = false python-versions = ">=3.9" groups = ["dev"] markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" files = [ {file = "jaraco_functools-4.2.1-py3-none-any.whl", hash = "sha256:590486285803805f4b1f99c60ca9e94ed348d4added84b74c7a12885561e524e"}, {file = "jaraco_functools-4.2.1.tar.gz", hash = "sha256:be634abfccabce56fa3053f8c7ebe37b682683a4ee7793670ced17bab0087353"}, ] [package.dependencies] more_itertools = "*" [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] test = ["jaraco.classes", "pytest (>=6,!=8.1.*)"] type = ["pytest-mypy"] [[package]] name = "jeepney" version = "0.9.0" description = "Low-level, pure Python DBus protocol wrapper." optional = false python-versions = ">=3.7" groups = ["dev"] markers = "sys_platform == \"linux\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" files = [ {file = "jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683"}, {file = "jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732"}, ] [package.extras] test = ["async-timeout ; python_version < \"3.11\"", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] trio = ["trio"] [[package]] name = "jinja2" version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, ] [package.dependencies] MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] [[package]] name = "keyring" version = "25.6.0" description = "Store and access your passwords safely." optional = false python-versions = ">=3.9" groups = ["dev"] markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" files = [ {file = "keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd"}, {file = "keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66"}, ] [package.dependencies] importlib_metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} "jaraco.classes" = "*" "jaraco.context" = "*" "jaraco.functools" = "*" jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] completion = ["shtab (>=1.1.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] test = ["pyfakefs", "pytest (>=6,!=8.1.*)"] type = ["pygobject-stubs", "pytest-mypy", "shtab", "types-pywin32"] [[package]] name = "markdown-it-py" version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.8" groups = ["dev", "linting"] files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, ] [package.dependencies] mdurl = ">=0.1,<1.0" [package.extras] benchmarking = ["psutil", "pytest", "pytest-benchmark"] code-style = ["pre-commit (>=3.0,<4.0)"] compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] linkify = ["linkify-it-py (>=1,<3)"] plugins = ["mdit-py-plugins"] profiling = ["gprof2dot"] rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "markupsafe" version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] [[package]] name = "mccabe" version = "0.7.0" description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" groups = ["linting"] files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] [[package]] name = "mdurl" version = "0.1.2" description = "Markdown URL utilities" optional = false python-versions = ">=3.7" groups = ["dev", "linting"] files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] [[package]] name = "mock" version = "5.2.0" description = "Rolling backport of unittest.mock for all Pythons" optional = false python-versions = ">=3.6" groups = ["testing"] files = [ {file = "mock-5.2.0-py3-none-any.whl", hash = "sha256:7ba87f72ca0e915175596069dbbcc7c75af7b5e9b9bc107ad6349ede0819982f"}, {file = "mock-5.2.0.tar.gz", hash = "sha256:4e460e818629b4b173f32d08bf30d3af8123afbb8e04bb5707a1fd4799e503f0"}, ] [package.extras] build = ["blurb", "twine", "wheel"] docs = ["sphinx"] test = ["pytest", "pytest-cov"] [[package]] name = "more-itertools" version = "10.7.0" description = "More routines for operating on iterables, beyond itertools" optional = false python-versions = ">=3.9" groups = ["dev"] markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" files = [ {file = "more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e"}, {file = "more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3"}, ] [[package]] name = "mypy" version = "1.17.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.9" groups = ["linting"] files = [ {file = "mypy-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8e08de6138043108b3b18f09d3f817a4783912e48828ab397ecf183135d84d6"}, {file = "mypy-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce4a17920ec144647d448fc43725b5873548b1aae6c603225626747ededf582d"}, {file = "mypy-1.17.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ff25d151cc057fdddb1cb1881ef36e9c41fa2a5e78d8dd71bee6e4dcd2bc05b"}, {file = "mypy-1.17.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93468cf29aa9a132bceb103bd8475f78cacde2b1b9a94fd978d50d4bdf616c9a"}, {file = "mypy-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:98189382b310f16343151f65dd7e6867386d3e35f7878c45cfa11383d175d91f"}, {file = "mypy-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:c004135a300ab06a045c1c0d8e3f10215e71d7b4f5bb9a42ab80236364429937"}, {file = "mypy-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d4fe5c72fd262d9c2c91c1117d16aac555e05f5beb2bae6a755274c6eec42be"}, {file = "mypy-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96b196e5c16f41b4f7736840e8455958e832871990c7ba26bf58175e357ed61"}, {file = "mypy-1.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73a0ff2dd10337ceb521c080d4147755ee302dcde6e1a913babd59473904615f"}, {file = "mypy-1.17.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cfcc1179c4447854e9e406d3af0f77736d631ec87d31c6281ecd5025df625d"}, {file = "mypy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c56f180ff6430e6373db7a1d569317675b0a451caf5fef6ce4ab365f5f2f6c3"}, {file = "mypy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:eafaf8b9252734400f9b77df98b4eee3d2eecab16104680d51341c75702cad70"}, {file = "mypy-1.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f986f1cab8dbec39ba6e0eaa42d4d3ac6686516a5d3dccd64be095db05ebc6bb"}, {file = "mypy-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:51e455a54d199dd6e931cd7ea987d061c2afbaf0960f7f66deef47c90d1b304d"}, {file = "mypy-1.17.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3204d773bab5ff4ebbd1f8efa11b498027cd57017c003ae970f310e5b96be8d8"}, {file = "mypy-1.17.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1051df7ec0886fa246a530ae917c473491e9a0ba6938cfd0ec2abc1076495c3e"}, {file = "mypy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f773c6d14dcc108a5b141b4456b0871df638eb411a89cd1c0c001fc4a9d08fc8"}, {file = "mypy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:1619a485fd0e9c959b943c7b519ed26b712de3002d7de43154a489a2d0fd817d"}, {file = "mypy-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c41aa59211e49d717d92b3bb1238c06d387c9325d3122085113c79118bebb06"}, {file = "mypy-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e69db1fb65b3114f98c753e3930a00514f5b68794ba80590eb02090d54a5d4a"}, {file = "mypy-1.17.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03ba330b76710f83d6ac500053f7727270b6b8553b0423348ffb3af6f2f7b889"}, {file = "mypy-1.17.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:037bc0f0b124ce46bfde955c647f3e395c6174476a968c0f22c95a8d2f589bba"}, {file = "mypy-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c38876106cb6132259683632b287238858bd58de267d80defb6f418e9ee50658"}, {file = "mypy-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:d30ba01c0f151998f367506fab31c2ac4527e6a7b2690107c7a7f9e3cb419a9c"}, {file = "mypy-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:63e751f1b5ab51d6f3d219fe3a2fe4523eaa387d854ad06906c63883fde5b1ab"}, {file = "mypy-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f7fb09d05e0f1c329a36dcd30e27564a3555717cde87301fae4fb542402ddfad"}, {file = "mypy-1.17.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b72c34ce05ac3a1361ae2ebb50757fb6e3624032d91488d93544e9f82db0ed6c"}, {file = "mypy-1.17.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434ad499ad8dde8b2f6391ddfa982f41cb07ccda8e3c67781b1bfd4e5f9450a8"}, {file = "mypy-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f105f61a5eff52e137fd73bee32958b2add9d9f0a856f17314018646af838e97"}, {file = "mypy-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:ba06254a5a22729853209550d80f94e28690d5530c661f9416a68ac097b13fc4"}, {file = "mypy-1.17.0-py3-none-any.whl", hash = "sha256:15d9d0018237ab058e5de3d8fce61b6fa72cc59cc78fd91f1b474bce12abf496"}, {file = "mypy-1.17.0.tar.gz", hash = "sha256:e5d7ccc08ba089c06e2f5629c660388ef1fee708444f1dee0b9203fa031dee03"}, ] [package.dependencies] mypy_extensions = ">=1.0.0" pathspec = ">=0.9.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing_extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] faster-cache = ["orjson"] install-types = ["pip"] mypyc = ["setuptools (>=50)"] reports = ["lxml"] [[package]] name = "mypy-extensions" version = "1.1.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.8" groups = ["linting"] files = [ {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] [[package]] name = "nh3" version = "0.3.0" description = "Python binding to Ammonia HTML sanitizer Rust crate" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "nh3-0.3.0-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a537ece1bf513e5a88d8cff8a872e12fe8d0f42ef71dd15a5e7520fecd191bbb"}, {file = "nh3-0.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c915060a2c8131bef6a29f78debc29ba40859b6dbe2362ef9e5fd44f11487c2"}, {file = "nh3-0.3.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba0caa8aa184196daa6e574d997a33867d6d10234018012d35f86d46024a2a95"}, {file = "nh3-0.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:80fe20171c6da69c7978ecba33b638e951b85fb92059259edd285ff108b82a6d"}, {file = "nh3-0.3.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e90883f9f85288f423c77b3f5a6f4486375636f25f793165112679a7b6363b35"}, {file = "nh3-0.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0649464ac8eee018644aacbc103874ccbfac80e3035643c3acaab4287e36e7f5"}, {file = "nh3-0.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1adeb1062a1c2974bc75b8d1ecb014c5fd4daf2df646bbe2831f7c23659793f9"}, {file = "nh3-0.3.0-cp313-cp313t-win32.whl", hash = "sha256:7275fdffaab10cc5801bf026e3c089d8de40a997afc9e41b981f7ac48c5aa7d5"}, {file = "nh3-0.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:423201bbdf3164a9e09aa01e540adbb94c9962cc177d5b1cbb385f5e1e79216e"}, {file = "nh3-0.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:16f8670201f7e8e0e05ed1a590eb84bfa51b01a69dd5caf1d3ea57733de6a52f"}, {file = "nh3-0.3.0-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ec6cfdd2e0399cb79ba4dcffb2332b94d9696c52272ff9d48a630c5dca5e325a"}, {file = "nh3-0.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5e7185599f89b0e391e2f29cc12dc2e206167380cea49b33beda4891be2fe1"}, {file = "nh3-0.3.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:389d93d59b8214d51c400fb5b07866c2a4f79e4e14b071ad66c92184fec3a392"}, {file = "nh3-0.3.0-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e9e6a7e4d38f7e8dda9edd1433af5170c597336c1a74b4693c5cb75ab2b30f2a"}, {file = "nh3-0.3.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7852f038a054e0096dac12b8141191e02e93e0b4608c4b993ec7d4ffafea4e49"}, {file = "nh3-0.3.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af5aa8127f62bbf03d68f67a956627b1bd0469703a35b3dad28d0c1195e6c7fb"}, {file = "nh3-0.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f416c35efee3e6a6c9ab7716d9e57aa0a49981be915963a82697952cba1353e1"}, {file = "nh3-0.3.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37d3003d98dedca6cd762bf88f2e70b67f05100f6b949ffe540e189cc06887f9"}, {file = "nh3-0.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:634e34e6162e0408e14fb61d5e69dbaea32f59e847cfcfa41b66100a6b796f62"}, {file = "nh3-0.3.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:b0612ccf5de8a480cf08f047b08f9d3fecc12e63d2ee91769cb19d7290614c23"}, {file = "nh3-0.3.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c7a32a7f0d89f7d30cb8f4a84bdbd56d1eb88b78a2434534f62c71dac538c450"}, {file = "nh3-0.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3f1b4f8a264a0c86ea01da0d0c390fe295ea0bcacc52c2103aca286f6884f518"}, {file = "nh3-0.3.0-cp38-abi3-win32.whl", hash = "sha256:6d68fa277b4a3cf04e5c4b84dd0c6149ff7d56c12b3e3fab304c525b850f613d"}, {file = "nh3-0.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:bae63772408fd63ad836ec569a7c8f444dd32863d0c67f6e0b25ebbd606afa95"}, {file = "nh3-0.3.0-cp38-abi3-win_arm64.whl", hash = "sha256:d97d3efd61404af7e5721a0e74d81cdbfc6e5f97e11e731bb6d090e30a7b62b2"}, {file = "nh3-0.3.0.tar.gz", hash = "sha256:d8ba24cb31525492ea71b6aac11a4adac91d828aadeff7c4586541bf5dc34d2f"}, ] [[package]] name = "packaging" version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["dev", "linting", "testing"] files = [ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] [[package]] name = "pathspec" version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" groups = ["linting"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] [[package]] name = "platformdirs" version = "4.3.8" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.9" groups = ["dev", "linting"] files = [ {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, ] [package.extras] docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] type = ["mypy (>=1.14.1)"] [[package]] name = "pluggy" version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.9" groups = ["dev", "testing"] files = [ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "pycodestyle" version = "2.14.0" description = "Python style guide checker" optional = false python-versions = ">=3.9" groups = ["linting"] files = [ {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, ] [[package]] name = "pycparser" version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" groups = ["dev"] markers = "sys_platform == \"linux\" and platform_python_implementation != \"PyPy\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] [[package]] name = "pydantic" version = "2.11.7" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" groups = ["linting"] files = [ {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, ] [package.dependencies] annotated-types = ">=0.6.0" pydantic-core = "2.33.2" typing-extensions = ">=4.12.2" typing-inspection = ">=0.4.0" [package.extras] email = ["email-validator (>=2.0.0)"] timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" version = "2.33.2" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" groups = ["linting"] files = [ {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, ] [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pydocstyle" version = "6.3.0" description = "Python docstring style checker" optional = false python-versions = ">=3.6" groups = ["linting"] files = [ {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, ] [package.dependencies] snowballstemmer = ">=2.2.0" [package.extras] toml = ["tomli (>=1.2.3) ; python_version < \"3.11\""] [[package]] name = "pygments" version = "2.20.0" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.9" groups = ["dev", "linting", "testing"] files = [ {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, ] [package.extras] windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pylint" version = "3.3.7" description = "python code static checker" optional = false python-versions = ">=3.9.0" groups = ["linting"] files = [ {file = "pylint-3.3.7-py3-none-any.whl", hash = "sha256:43860aafefce92fca4cf6b61fe199cdc5ae54ea28f9bf4cd49de267b5195803d"}, {file = "pylint-3.3.7.tar.gz", hash = "sha256:2b11de8bde49f9c5059452e0c310c079c746a0a8eeaa789e5aa966ecc23e4559"}, ] [package.dependencies] astroid = ">=3.3.8,<=3.4.0.dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, {version = ">=0.3.6", markers = "python_version == \"3.11\""}, ] isort = ">=4.2.5,<5.13 || >5.13,<7" mccabe = ">=0.6,<0.8" platformdirs = ">=2.2" tomli = {version = ">=1.1", markers = "python_version < \"3.11\""} tomlkit = ">=0.10.1" typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} [package.extras] spelling = ["pyenchant (>=3.2,<4.0)"] testutils = ["gitpython (>3)"] [[package]] name = "pyproject-api" version = "1.9.1" description = "API to interact with the python pyproject.toml based projects" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "pyproject_api-1.9.1-py3-none-any.whl", hash = "sha256:7d6238d92f8962773dd75b5f0c4a6a27cce092a14b623b811dba656f3b628948"}, {file = "pyproject_api-1.9.1.tar.gz", hash = "sha256:43c9918f49daab37e302038fc1aed54a8c7a91a9fa935d00b9a485f37e0f5335"}, ] [package.dependencies] packaging = ">=25" tomli = {version = ">=2.2.1", markers = "python_version < \"3.11\""} [package.extras] docs = ["furo (>=2024.8.6)", "sphinx-autodoc-typehints (>=3.2)"] testing = ["covdefaults (>=2.3)", "pytest (>=8.3.5)", "pytest-cov (>=6.1.1)", "pytest-mock (>=3.14)", "setuptools (>=80.3.1)"] [[package]] name = "pytest" version = "8.4.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.9" groups = ["testing"] files = [ {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, ] [package.dependencies] colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} iniconfig = ">=1" packaging = ">=20" pluggy = ">=1.5,<2" pygments = ">=2.7.2" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" version = "6.2.1" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" groups = ["testing"] files = [ {file = "pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5"}, {file = "pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2"}, ] [package.dependencies] coverage = {version = ">=7.5", extras = ["toml"]} pluggy = ">=1.2" pytest = ">=6.2.5" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "pytest-order" version = "1.3.0" description = "pytest plugin to run your tests in a specific order" optional = false python-versions = ">=3.7" groups = ["testing"] files = [ {file = "pytest_order-1.3.0-py3-none-any.whl", hash = "sha256:2cd562a21380345dd8d5774aa5fd38b7849b6ee7397ca5f6999bbe6e89f07f6e"}, {file = "pytest_order-1.3.0.tar.gz", hash = "sha256:51608fec3d3ee9c0adaea94daa124a5c4c1d2bb99b00269f098f414307f23dde"}, ] [package.dependencies] pytest = [ {version = ">=5.0", markers = "python_version < \"3.10\""}, {version = ">=6.2.4", markers = "python_version >= \"3.10\""}, ] [[package]] name = "pywin32-ctypes" version = "0.2.3" description = "A (partial) reimplementation of pywin32 using ctypes/cffi" optional = false python-versions = ">=3.6" groups = ["dev"] markers = "sys_platform == \"win32\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" files = [ {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, ] [[package]] name = "readme-renderer" version = "43.0" description = "readme_renderer is a library for rendering readme descriptions for Warehouse" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "readme_renderer-43.0-py3-none-any.whl", hash = "sha256:19db308d86ecd60e5affa3b2a98f017af384678c63c88e5d4556a380e674f3f9"}, {file = "readme_renderer-43.0.tar.gz", hash = "sha256:1818dd28140813509eeed8d62687f7cd4f7bad90d4db586001c5dc09d4fde311"}, ] [package.dependencies] docutils = ">=0.13.1" nh3 = ">=0.2.14" Pygments = ">=2.5.1" [package.extras] md = ["cmarkgfm (>=0.8.0)"] [[package]] name = "requests" version = "2.32.4" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, ] [package.dependencies] certifi = ">=2017.4.17" charset_normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requests-toolbelt" version = "1.0.0" description = "A utility belt for advanced users of python-requests" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" groups = ["dev"] files = [ {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, ] [package.dependencies] requests = ">=2.0.1,<3.0.0" [[package]] name = "rfc3986" version = "2.0.0" description = "Validating URI References per RFC 3986" optional = false python-versions = ">=3.7" groups = ["dev"] files = [ {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"}, {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"}, ] [package.extras] idna2008 = ["idna"] [[package]] name = "rich" version = "14.0.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" groups = ["dev", "linting"] files = [ {file = "rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0"}, {file = "rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725"}, ] [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "rstcheck" version = "6.2.5" description = "Checks syntax of reStructuredText and code blocks nested within it" optional = false python-versions = ">=3.9" groups = ["linting"] files = [ {file = "rstcheck-6.2.5-py3-none-any.whl", hash = "sha256:09af9555cf05f23651189154066d483ced25d36ebb3f01dc3a5d27524e4a5fdc"}, {file = "rstcheck-6.2.5.tar.gz", hash = "sha256:122b6d6b953fa1a09d7e7de42ac5d8938da291c6f68351ace6166bb50fc3bd6c"}, ] [package.dependencies] rstcheck-core = ">=1.1" typer = ">=0.12.0" [package.extras] dev = ["rstcheck[docs,sphinx,testing,toml,type-check]", "tox (>=3.15)"] docs = ["myst-parser (>=3)", "sphinx (>=6.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx-click (>=4.0.3)", "sphinx-rtd-theme (>=1.2)", "sphinxcontrib-spelling (>=7.3)"] sphinx = ["sphinx (>=6.0)"] testing = ["coverage-conditional-plugin (>=0.5)", "coverage[toml] (>=6.0)", "pytest (>=7.2)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.0)", "pytest-sugar (>=0.9.5)"] toml = ["tomli (>=2.0) ; python_version <= \"3.10\""] type-check = ["mypy (>=1.0)"] [[package]] name = "rstcheck-core" version = "1.2.2" description = "Checks syntax of reStructuredText and code blocks nested within it" optional = false python-versions = ">=3.9" groups = ["linting"] files = [ {file = "rstcheck_core-1.2.2-py3-none-any.whl", hash = "sha256:2af6be0f91c8bed88f05ae9695331dc0f329ac379e98d469f4ff4536ef95e718"}, {file = "rstcheck_core-1.2.2.tar.gz", hash = "sha256:9e4842efcc32fe6dbe1767bf1f96225495369c82a2b5e0ed2969082f1ed56c9e"}, ] [package.dependencies] docutils = ">=0.7" pydantic = ">=2" [package.extras] dev = ["rstcheck-core[docs,sphinx,testing,toml,type-check,yaml]", "tox (>=3.15)"] docs = ["myst-parser (>=3)", "sphinx (>=6.0,!=7.2.5)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.15)", "sphinx-rtd-theme (>=1.2)", "sphinxcontrib-apidoc (>=0.3)", "sphinxcontrib-spelling (>=7.3)"] sphinx = ["sphinx (>=6.0)"] testing = ["coverage-conditional-plugin (>=0.5)", "coverage[toml] (>=6.0)", "pytest (>=7.2)", "pytest-cov (>=3.0)", "pytest-mock (>=3.7)", "pytest-randomly (>=3.0)", "pytest-sugar (>=0.9.5)"] toml = ["tomli (>=2.0) ; python_version <= \"3.10\""] type-check = ["mypy (>=1.0)", "types-PyYAML (>=6.0.0)", "types-docutils (>=0.18)"] yaml = ["pyyaml (>=6.0.0)"] [[package]] name = "ruff" version = "0.12.4" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["linting"] files = [ {file = "ruff-0.12.4-py3-none-linux_armv6l.whl", hash = "sha256:cb0d261dac457ab939aeb247e804125a5d521b21adf27e721895b0d3f83a0d0a"}, {file = "ruff-0.12.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:55c0f4ca9769408d9b9bac530c30d3e66490bd2beb2d3dae3e4128a1f05c7442"}, {file = "ruff-0.12.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a8224cc3722c9ad9044da7f89c4c1ec452aef2cfe3904365025dd2f51daeae0e"}, {file = "ruff-0.12.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9949d01d64fa3672449a51ddb5d7548b33e130240ad418884ee6efa7a229586"}, {file = "ruff-0.12.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:be0593c69df9ad1465e8a2d10e3defd111fdb62dcd5be23ae2c06da77e8fcffb"}, {file = "ruff-0.12.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7dea966bcb55d4ecc4cc3270bccb6f87a337326c9dcd3c07d5b97000dbff41c"}, {file = "ruff-0.12.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:afcfa3ab5ab5dd0e1c39bf286d829e042a15e966b3726eea79528e2e24d8371a"}, {file = "ruff-0.12.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c057ce464b1413c926cdb203a0f858cd52f3e73dcb3270a3318d1630f6395bb3"}, {file = "ruff-0.12.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64b90d1122dc2713330350626b10d60818930819623abbb56535c6466cce045"}, {file = "ruff-0.12.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2abc48f3d9667fdc74022380b5c745873499ff827393a636f7a59da1515e7c57"}, {file = "ruff-0.12.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2b2449dc0c138d877d629bea151bee8c0ae3b8e9c43f5fcaafcd0c0d0726b184"}, {file = "ruff-0.12.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:56e45bb11f625db55f9b70477062e6a1a04d53628eda7784dce6e0f55fd549eb"}, {file = "ruff-0.12.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:478fccdb82ca148a98a9ff43658944f7ab5ec41c3c49d77cd99d44da019371a1"}, {file = "ruff-0.12.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0fc426bec2e4e5f4c4f182b9d2ce6a75c85ba9bcdbe5c6f2a74fcb8df437df4b"}, {file = "ruff-0.12.4-py3-none-win32.whl", hash = "sha256:4de27977827893cdfb1211d42d84bc180fceb7b72471104671c59be37041cf93"}, {file = "ruff-0.12.4-py3-none-win_amd64.whl", hash = "sha256:fe0b9e9eb23736b453143d72d2ceca5db323963330d5b7859d60d101147d461a"}, {file = "ruff-0.12.4-py3-none-win_arm64.whl", hash = "sha256:0618ec4442a83ab545e5b71202a5c0ed7791e8471435b94e655b570a5031a98e"}, {file = "ruff-0.12.4.tar.gz", hash = "sha256:13efa16df6c6eeb7d0f091abae50f58e9522f3843edb40d56ad52a5a4a4b6873"}, ] [[package]] name = "secretstorage" version = "3.3.3" description = "Python bindings to FreeDesktop.org Secret Service API" optional = false python-versions = ">=3.6" groups = ["dev"] markers = "sys_platform == \"linux\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" files = [ {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, ] [package.dependencies] cryptography = ">=2.0" jeepney = ">=0.6" [[package]] name = "shellingham" version = "1.5.4" description = "Tool to Detect Surrounding Shell" optional = false python-versions = ">=3.7" groups = ["linting"] files = [ {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, ] [[package]] name = "snowballstemmer" version = "3.0.1" description = "This package provides 32 stemmers for 30 languages generated from Snowball algorithms." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*" groups = ["dev", "linting"] files = [ {file = "snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064"}, {file = "snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895"}, ] [[package]] name = "sphinx" version = "6.2.1" description = "Python documentation generator" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "Sphinx-6.2.1.tar.gz", hash = "sha256:6d56a34697bb749ffa0152feafc4b19836c755d90a7c59b72bc7dfd371b9cc6b"}, {file = "sphinx-6.2.1-py3-none-any.whl", hash = "sha256:97787ff1fa3256a3eef9eda523a63dbf299f7b47e053cfcf684a1c2a8380c912"}, ] [package.dependencies] alabaster = ">=0.7,<0.8" babel = ">=2.9" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} docutils = ">=0.18.1,<0.20" imagesize = ">=1.3" importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} Jinja2 = ">=3.0" packaging = ">=21.0" Pygments = ">=2.13" requests = ">=2.25.0" snowballstemmer = ">=2.0" sphinxcontrib-applehelp = "*" sphinxcontrib-devhelp = "*" sphinxcontrib-htmlhelp = ">=2.0.0" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" sphinxcontrib-serializinghtml = ">=1.1.5" [package.extras] docs = ["sphinxcontrib-websupport"] lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"] test = ["cython", "filelock", "html5lib", "pytest (>=4.6)"] [[package]] name = "sphinxcontrib-applehelp" version = "2.0.0" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, ] [package.extras] lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-devhelp" version = "2.0.0" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, ] [package.extras] lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-htmlhelp" version = "2.1.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, ] [package.extras] lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["html5lib", "pytest"] [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" optional = false python-versions = ">=3.5" groups = ["dev"] files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, ] [package.extras] test = ["flake8", "mypy", "pytest"] [[package]] name = "sphinxcontrib-qthelp" version = "2.0.0" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, ] [package.extras] lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["defusedxml (>=0.7.1)", "pytest"] [[package]] name = "sphinxcontrib-serializinghtml" version = "2.0.0" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, ] [package.extras] lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "tomli" version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["main", "dev", "linting", "testing"] files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] markers = {main = "extra == \"tomli\" and python_version < \"3.11\"", dev = "python_version < \"3.11\"", linting = "python_version < \"3.11\"", testing = "python_version < \"3.11\""} [[package]] name = "tomlkit" version = "0.13.3" description = "Style preserving TOML library" optional = false python-versions = ">=3.8" groups = ["linting"] files = [ {file = "tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0"}, {file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"}, ] [[package]] name = "tox" version = "4.28.1" description = "tox is a generic virtualenv management and test command line tool" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "tox-4.28.1-py3-none-any.whl", hash = "sha256:d5c84de6efc5d7e8acadb09528943e87ee501a35e064cf852082bc600485c13d"}, {file = "tox-4.28.1.tar.gz", hash = "sha256:227ce1fdfea7763107aed3a8ac87d74b1bd1240ad7dd9c37fc2cb2b318006520"}, ] [package.dependencies] cachetools = ">=6.1" chardet = ">=5.2" colorama = ">=0.4.6" filelock = ">=3.18" packaging = ">=25" platformdirs = ">=4.3.8" pluggy = ">=1.6" pyproject-api = ">=1.9.1" tomli = {version = ">=2.2.1", markers = "python_version < \"3.11\""} typing-extensions = {version = ">=4.14.1", markers = "python_version < \"3.11\""} virtualenv = ">=20.31.2" [[package]] name = "twine" version = "6.1.0" description = "Collection of utilities for publishing packages on PyPI" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "twine-6.1.0-py3-none-any.whl", hash = "sha256:a47f973caf122930bf0fbbf17f80b83bc1602c9ce393c7845f289a3001dc5384"}, {file = "twine-6.1.0.tar.gz", hash = "sha256:be324f6272eff91d07ee93f251edf232fc647935dd585ac003539b42404a8dbd"}, ] [package.dependencies] id = "*" importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} keyring = {version = ">=15.1", markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\""} packaging = ">=24.0" readme-renderer = ">=35.0" requests = ">=2.20" requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" rfc3986 = ">=1.4.0" rich = ">=12.0.0" urllib3 = ">=1.26.0" [package.extras] keyring = ["keyring (>=15.1)"] [[package]] name = "typer" version = "0.16.0" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false python-versions = ">=3.7" groups = ["linting"] files = [ {file = "typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855"}, {file = "typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b"}, ] [package.dependencies] click = ">=8.0.0" rich = ">=10.11.0" shellingham = ">=1.3.0" typing-extensions = ">=3.7.4.3" [[package]] name = "typing-extensions" version = "4.14.1" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" groups = ["dev", "linting", "testing"] files = [ {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, ] markers = {dev = "python_version < \"3.11\"", testing = "python_version < \"3.11\""} [[package]] name = "typing-inspection" version = "0.4.1" description = "Runtime typing introspection tools" optional = false python-versions = ">=3.9" groups = ["linting"] files = [ {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, ] [package.dependencies] typing-extensions = ">=4.12.0" [[package]] name = "urllib3" version = "2.6.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, ] [package.extras] brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [[package]] name = "virtualenv" version = "20.36.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f"}, {file = "virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba"}, ] [package.dependencies] distlib = ">=0.3.7,<1" filelock = [ {version = ">=3.16.1,<4", markers = "python_version < \"3.10\""}, {version = ">=3.20.1,<4", markers = "python_version >= \"3.10\""}, ] platformdirs = ">=3.9.1,<5" typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] [[package]] name = "zipp" version = "3.23.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.9" groups = ["dev"] markers = "python_version <= \"3.11\" and platform_machine != \"ppc64le\" and platform_machine != \"s390x\" or python_version == \"3.9\"" files = [ {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, ] [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy"] [extras] tomli = ["tomli"] [metadata] lock-version = "2.1" python-versions = "^3.9" content-hash = "7353b2b2903dd42cd20a4edad8d7b2519f548e171d413f8c90926c4dfb6b5738" docformatter-1.7.8/pyproject.toml000066400000000000000000000114611517155121300171400ustar00rootroot00000000000000[tool.poetry] name = "docformatter" version = "1.7.8" description = "Formats docstrings to follow PEP 257" authors = ["Steven Myint"] maintainers = [ "Doyle Rowland ", ] license = "Expat" readme = "README.rst" homepage = "https://github.com/PyCQA/docformatter" repository = "https://github.com/PyCQA/docformatter" documentation = "https://docformatter.readthedocs.io/en/latest/" keywords = [ "PEP 257", "pep257", "style", "formatter", "docstrings", ] classifiers=[ 'Intended Audience :: Developers', 'Environment :: Console', 'Programming Language :: Python :: 3', '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', 'Programming Language :: Python :: Implementation', 'Programming Language :: Python :: Implementation :: PyPy', 'Programming Language :: Python :: Implementation :: CPython', 'License :: OSI Approved :: MIT License', ] packages = [{include = "docformatter", from = "src"}] [tool.poetry.dependencies] python = "^3.10" charset_normalizer = "^3.0.0" tomli = {version = "^2.0.0", python = "<3.11", optional = true} [tool.poetry.group.dev.dependencies] Sphinx = "^6.0.0" tox = "^4.0.0" twine = "^6.1.0" [tool.poetry.group.testing.dependencies] coverage = {extras = ["toml"], version = "^7.5.0"} mock = "^5.2.0" pytest = "^8.4.0" pytest-cov = "^6.2.0" pytest-order = "^1.3.0" [tool.poetry.group.linting.dependencies] autopep8 = "^2.0.0" black = ">=25" isort = "^6.0.0" mypy = "^1.17.0" pycodestyle = "^2.8.0" pydocstyle = "^6.1.1" pylint = "^3.3.0" rstcheck = "^6.1.0" ruff = "^0.12.0" [tool.poetry.extras] tomli = ["tomli"] [tool.poetry.scripts] docformatter = "docformatter.__main__:main" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.pylint.master] ignore-paths = [ "tests*", ] [tool.pylint.messages_control] disable = [ "fixme", "import-outside-toplevel", "inconsistent-return-statements", "invalid-name", "no-else-return", "no-member", "too-few-public-methods", "too-many-arguments", "too-many-boolean-expressions", "too-many-locals", "too-many-return-statements", "useless-object-inheritance", ] [tool.docformatter] black = true non-strict = false non-cap = [ "docformatter", ] [tool.mypy] allow_subclassing_any = true follow_imports = "skip" implicit_reexport = true ignore_missing_imports = true [tool.pydocstyle] convention = "pep257" [tool.pytest.ini_options] markers = [ "unit: mark the test as a unit test.", "integration: mark the test as an integration test.", "system: mark the test as a system test.", ] [tool.coverage.run] branch = true cover_pylib = false omit = [ '*/site-packages/*', '*/*pypy/*', '*/tests/*', '__init__.py', 'setup.py', ] relative_files = true [tool.coverage.report] omit = [ '*/site-packages/*', '*/*pypy/*', '*/tests/*', '__init__.py', 'setup.py', ] exclude_lines = [ 'pragma: no cover', 'import', ] show_missing = true [tool.coverage.xml] output = 'coverage.xml' [tool.black] line-length = 88 target-version = [ 'py310', 'py311', 'py312', 'py313', 'py314', ] exclude = ''' /( \.eggs | \.git | \.hg | \.mypy_cache | \.tox | \.venv | _build | buck-out | build | dist )/ ''' [tool.isort] known_first_party = 'docformatter' known_third_party = ['toml'] import_heading_firstparty = 'docformatter Package Imports' import_heading_localfolder = 'docformatter Local Imports' import_heading_stdlib = 'Standard Library Imports' import_heading_thirdparty = 'Third Party Imports' multi_line_output = 3 include_trailing_comma = true force_grid_wrap = 0 use_parentheses = true ensure_newline_before_comments = true line_length = 88 [tool.rstcheck] report = "warning" ignore_directives = [ "automodule", "literalinclude", "tabularcolumns", "toctree", ] ignore_messages = [ "Possible title underline" ] ignore_roles = [ "numref", "ref", ] [tool.ruff] exclude = [ ".bzr", ".direnv", ".eggs", ".git", ".git-rewrite", ".hg", ".ipynb_checkpoints", ".mypy_cache", ".nox", ".pants.d", ".pyenv", ".pytest_cache", ".pytype", ".ruff_cache", ".svn", ".tox", ".venv", ".vscode", "__pypackages__", "_build", "buck-out", "build", "dist", "node_modules", "site-packages", "venv", "tests/", ] line-length = 88 indent-width = 4 target-version = "py310" [tool.ruff.lint] select = ["E", "F", "PL"] ignore = [] [tool.ruff.format] quote-style = "double" indent-style = "space" skip-magic-trailing-comma = false line-ending = "auto" docformatter-1.7.8/src/000077500000000000000000000000001517155121300150105ustar00rootroot00000000000000docformatter-1.7.8/src/docformatter/000077500000000000000000000000001517155121300175015ustar00rootroot00000000000000docformatter-1.7.8/src/docformatter/__init__.py000066400000000000000000000034501517155121300216140ustar00rootroot00000000000000#!/usr/bin/env python # # docformatter.__init__.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """This is the docformatter package.""" __all__ = ["__version__"] # docformatter Local Imports from .__pkginfo__ import __version__ from .classify import * # noqa F403 from .format import FormatResult # noqa F403 from .format import Formatter # noqa F401 from .patterns import * # noqa F403 from .strings import * # noqa F403 from .util import * # noqa F403 from .wrappers import * # noqa F403 # Have isort skip these they require the functions above. from .configuration import Configurater # isort: skip # noqa F401 from .encode import Encoder # isort: skip # noqa F401 docformatter-1.7.8/src/docformatter/__main__.py000077500000000000000000000146431517155121300216060ustar00rootroot00000000000000#!/usr/bin/env python # # docformatter.__main__.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Formats docstrings to follow PEP 257.""" # Standard Library Imports import contextlib import signal import sys # docformatter Package Imports import docformatter.configuration as _configuration import docformatter.format as _format def _help(): """Print docformatter's help.""" print("""\ usage: docformatter [-h] [-i | -c] [-d] [-r] [-e [EXCLUDE ...]] [-n [NON-CAP ...]] [-s [style]] [--rest-section-adorns REGEX] [--black] [--wrap-summaries length] [--wrap-descriptions length] [--force-wrap] [--tab-width width] [--blank] [--pre-summary-newline] [--pre-summary-space] [--make-summary-multi-line] [--close-quotes-on-newline] [--range line line] [--docstring-length length length] [--non-strict] [--config CONFIG] [--version] files [files ...] positional arguments: files files to format or '-' for standard in options: -h, --help show this help message and exit -i, --in-place make changes to files instead of printing diffs -c, --check only check and report incorrectly formatted files -d, --diff when used with `--check` or `--in-place`, also what changes would be made -r, --recursive drill down directories recursively -e [EXCLUDE ...], --exclude [EXCLUDE ...] in recursive mode, exclude directories and files by names -n [NON-CAP ...], --non-cap [NON-CAP ...] list of words not to capitalize when they appear as the first word in the summary -s style, --style style the docstring style to use when formatting parameter lists. One of epytext, sphinx. (default: sphinx) --rest-section-adorns REGEX regular expression for identifying reST section adornments (default: [!\"#$%&'()*+,-./\\:;<=>?@[]^_`{|}~]{4,}) --black make formatting compatible with standard black options (default: False) --wrap-summaries length wrap long summary lines at this length; set to 0 to disable wrapping (default: 79, 88 with --black option) --wrap-descriptions length wrap descriptions at this length; set to 0 to disable wrapping (default: 72, 88 with --black option) --force-wrap force descriptions to be wrapped even if it may result in a mess (default: False) --tab-width width tabs in indentation are this many characters when wrapping lines (default: 1) --blank add blank line after description (default: False) --pre-summary-newline add a newline before the summary of a multi-line docstring (default: False) --pre-summary-space add a space after the opening triple quotes (default: False) --make-summary-multi-line add a newline before and after the summary of a one-line docstring (default: False) --close-quotes-on-newline place closing triple quotes on a new-line when a one-line docstring wraps to two or more lines (default: False) --range line line apply docformatter to docstrings between these lines; line numbers are indexed at 1 (default: None) --docstring-length length length apply docformatter to docstrings of given length range (default: None) --non-strict don't strictly follow reST syntax to identify lists (see issue #67) (default: False) --config CONFIG path to file containing docformatter options --version show program's version number and exit """) def _main(argv, standard_out, standard_error, standard_in): """Run internal main entry point.""" configurator = _configuration.Configurater(argv) if "--help" in configurator.args_lst or "-h" in configurator.args_lst: _help() return 0 else: configurator.do_parse_arguments() formator = _format.Formatter( configurator.args, stderror=standard_error, stdin=standard_in, stdout=standard_out, ) if "-" in configurator.args.files: formator.do_format_standard_in( configurator.parser, ) else: return formator.do_format_files() def main(): """Run the main entry point.""" # SIGPIPE is not available on Windows. with contextlib.suppress(AttributeError): # Exit on broken pipe. signal.signal(signal.SIGPIPE, signal.SIG_DFL) try: return _main( sys.argv, standard_out=sys.stdout, standard_error=sys.stderr, standard_in=sys.stdin, ) except KeyboardInterrupt: # pragma: no cover return _format.FormatResult.interrupted # pragma: no cover if __name__ == "__main__": sys.exit(main()) docformatter-1.7.8/src/docformatter/__pkginfo__.py000066400000000000000000000024571517155121300223140ustar00rootroot00000000000000#!/usr/bin/env python # # docformatter.patterns.__pkginfo__.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowlans # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Package information for docformatter.""" __version__ = "1.7.8" docformatter-1.7.8/src/docformatter/classify.py000066400000000000000000000337451517155121300217040ustar00rootroot00000000000000#!/usr/bin/env python # # docformatter.classify.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """This module provides docformatter's classification functions.""" # Standard Library Imports import re import sys import tokenize from tokenize import TokenInfo from typing import Union # docformatter Package Imports from docformatter.constants import MAX_PYTHON_VERSION PY312 = (sys.version_info[0], sys.version_info[1]) > MAX_PYTHON_VERSION def do_find_docstring_blocks(tokens: list[TokenInfo]) -> list[tuple[int, int, str]]: """Identify all docstring blocks and their anchor points. Parameters ---------- tokens (list[TokenInfo]): A list of tokenized Python source code. Returns ------- list[tuple[int, int, str]]: A list of tuples representing each docstring block. Each tuple contains: - anchor_index (int): Index of the anchor (class, def, async def, or assignment). - string_index (int): Index of the docstring token. - docstring_type (str): One of "module", "class", "function", or "attribute". """ docstring_blocks = [] for i, token in enumerate(tokens): if ( token.type != tokenize.STRING or not ( token.string.startswith('"""') or token.string.startswith('r"""') or token.string.startswith('R"""') or token.string.startswith('u"""') or token.string.startswith('U"""') or token.string.startswith("'''") or token.string.startswith("r'''") or token.string.startswith("R'''") or token.string.startswith("u'''") or token.string.startswith("U'''") ) or " = " in token.line ): continue if is_module_docstring(tokens, i): docstring_blocks.append((0, i, "module")) continue if is_attribute_docstring(tokens, i): anchor_idx = _do_find_anchor_index(tokens, i, target="attribute") if anchor_idx is not None: docstring_blocks.append((anchor_idx, i, "attribute")) continue if is_class_docstring(tokens, i): anchor_idx = _do_find_anchor_index(tokens, i, target="class") if anchor_idx is not None: docstring_blocks.append((anchor_idx, i, "class")) continue if is_function_or_method_docstring(tokens, i): anchor_idx = _do_find_anchor_index(tokens, i, target="def") if anchor_idx is not None: docstring_blocks.append((anchor_idx, i, "function")) continue # If adjacent docstrings have the same anchor index, remove the second one as # there can only be one docstring per anchor. i = 1 while i < len(docstring_blocks): if docstring_blocks[i][0] == docstring_blocks[i - 1][0]: docstring_blocks.pop(i) i += 1 return docstring_blocks def _do_find_anchor_index( tokens: list[TokenInfo], docstring_index: int, target: str, ) -> Union[int, None]: """Walk backward from a docstring to find the matching anchor. The matching anchor would be one of `class`, `def`, `async def`, or an assignment. Parameters ---------- tokens (list[TokenInfo]): A list of tokenized Python source code. docstring_index (int): Index of the STRING token representing the docstring. target (str): One of "class", "def", or "attribute" indicating what to search for. Returns ------- int | None: Index of the anchor token if found, otherwise None. """ i = docstring_index - 1 saw_decorator = False while i >= 0: tok = tokens[i] if tok.type == tokenize.OP and tok.string == "@": saw_decorator = True if target == "class" and tok.type == tokenize.NAME and tok.string == "class": return i if target == "def" and tok.type == tokenize.NAME and tok.string == "def": # Handle @decorator above def if saw_decorator: while i > 0 and tokens[i - 1].type != tokenize.NEWLINE: i -= 1 return i if target == "attribute": if tok.type == tokenize.NAME: return i i -= 1 return None def is_attribute_docstring( tokens: list[tokenize.TokenInfo], index: int, ) -> bool: """Return True if the string token is an attribute docstring. Parameters ---------- tokens : list[TokenInfo] A list of tokenized Python source code. index : int Index of the anchor token. Returns ------- True if attribute docstring, False otherwise. """ if index < 2: # noqa: PLR2004 return False # Step 1: Find the previous NEWLINE before the docstring k = index - 1 while k > 0 and tokens[k].type != tokenize.NEWLINE: k -= 1 # Step 2: Check for '=' or ':' on the line *before* the docstring seen_equal_or_colon = False for tok in tokens[0:index]: if tok.type == tokenize.OP and tok.string == "=" and '"""' not in tok.line: seen_equal_or_colon = True break else: seen_equal_or_colon = False if not seen_equal_or_colon: return False return True def is_class_docstring( tokens: list[tokenize.TokenInfo], index: int, ) -> bool: """Determine if docstring is a class docstring.""" # Walk backward to find the most recent `class` keyword before the string, # without crossing over a `def`, `async`, or another block for i in range(index - 1, -1, -1): tok = tokens[i] if tok.type == tokenize.NAME and tok.string == "class": return True if tok.type == tokenize.NAME and tok.string in ("def", "async"): return False # Hit enclosing function or method first. if tok.type == tokenize.OP and tok.string == "=": return False # Hit assignment, not a class docstring. return False def is_closing_quotes( token: tokenize.TokenInfo, prev_token: tokenize.TokenInfo ) -> bool: """Determine if token is a closing quote for a docstring. Parameters ---------- token : tokenize.TokenInfo The token to check. prev_token : tokenize.TokenInfo The previous token in the stream. Returns ------- bool True if the token is a closing quote for a docstring, False otherwise. """ _offset = prev_token.line.split("\n")[-1] if prev_token.line.endswith("\n"): _offset = prev_token.line.split("\n")[-2] if ( token.line.strip() == '"""' and token.type == tokenize.NEWLINE or token.line == _offset ): return True return False def is_code_line(token: tokenize.TokenInfo) -> bool: """Determine if token is a line of code. Parameters ---------- token : tokenize.TokenInfo The token to check. Returns ------- bool True if the token is a code line, False otherwise. """ if (token.type == tokenize.NAME or token.string == "...") and not ( token.line.strip().startswith("def ") or token.line.strip().startswith("async ") or token.line.strip().startswith("class ") ): return True return False def is_definition_line(token: tokenize.TokenInfo) -> bool: """Determine if token is a class or function/method definition line. Parameters ---------- token : tokenize.TokenInfo The token to check. Returns ------- bool True if the token is a definition line, False otherwise. """ if token.type == tokenize.NAME and ( token.line.startswith("def ") or token.line.startswith("async ") or token.line.startswith("class ") ): return True return False def is_f_string(token: tokenize.TokenInfo, prev_token: tokenize.TokenInfo) -> bool: """Determine if token is an f-string. Parameters ---------- token : tokenize.TokenInfo The token to check. prev_token : tokenize.TokenInfo The previous token in the stream. Returns ------- bool True if the token is an f-string, False otherwise. """ if PY312: if tokenize.FSTRING_MIDDLE in [token.type, prev_token.type]: return True elif any( [ token.string.startswith('f"""'), prev_token.string.startswith('f"""'), token.string.startswith("f'''"), prev_token.string.startswith("f'''"), ] ): return True return False def is_function_or_method_docstring( tokens: list[tokenize.TokenInfo], index: int, ) -> bool: """Determine if docstring is a function or method docstring.""" for i in range(index - 1, -1, -1): tok = tokens[i] if tok.type == tokenize.NAME and tok.string in ("def", "async"): return True if tok.type == tokenize.NAME and tok.string == "class": return False # hit enclosing class first return False def is_inline_comment(token: tokenize.TokenInfo) -> bool: """Determine if token is an inline comment. Parameters ---------- token : tokenize.TokenInfo The token to check. Returns ------- bool True if the token is an inline comment, False otherwise. """ if token.line.strip().startswith('"""') and token.string.startswith("#"): return True return False def is_line_following_indent( token: tokenize.TokenInfo, prev_token: tokenize.TokenInfo, ) -> bool: """Determine if token is a line that follows an indent. Parameters ---------- token : tokenize.TokenInfo The token to check. prev_token : tokenize.TokenInfo The previous token in the stream. Returns ------- bool True if the token is a line that follows an indent, False otherwise. """ if prev_token.type == tokenize.INDENT and prev_token.line in token.line: return True return False def is_module_docstring( tokens: list[tokenize.TokenInfo], index: int, ) -> bool: """Determine if docstring is a module docstring.""" # No code tokens before the string for k in range(index): if tokens[k][0] not in ( tokenize.ENCODING, tokenize.COMMENT, tokenize.NEWLINE, tokenize.NL, ): return False return True def is_nested_definition_line(token: tokenize.TokenInfo) -> bool: """Determine if token is a nested class or function/method definition line. Parameters ---------- token : tokenize.TokenInfo The token to check. Returns ------- bool True if the token is a nested definition line, False otherwise. """ return re.match(r"^ {4,}(async|class|def) ", token.line) is not None def is_newline_continuation( token: tokenize.TokenInfo, prev_token: tokenize.TokenInfo, ) -> bool: """Determine if token is a continuation of a previous line. Parameters ---------- token : tokenize.TokenInfo The token to check. prev_token : tokenize.TokenInfo The previous token in the stream. Returns ------- bool True if the token is a continuation of a previous line, False otherwise. """ if ( token.type in (tokenize.NEWLINE, tokenize.NL) and token.line.strip() in prev_token.line.strip() and token.line not in {"\n", "\r\n"} ): return True return False def is_string_variable( token: tokenize.TokenInfo, prev_token: tokenize.TokenInfo, ) -> bool: """Determine if token is a string variable assignment. Parameters ---------- token : tokenize.TokenInfo The token to check. prev_token : tokenize.TokenInfo The previous token in the stream. Returns ------- bool True if the token is a string variable assignment, False otherwise. """ # TODO: The AWAIT token is removed in Python 3.13 and later. Only Python 3.9 # seems to generate the AWAIT token, so we can safely remove the check for it when # support for Python 3.9 is dropped in April 2026. if sys.version_info <= (3, 12): _token_types = (tokenize.AWAIT, tokenize.OP) else: _token_types = (tokenize.OP,) if prev_token.type in _token_types and ( '= """' in token.line or token.line in prev_token.line ): return True return False def is_docstring_at_end_of_file(tokens: list[tokenize.TokenInfo], index: int) -> bool: """Determine if the docstring is at the end of the file.""" for i in range(index + 1, len(tokens)): tok = tokens[i] if tok.type not in ( tokenize.NL, tokenize.NEWLINE, tokenize.DEDENT, tokenize.ENDMARKER, ): return False return True docformatter-1.7.8/src/docformatter/configuration.py000066400000000000000000000324361517155121300227320ustar00rootroot00000000000000#!/usr/bin/env python # # docformatter.configuration.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """This module provides docformatter's Configurater class.""" # Standard Library Imports import argparse import contextlib import os import sys from configparser import ConfigParser from typing import Dict, Sequence, Union with contextlib.suppress(ImportError): if sys.version_info >= (3, 11): # Standard Library Imports import tomllib else: # Third Party Imports import tomli as tomllib # docformatter Package Imports from docformatter import __pkginfo__ class Configurater: """Read and store all the docformatter configuration information.""" parser: argparse.ArgumentParser = argparse.ArgumentParser() """Parser object.""" flargs: Dict[str, Union[bool, float, int, str]] = {} """Dictionary of configuration file arguments.""" configuration_file_lst = [ "pyproject.toml", "setup.cfg", "tox.ini", ] """List of supported configuration files.""" args: argparse.Namespace = argparse.Namespace() def __init__(self, args: Union[Sequence[str], None]) -> None: """Initialize a Configurater class instance. Parameters ---------- args : list Any command line arguments passed during invocation. """ self.args_lst: Union[Sequence[str], None] = args self.config_file = "" self.parser = argparse.ArgumentParser( description=__doc__, prog="docformatter", ) try: if self.args_lst is not None: self.config_file = str( self.args_lst[self.args_lst.index("--config") + 1] ) except ValueError: for _configuration_file in self.configuration_file_lst: if os.path.isfile(_configuration_file): self.config_file = f"./{_configuration_file}" break if os.path.isfile(self.config_file): self._do_read_configuration_file() def do_parse_arguments(self) -> None: """Parse configuration file and command line arguments.""" changes = self.parser.add_mutually_exclusive_group() changes.add_argument( "-i", "--in-place", action="store_true", default=str(self.flargs.get("in-place", "false")).lower() == "true", help="make changes to files instead of printing diffs", ) changes.add_argument( "-c", "--check", action="store_true", default=str(self.flargs.get("check", "false")).lower() == "true", help="only check and report incorrectly formatted files", ) self.parser.add_argument( "-d", "--diff", action="store_true", default=str(self.flargs.get("diff", "false")).lower() == "true", help="when used with `--check` or `--in-place`, also what changes " "would be made", ) self.parser.add_argument( "-r", "--recursive", action="store_true", default=str(self.flargs.get("recursive", "false")).lower() == "true", help="drill down directories recursively", ) self.parser.add_argument( "-e", "--exclude", nargs="*", default=self.flargs.get("exclude", None), help="in recursive mode, exclude directories and files by names", ) self.parser.add_argument( "-n", "--non-cap", action="store", nargs="*", default=self.flargs.get("non-cap", None), help="list of words not to capitalize when they appear as the first word " "in the summary", ) self.parser.add_argument( "--black", action="store_true", default=str(self.flargs.get("black", "false")).lower() == "true", help="make formatting compatible with standard black options " "(default: False)", ) if self.args_lst is not None: self.args = self.parser.parse_known_args(self.args_lst[1:])[0] # Default black line length is 88, so use this when not specified # otherwise use PEP-8 defaults if self.args.black: _default_wrap_summaries = 88 _default_wrap_descriptions = 88 _default_pre_summary_space = "true" else: _default_wrap_summaries = 79 _default_wrap_descriptions = 72 _default_pre_summary_space = "false" self.parser.add_argument( "-s", "--style", default=self.flargs.get("style", "sphinx"), help="name of the docstring style to use when formatting " "parameter lists (default: sphinx)", ) self.parser.add_argument( "--rest-section-adorns", type=str, dest="rest_section_adorns", default=self.flargs.get( "rest_section_adorns", r"[!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~]{4,}" ), help="regex for identifying reST section header adornments", ) self.parser.add_argument( "--wrap-summaries", default=int(self.flargs.get("wrap-summaries", _default_wrap_summaries)), type=int, metavar="length", help="wrap long summary lines at this length; " "set to 0 to disable wrapping (default: 79, 88 with --black " "option)", ) self.parser.add_argument( "--wrap-descriptions", default=int( self.flargs.get("wrap-descriptions", _default_wrap_descriptions) ), type=int, metavar="length", help="wrap descriptions at this length; " "set to 0 to disable wrapping (default: 72, 88 with --black " "option)", ) self.parser.add_argument( "--force-wrap", action="store_true", default=str(self.flargs.get("force-wrap", "false")).lower() == "true", help="force descriptions to be wrapped even if it may " "result in a mess (default: False)", ) self.parser.add_argument( "--tab-width", type=int, dest="tab_width", metavar="width", default=int(self.flargs.get("tab-width", 1)), help="tabs in indentation are this many characters when " "wrapping lines (default: 1)", ) self.parser.add_argument( "--blank", dest="post_description_blank", action="store_true", default=str(self.flargs.get("blank", "false")).lower() == "true", help="add blank line after description (default: False)", ) self.parser.add_argument( "--pre-summary-newline", action="store_true", default=str(self.flargs.get("pre-summary-newline", "false")).lower() == "true", help="add a newline before the summary of a multi-line docstring " "(default: False)", ) self.parser.add_argument( "--pre-summary-space", action="store_true", default=str( self.flargs.get("pre-summary-space", _default_pre_summary_space) ).lower() == "true", help="add a space after the opening triple quotes (default: False)", ) self.parser.add_argument( "--make-summary-multi-line", action="store_true", default=str(self.flargs.get("make-summary-multi-line", "false")).lower() == "true", help="add a newline before and after the summary of a one-line " "docstring (default: False)", ) self.parser.add_argument( "--close-quotes-on-newline", action="store_true", default=str(self.flargs.get("close-quotes-on-newline", "false")).lower() == "true", help="place closing triple quotes on a new-line when a " "one-line docstring wraps to two or more lines " "(default: False)", ) self.parser.add_argument( "--range", metavar="line", dest="line_range", default=self.flargs.get("range", None), type=int, nargs=2, help="apply docformatter to docstrings between these " "lines; line numbers are indexed at 1 (default: None)", ) self.parser.add_argument( "--docstring-length", metavar="length", dest="length_range", default=self.flargs.get("docstring-length", None), type=int, nargs=2, help="apply docformatter to docstrings of given length range " "(default: None)", ) self.parser.add_argument( "--non-strict", action="store_true", default=str(self.flargs.get("non-strict", "false")).lower() == "true", help="don't strictly follow reST syntax to identify lists (see " "issue #67) (default: False)", ) self.parser.add_argument( "--config", default=self.config_file, help="path to file containing docformatter options", ) self.parser.add_argument( "--version", action="version", version=f"%(prog)s {__pkginfo__.__version__}", ) self.parser.add_argument( "files", nargs="+", help="files to format or '-' for standard in", ) if self.args_lst is not None: self.args = self.parser.parse_args(self.args_lst[1:]) if self.args.line_range: if self.args.line_range[0] <= 0: self.parser.error("--range must be positive numbers") if self.args.line_range[0] > self.args.line_range[1]: self.parser.error( "First value of --range should be less than or equal " "to the second" ) if self.args.length_range: if self.args.length_range[0] <= 0: self.parser.error("--docstring-length must be positive numbers") if self.args.length_range[0] > self.args.length_range[1]: self.parser.error( "First value of --docstring-length should be less " "than or equal to the second" ) def _do_read_configuration_file(self) -> None: """Read docformatter options from a configuration file.""" argfile = os.path.basename(self.config_file) for f in self.configuration_file_lst: if argfile == f: break fullpath, ext = os.path.splitext(self.config_file) filename = os.path.basename(fullpath) if ext == ".toml" and filename == "pyproject": self._do_read_toml_configuration() if (ext == ".cfg" and filename == "setup") or ( ext == ".ini" and filename == "tox" ): self._do_read_parser_configuration() def _do_read_toml_configuration(self) -> None: """Load configuration information from a *.toml file.""" with open(self.config_file, "rb") as f: config = tomllib.load(f) result = config.get("tool", {}).get("docformatter", None) if result is not None: self.flargs = { k: v if isinstance(v, list) else str(v) for k, v in result.items() # type: ignore } def _do_read_parser_configuration(self) -> None: """Load configuration information from a *.cfg or *.ini file.""" config = ConfigParser() config.read(self.config_file) for _section in [ "tool.docformatter", "tool:docformatter", "docformatter", ]: if _section in config.sections(): self.flargs = { k: v if isinstance(v, list) else str(v) for k, v in config[_section].items() } docformatter-1.7.8/src/docformatter/constants.py000066400000000000000000000154741517155121300221020ustar00rootroot00000000000000#!/usr/bin/env python # # docformatter.constants.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """This module provides docformatter's constants.""" # TODO: Move these constants to the configuration file and/or command line. ABBREVIATIONS = ( "e.g.", "i.e.", "et. al.", "etc.", "Dr.", "Mr.", "Mrs.", "Ms.", ) ALEMBIC_REGEX = r"^(Revision ID|Revises|Create Date): {0,}" """Regular expression to use for finding alembic headers.""" BULLET_REGEX = r"\s*[*\-+] [\S ]+" """Regular expression to use for finding bullet lists.""" CODE_PATTERN_REGEX = ( r"^ {0,}(assert|async|await|break|class|continue|def|del|do|elif|else|except|" r"finally|for|global|if|import|lambda|pass|print|raise|return|super|try|while|" r"with|yield)" ) """Regular expression to use for finding code patterns.""" ENUM_REGEX = r"\s*\d\." """Regular expression to use for finding enumerated lists.""" EPYTEXT_REGEX = r"@[a-zA-Z0-9_\-\s]+:" """Regular expression to use for finding Epytext-style field lists.""" GOOGLE_REGEX = r"^ *[a-zA-Z0-9_\- ]*:$" """Regular expression to use for finding Google-style field lists.""" LITERAL_REGEX = r"[\S ]*::" """Regular expression to use for finding literal blocks.""" NUMPY_REGEX = r"^\s[a-zA-Z0-9_\- ]+ ?: [\S ]+" """Regular expression to use for finding Numpy-style field lists.""" NUMPY_SECTION_REGEX = ( r"^ *?(Parameters|Other Parameters|Returns|Raises|See " r"Also|Notes|Examples|References|Yields|Warns|Warnings|Receives)\n[- ]+" ) """Regular expression to use for finding Numpy section headers.""" OPTION_REGEX = r"^ {0,}-{1,2}[\S ]+ \w+" """Regular expression to use for finding option lists.""" REST_DIRECTIVE_REGEX = r"^( {0,}\.\. .+?:{1,2}.*\n(?:[ \t]{1,}.*\n|\n)*)" """Regular expression to use for finding reST directives.""" REST_INLINE_REGEX = r"(?:.-]+([*]{1,2}|[`]{1,2}_?|[|]|[\]]_?)" # noqa: E501 """Regular expression to use for finding inline reST markup.""" REST_SECTION_REGEX = ( r"(^ *[#\*=\-^\'\"\+_\~`\.\:]+\n)?[\w ]+\n *[#\*=\-^\'\"\+_\~`\.\:]+" ) """Regular expression to use for finding reST section headers.""" # Complete list: # https://www.sphinx-doc.org/en/master/usage/domains/python.html#info-field-lists SPHINX_FIELD_PATTERNS = ( "arg|" "cvar|" "except|" "ivar|" "key|" "meta|" "param|" "raise|" "return|" "rtype|" "type|" "var|" "yield" ) SPHINX_REGEX = rf":({SPHINX_FIELD_PATTERNS})[a-zA-Z0-9_\-.() ]*:" """Regular expression to use for finding Sphinx-style field lists.""" URL_PATTERNS = ( "afp|" "apt|" "bitcoin|" "chrome|" "cvs|" "dav|" "dns|" "file|" "finger|" "fish|" "ftp|" "ftps|" "git|" "http|" "https|" "imap|" "ipp|" "ipps|" "irc|" "irc6|" "ircs|" "jar|" "ldap|" "ldaps|" "mailto|" "news|" "nfs|" "nntp|" "pop|" "rsync|" "s3|" "sftp|" "shttp|" "sip|" "sips|" "smb|" "sms|" "snmp|" "ssh|" "svn|" "telnet|" "vnc|" "xmpp|" "xri" ) """The URL patterns to look for when finding links. Based on the table at """ # This is the regex used to find URL links: # # (__ |`{{2}}|`\w[\w. :\n]*|\.\. _?[\w. :]+|')? is used to find in-line links that # should remain on a single line even if it exceeds the wrap length. # __ is used to find to underscores followed by a single space. # This finds patterns like: __ https://sw.kovidgoyal.net/kitty/graphics-protocol/ # # `{{2}} is used to find two back-tick characters. # This finds patterns like: ``http://www.example.com`` # # `\w[a-zA-Z0-9. :#\n]* matches the back-tick character immediately followed by one # letter, then followed by any number of letters, numbers, periods, spaces, colons, # hash marks or newlines. # This finds patterns like: `Link text `_ # # \.\. _?[\w. :]+ matches the pattern .. followed one space, then by zero or # one underscore, then any number of letters, periods, spaces, or colons. # This finds patterns like: .. _a link: https://domain.invalid/ # # ' matches a single quote. # This finds patterns like: 'http://www.example.com' # # ? matches the previous pattern between zero or one times. # # ? is used to find the actual link. # ? matches the character > between zero and one times. URL_REGEX = ( rf"(__ |`{{2}}|`\w[\w :#\n]*[.|\.\. _?[\w. :]+|')??" ) URL_SKIP_REGEX = rf"({URL_PATTERNS}):(/){{0,2}}(``|')" """The regex used to ignore found hyperlinks. URLs that don't actually contain a domain, but only the URL pattern should be treated like simple text. This will ignore URLs like ``http://`` or 'ftp:`. ({URL_PATTERNS}) matches one of the URL patterns. :(/){{0,2}} matches a colon followed by up to two forward slashes. (``|') matches a double back-tick or single quote. """ # Keep these constants as constants. MAX_PYTHON_VERSION = (3, 11) DEFAULT_INDENT = 4 """The default indentation for docformatter.""" HEURISTIC_MIN_LIST_ASPECT_RATIO = 0.4 """The minimum aspect ratio to consider a list.""" STR_QUOTE_TYPES = ( '"""', "'''", ) RAW_QUOTE_TYPES = ( 'r"""', 'R"""', "r'''", "R'''", ) UCODE_QUOTE_TYPES = ( 'u"""', 'U"""', "u'''", "U'''", ) QUOTE_TYPES = STR_QUOTE_TYPES + RAW_QUOTE_TYPES + UCODE_QUOTE_TYPES docformatter-1.7.8/src/docformatter/encode.py000066400000000000000000000104411517155121300213100ustar00rootroot00000000000000#!/usr/bin/env python # # docformatter.encode.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """This module provides docformatter's Encoder class.""" # Standard Library Imports import collections import locale import sys from typing import Dict, List # Third Party Imports from charset_normalizer import from_path # pylint: disable=import-error unicode = str class Encoder: """Encoding and decoding of files.""" CR = "\r" LF = "\n" CRLF = "\r\n" # Default encoding to use if the file encoding cannot be detected DEFAULT_ENCODING = sys.getdefaultencoding() def __init__(self): """Initialize an Encoder instance.""" self.encoding = self.DEFAULT_ENCODING self.system_encoding = locale.getpreferredencoding() or sys.getdefaultencoding() def do_detect_encoding(self, filename) -> None: """Return the detected file encoding. Parameters ---------- filename : str The full path name of the file whose encoding is to be detected. """ try: detection_result = from_path(filename).best() if detection_result and detection_result.encoding in ["utf_16", "utf_32"]: # Treat undetectable/binary encodings as failure self.encoding = self.DEFAULT_ENCODING else: self.encoding = ( detection_result.encoding if detection_result else self.DEFAULT_ENCODING ) # Check for correctness of encoding. with self.do_open_with_encoding(filename) as check_file: check_file.read() except (SyntaxError, LookupError, UnicodeDecodeError): self.encoding = self.DEFAULT_ENCODING def do_find_newline(self, source: List[str]) -> str: """Return type of newline used in source. Parameters ---------- source : list A list of lines. Returns ------- newline : str The most prevalent new line type found. """ assert not isinstance(source, unicode) counter: Dict[str, int] = collections.defaultdict(int) for line in source: if line.endswith(self.CRLF): counter[self.CRLF] += 1 elif line.endswith(self.CR): counter[self.CR] += 1 elif line.endswith(self.LF): counter[self.LF] += 1 return ( sorted( counter, key=counter.get, # type: ignore reverse=True, ) or [self.LF] )[0] def do_open_with_encoding(self, filename, mode: str = "r"): """Return opened file with a specific encoding. Parameters ---------- filename : str The full path name of the file to open. mode : str The mode to open the file in. Defaults to read-only. Returns ------- contents : TextIO The contents of the file. """ return open( filename, mode=mode, encoding=self.encoding, newline="" ) # Preserve line endings docformatter-1.7.8/src/docformatter/format.py000066400000000000000000001107511517155121300213500ustar00rootroot00000000000000#!/usr/bin/env python # # docformatter.format.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """This module provides docformatter's Formattor class.""" # Standard Library Imports import argparse import collections import contextlib import difflib import io import tokenize from typing import TextIO, Union # docformatter Package Imports import docformatter.classify as _classify import docformatter.encode as _encode import docformatter.patterns as _patterns import docformatter.strings as _strings import docformatter.util as _util import docformatter.wrappers as _wrappers from docformatter.constants import QUOTE_TYPES unicode = str def _do_remove_preceding_blank_lines( tokens: list[tokenize.TokenInfo], blocks: list[tuple[int, int, str]], ) -> list[tokenize.TokenInfo]: """Remove all blank lines preceding a docstring. docformatter_6.8: No blank lines before a class docstring. docformatter_8.3: No blank lines before a module docstring. docformatter_8.3: One blank line before a module docstring if the docstring follows immediately after a shebang line. docformatter_9.9: No blank lines before a function or method docstring. docformatter_12.2: No blank lines before an attribute docstring. Parameters ---------- tokens : list A list of tokens from the source code. blocks : list A list of tuples containing the index of any docstrings and the docstring type. Returns ------- list A list of tokens with blank lines preceding docstrings removed. """ _num_tokens = len(tokens) _indices_to_remove = [] for i in range(_num_tokens): match = next(((s, d, t) for (s, d, t) in blocks if d == i), None) if match: s, d, typ = match for j in range(d - 1, 0, -1): # Break out of loop once we reach a class, function, method, or # attribute. No more blank lines should be removed once we get to the # structure the docstring is associated with. if ( tokens[j].type == tokenize.NAME and tokens[j].string in ("class", "def", "async") ) or (tokens[j].type == tokenize.OP and tokens[j].string in ("=", ":")): break elif ( tokens[j].type in (tokenize.NEWLINE, tokenize.NL) and tokens[j].line == "\n" and not tokens[j - 1].line.startswith("#") ): _indices_to_remove.append(j) # We need to go in reverse order to prevent the token list indices from # getting out of whack. For example, if _indices_to_remove = [5, 21] and we # removed index 5 first, then old index 22 would become the new index 21 and # the next iteration of the loop would remove the wrong token. _indices_to_remove.sort(reverse=True) # Loop through the token indices in reverse order and remove them from the token # line. for i in _indices_to_remove: tokens.pop(i) return tokens def _do_skip_newlines( tokens: list[tokenize.TokenInfo], docstring_idx: int, ) -> int: """Skip newline tokens between anchor and docstring indices. Parameters ---------- tokens : list The list of tokens representing the docstring text. docstring_idx : int The index in the list of tokens where the docstring begins. Returns ------- int The index of the last newline token. """ j = docstring_idx + 1 while j < len(tokens) and tokens[j].type in ( tokenize.NL, tokenize.NEWLINE, ): j += 1 return j def _is_multiline_parameter(tokens: list[tokenize.TokenInfo], index: int) -> bool: """Determine if a token is a multiline string parameter. A multiline string token spans multiple physical lines in the source code. This is determined by checking if the token's start and end line numbers are different. Parameters ---------- tokens : list[tokenize.TokenInfo] The list of tokens. index : int The index of the token to check. Returns ------- bool True if the token is a multiline string parameter, False otherwise. """ if index >= len(tokens): return False token = tokens[index] return token.type == tokenize.STRING and token.start[0] != token.end[0] def _do_update_token_indices( tokens: list[tokenize.TokenInfo], ) -> list[tokenize.TokenInfo]: """Update the indices of tokens after a newline that is to be removed. When a newline before a docstring is removed, the indices of all following tokens must be updated to reflect the missing newline. Parameters ---------- tokens : list A list of tokens from the source code. Returns ------- list The updated list of tokens. """ _end_row = tokens[0].end[0] _end_col = tokens[0].end[1] _num_tokens = len(tokens) for i in range(1, _num_tokens): _num_rows, _num_cols = _get_num_rows_columns(tokens[i]) # If the current token line is the same as the preceding token line, # the starting row for the current token should be the same as the ending # line for the previous token unless both lines are NEWLINES. # Also check if tokens are at the same position (handles multiline strings). is_multiline = _is_multiline_parameter(tokens, i - 1) is_same_line = tokens[i].line == tokens[i - 1].line is_same_position = tokens[i].start[0] == tokens[i - 1].end[0] if ( is_multiline or (is_same_line or is_same_position) and tokens[i - 1].type not in ( tokenize.NEWLINE, tokenize.NL, ) ): _start_idx, _end_idx = _get_start_end_indices( tokens[i], tokens[i - 1], _num_rows, _num_cols, ) tokens[i] = tokens[i]._replace(start=_start_idx) tokens[i] = tokens[i]._replace(end=_end_idx) if tokens[i].type in (tokenize.NEWLINE, tokenize.NL) and tokens[ i - 1 ].type in (tokenize.NEWLINE, tokenize.NL): tokens[i] = tokens[i]._replace(start=(_end_idx[0], tokens[i].start[1])) # If the current token line is different from the preceding token line, # the current token starting row should be one greater than the previous # token's end row. else: _start_idx, _end_idx = _get_unmatched_start_end_indices( tokens[i], tokens[i - 1], _num_rows, ) tokens[i] = tokens[i]._replace(start=_start_idx) tokens[i] = tokens[i]._replace(end=_end_idx) return tokens def _get_attribute_docstring_newlines( tokens: list[tokenize.TokenInfo], index: int, ) -> int: """Return number of newlines after an attribute docstring. docformatter_12.1: One blank line after an attribute docstring. docformatter_12.1.1: Two blank lines if followed by top-level class or function definition. Parameters ---------- tokens : list A list of tokens from the source code. index : int The index of the docstring token in the list of tokens. Returns ------- newlines : int The number of newlines to insert after the docstring. """ _num_tokens = len(tokens) _offset = 2 for i in range(index + 2, _num_tokens - index - 1): if tokens[i].line == "\n": _offset += 1 else: break if tokens[index + _offset].line.startswith("class") or tokens[ index + _offset ].line.startswith("def"): return 2 return 1 def _get_class_docstring_newlines( tokens: list[tokenize.TokenInfo], index: int, ) -> int: """Return number of newlines after a class docstring. PEP_257_6.1: One blank line after a class docstring. docformatter_6.9: Keep in-line comment after triple quotes in-line. Parameters ---------- tokens : list A list of tokens from the source code. index : int The index of the docstring token in the list of tokens. Returns ------- newlines : int The number of newlines to insert after the docstring. """ j = index + 1 indention_level = tokens[index].start[1] # The docstring is followed by a comment. if tokens[j].string.startswith("#"): return 0 while j < len(tokens): if tokens[j].type in (tokenize.NL, tokenize.NEWLINE): j += 1 continue if tokens[j].start[1] < indention_level: return 2 break return 1 def _get_function_docstring_newlines( # noqa: PLR0911 tokens: list[tokenize.TokenInfo], index: int, ) -> int: """Return number of newlines after a function or method docstring. PEP_257_9.5: No blank lines after a function or method docstring. docformatter_9.6: One blank line after a function or method docstring if there is an inner function definition when in black mode. docformatter_9.7: Two blank lines after a function docstring if the stub function has no code. docformatter_9.8: One blank line after a method docstring if the stub method has no code. Parameters ---------- tokens : list A list of tokens from the source code. index : int The index of the docstring token in the list of tokens. Returns ------- newlines : int The number of newlines to insert after the docstring. """ j = index + 1 # The docstring is followed by a comment. if tokens[j].string.startswith("#"): return 0 # Scan ahead to skip decorators and check for def/async def while j < len(tokens): if tokens[j].type == tokenize.OP and tokens[j].string == "@": # Skip to the end of the decorator line while j < len(tokens) and tokens[j][0] != tokenize.NEWLINE: j += 1 j += 1 continue # The docstring is followed by an attribute assignment. if tokens[j].type == tokenize.OP and tokens[j].string == "=": return 0 # There is a line of code following the docstring. if _classify.is_code_line(tokens[j]): if tokens[j].start[1] == 0: return 1 return 0 # There is a method definition or nested function or class definition following # the docstring and docformatter is running in black mode. if _classify.is_nested_definition_line(tokens[j]): return 1 # There is a function or class definition following the docstring. if _classify.is_definition_line(tokens[j]): return 2 j += 1 return 0 def _get_module_docstring_newlines() -> int: """Return number of newlines after a module docstring. docformatter_8.2: One blank line after a module docstring. Returns ------- newlines : int The number of newlines to insert after the docstring. """ return 1 def _get_newlines_by_type( tokens: list[tokenize.TokenInfo], index: int, ) -> int: """Dispatch to the correct docstring formatter based on context. Returns the number of newlines to insert after the docstring. Parameters ---------- tokens : list A list of tokens from the source code. index : int The index of the docstring token in the list of tokens. Returns ------- int The number of newlines to insert after the docstring. """ if _classify.is_docstring_at_end_of_file(tokens, index): # print("End of file") return 0 elif _classify.is_module_docstring(tokens, index): # print("Module") return _get_module_docstring_newlines() elif _classify.is_class_docstring(tokens, index): # print("Class") return _get_class_docstring_newlines(tokens, index) elif _classify.is_function_or_method_docstring(tokens, index): # print("Function or method") return _get_function_docstring_newlines(tokens, index) elif _classify.is_attribute_docstring(tokens, index): # print("Attribute") return _get_attribute_docstring_newlines(tokens, index) return 0 # Default: probably a string literal def _get_num_rows_columns(token: tokenize.TokenInfo) -> tuple[int, int]: """Determine the number of rows and columns needed for the docstring. Parameters ---------- token : tokenize.TokenInfo The token whose rows and columns needs to be determined. Returns ------- rows_cols : tuple(int, int) The number of rows and columns for the token. """ # Find the number of rows and columns the line requires. When the docstring is # multiple lines, we'll need to update the row in the end index appropriately. # The number of columns is needed to properly set the end index column number. _split_line = token.line.split("\n") _num_rows = len(_split_line) - 1 _num_cols = len(_split_line[_num_rows - 1]) return _num_rows, _num_cols def _get_start_end_indices( token: tokenize.TokenInfo, prev_token: tokenize.TokenInfo, num_rows: int, num_cols: int, ) -> tuple[tuple[int, int], tuple[int, int]]: """Determine the start and end indices for the token. Parameters ---------- token : tokenize.TokenInfo The token whose start and end indices are being determined. prev_token : tokenize.TokenInfo The token prior to the token whose start and end indices are being determined. num_rows : int The number of rows the token requires. num_cols : int The number of columns the token requires. Returns ------- indices : tuple(tuple(int, int), tuple(int, int)) The start and end index for the token. """ _start_row = prev_token.end[0] _start_col = token.start[1] _end_row = _start_row _end_col = token.end[1] # For multiline STRING tokens, update the end row but keep the original end column. # The num_cols calculation from the line is incorrect for multiline strings because # the line attribute contains more than just the string content. if num_rows > 1 and token.type == tokenize.STRING: _end_row = _start_row + num_rows - 1 elif num_rows > 1 and _end_row != prev_token.end[0]: _end_row = _start_row + num_rows - 1 _end_col = num_cols return (_start_row, _start_col), (_end_row, _end_col) def _get_unmatched_start_end_indices( token: tokenize.TokenInfo, prev_token: tokenize.TokenInfo, num_rows: int, ) -> tuple[tuple[int, int], tuple[int, int]]: """Determine the start and end indices for the token if it doesn't match the prior. Parameters ---------- token : tokenize.TokenInfo The token whose start and end indices are being determined. prev_token : tokenize.TokenInfo The token prior to the token whose start and end indices are being determined. num_rows : int The number of rows the token requires. Returns ------- indices : tuple(tuple(int, int), tuple(int, int)) The start and end index for the token. """ _start_row = prev_token.end[0] + 1 _start_col = token.start[1] _end_row = _start_row _end_col = token.end[1] if any( [ _classify.is_inline_comment(token), _classify.is_string_variable(token, prev_token), _classify.is_newline_continuation(token, prev_token), _classify.is_line_following_indent(token, prev_token), _classify.is_closing_quotes(token, prev_token), _classify.is_f_string(token, prev_token), ] ): _start_row = prev_token.end[0] if num_rows > 1 and token.type != tokenize.INDENT: _end_row = _end_row + num_rows - 1 return (_start_row, _start_col), (_end_row, _end_col) class FormatResult: """Possible exit codes.""" ok = 0 error = 1 interrupted = 2 format_required = 3 # noinspection PyArgumentList class Formatter: """Format docstrings.""" parser = None """Parser object.""" args: argparse.Namespace = argparse.Namespace() def __init__( self, args: argparse.Namespace, stderror: TextIO, stdin: TextIO, stdout: TextIO, ) -> None: """Initialize a Formattor instance. Parameters ---------- args : argparse.Namespace Any command line arguments passed during invocation or configuration file options. stderror : TextIO The standard error device. Typically, the screen. stdin : TextIO The standard input device. Typically, the keyboard. stdout : TextIO The standard output device. Typically, the screen. Returns ------- object """ self.args = args self.stderror: TextIO = stderror self.stdin: TextIO = stdin self.stdout: TextIO = stdout self.encodor = _encode.Encoder() self.new_tokens: list[tokenize.TokenInfo] = [] def do_format_standard_in(self, parser: argparse.ArgumentParser) -> None: """Print formatted text from standard in to standard out. Parameters ---------- parser : argparse.ArgumentParser The argument parser containing the formatting options. """ if len(self.args.files) > 1: parser.error("cannot mix standard in and regular files") if self.args.in_place: parser.error("--in-place cannot be used with standard input") if self.args.recursive: parser.error("--recursive cannot be used with standard input") encoding = None source = self.stdin.read() if not isinstance(source, unicode): encoding = self.stdin.encoding or self.encodor.system_encoding source = source.decode(encoding) formatted_source = self._do_format_code(source) if encoding: formatted_source = formatted_source.encode(encoding) self.stdout.write(formatted_source) def do_format_files(self) -> Union[int, None]: """Format multiple files. Return ------ code : int | None One of the FormatResult return codes. """ outcomes: dict[int, int] = collections.Counter() return_codes = [ # in order of preference FormatResult.error, FormatResult.format_required, FormatResult.ok, ] _files_to_format = _util.find_py_files( list(self.args.files), self.args.recursive, self.args.exclude ) is_empty = True for filename in _files_to_format: is_empty = False try: result = self._do_format_file(filename) outcomes[result] += 1 except OSError as exception: outcomes[FormatResult.error] += 1 # noinspection PyTypeChecker print(unicode(exception), file=self.stderror) # There were no files to process. if is_empty: outcomes[FormatResult.error] += 1 for code in return_codes: if outcomes[code]: return code return 0 def _do_add_blank_lines( self, num_blank_lines: int, start_row: int, end_row: int, ) -> None: """Add the number of blank lines specified by num_blanks after the docstring. Parameters ---------- num_blank_lines : int The number of blank lines to add. start_row : int The start index row for the first blank line. end_row : int The end index row for the first blank line. """ _start = (start_row, 0) _end = (end_row, 1) for k in range(num_blank_lines): new_tok = tokenize.TokenInfo( type=tokenize.NEWLINE, string="\n", start=_start, end=_end, line="\n", ) self.new_tokens.append(new_tok) _start = (_end[0] + 1, 0) _end = (_start[0], 1) def _do_add_formatted_docstring( self, token: tokenize.TokenInfo, next_token: tokenize.TokenInfo, docstring_type: str, blank_line_count: int, ) -> None: """Add a formatted docstring to the new tokens list. Parameters ---------- token : tokenize.TokenInfo The token representing the docstring. next_token : tokenize.TokenInfo The next token after the docstring. docstring_type : str The type of the docstring (e.g., module, class, function, attribute). blank_line_count : int The number of blank lines to add after the docstring. """ _indent = " " * token.start[1] if docstring_type != "module" else "" _formatted = self._do_format_docstring(_indent, token.string) _line = _indent + _formatted # Add a newline to the end of the docstring line unless it already # has one or there is an in-line comment following it. if not _line.endswith("\n") and not next_token.string.startswith("#"): _line += "\n" # Add a token with the formatted docstring line. _new_tok = tokenize.TokenInfo( type=tokenize.STRING, string=_formatted, start=token.start, end=token.end, line=_line, ) self.new_tokens.append(_new_tok) with contextlib.suppress(IndexError): if ( self.new_tokens[-2].type == tokenize.INDENT and self.new_tokens[-2].end[0] == _new_tok.start[0] ): self.new_tokens[-2] = self.new_tokens[-2]._replace(line=_line) # If a comment follows the docstring, skip adding a newline token for # the line. if not next_token.string.startswith("#"): _new_tok = tokenize.TokenInfo( type=tokenize.NEWLINE, string="\n", start=token.end, end=(token.end[0], token.end[1] + 1), line=_line, ) self.new_tokens.append(_new_tok) # Add the appropriate number of NEWLINE tokens based on the type of # docstring. self._do_add_blank_lines( blank_line_count, _new_tok.end[0] + 1, _new_tok.end[0] + 1, ) def _do_add_unformatted_docstring( self, token: tokenize.TokenInfo, docstring_type: str, ) -> None: """Add an unformatted docstring to the new tokens list. Parameters ---------- token : tokenize.TokenInfo The token representing the docstring. docstring_type : str The type of the docstring (e.g., module, class, function, attribute). """ _indent = " " * token.start[1] if docstring_type != "module" else "" _line = _indent + token.string _new_token = tokenize.TokenInfo( type=tokenize.STRING, string=token.string, start=token.start, end=token.end, line=_line, ) self.new_tokens.append(_new_token) # Add a token for the newline after the docstring. _new_token = tokenize.TokenInfo( type=tokenize.NEWLINE, string="\n", start=token.end, end=(token.end[0], token.end[1] + 1), line=_line, ) self.new_tokens.append(_new_token) def _do_format_file(self, filename: str) -> int: """Format docstrings in a file. Parameters ---------- filename : str The path to the file to be formatted. Return ------ int One of the FormatResult codes. """ self.encodor.do_detect_encoding(filename) with self.encodor.do_open_with_encoding(filename) as input_file: source = input_file.read() formatted_source = self._do_format_code(source) ret = FormatResult.ok show_diff = self.args.diff if source != formatted_source: ret = FormatResult.format_required if self.args.check: # noinspection PyTypeChecker print(unicode(filename), file=self.stderror) elif self.args.in_place: with self.encodor.do_open_with_encoding( filename, mode="w", ) as output_file: output_file.write(formatted_source) else: show_diff = True if show_diff: diff = difflib.unified_diff( source.splitlines(), formatted_source.splitlines(), f"before/{filename}", f"after/{filename}", lineterm="", ) self.stdout.write("\n".join(list(diff) + [""])) return ret def _do_format_code(self, source: str) -> str: """Return source code with docstrings formatted. Parameters ---------- source : str The text from the source file. Returns ------- str The source file text with docstrings formatted. """ if not source: return source if self.args.line_range is not None: assert self.args.line_range[0] > 0 and self.args.line_range[1] > 0 if self.args.length_range is not None: assert self.args.length_range[0] > 0 and self.args.length_range[1] > 0 try: _original_newline = self.encodor.do_find_newline(source.splitlines(True)) tokens = list( tokenize.generate_tokens(io.StringIO(source, newline="").readline) ) # Perform docstring rewriting self._do_rewrite_docstring_blocks(tokens) _code = tokenize.untokenize(self.new_tokens) return _strings.do_normalize_line_endings( _code.splitlines(True), _original_newline ).rstrip(" ") except (tokenize.TokenError, IndentationError): return source def _do_format_docstring( # noqa PLR0911 self, indentation: str, docstring: str, ) -> str: """Return formatted version of docstring. Parameters ---------- indentation : str The indentation characters for the docstring. docstring : str The docstring itself. Returns ------- str The docstring formatted according the various options. """ contents, open_quote = _strings.do_strip_docstring(docstring) if ( self.args.black and contents.startswith('"') or not self.args.black and self.args.pre_summary_space ): open_quote = f"{open_quote} " # Skip if there are nested triple double quotes if contents.count(QUOTE_TYPES[0]): return docstring # Do not modify things that start with doctests. if contents.lstrip().startswith(">>>"): return docstring # Do not modify docstring if the only thing it contains is a link. _links = _patterns.do_find_links(contents) with contextlib.suppress(IndexError): if _links[0][0] == 0 and _links[0][1] == len(contents): return docstring summary, description = _strings.do_split_summary_and_description(contents) # Leave docstrings with only field lists alone. if _patterns.is_field_list( summary, self.args.style, ): return docstring if not self.args.force_wrap and ( _patterns.is_type_of_list( summary, self.args.non_strict, self.args.style, ) or _patterns.do_find_links(summary) ): # Something probably isn't right with the splitting. return docstring # Compensate for textwrap counting each tab in indentation as 1 # character. tab_compensation = indentation.count("\t") * (self.args.tab_width - 1) self.args.wrap_summaries -= tab_compensation self.args.wrap_descriptions -= tab_compensation if description: return self._do_format_multiline_docstring( indentation, summary, description, open_quote, ) return self._do_format_oneline_docstring( indentation, contents, open_quote, ) def _do_format_oneline_docstring( self, indentation: str, contents: str, open_quote: str, ) -> str: """Format one line docstrings. Parameters ---------- indentation : str The indentation to use for each line. contents : str The contents of the original docstring. open_quote : str The type of quote used by the original docstring. Selected from QUOTE_TYPES. Returns ------- str The formatted docstring. """ if self.args.make_summary_multi_line: beginning = f"{open_quote}\n{indentation}" ending = f'\n{indentation}"""' summary_wrapped = _wrappers.do_wrap_summary( _strings.do_normalize_summary(contents, self.args.non_cap), wrap_length=self.args.wrap_summaries, initial_indent=indentation, subsequent_indent=indentation, ).strip() return f"{beginning}{summary_wrapped}{ending}" else: summary_wrapped = _wrappers.do_wrap_summary( open_quote + _strings.do_normalize_summary(contents, self.args.non_cap) + '"""', wrap_length=self.args.wrap_summaries, initial_indent=indentation, subsequent_indent=indentation, ).strip() if self.args.close_quotes_on_newline and "\n" in summary_wrapped: summary_wrapped = ( f"{summary_wrapped[:-3]}\n{indentation}{summary_wrapped[-3:]}" ) return summary_wrapped def _do_format_multiline_docstring( self, indentation: str, summary: str, description: str, open_quote: str, ) -> str: """Format multiline docstrings. Parameters ---------- indentation : str The indentation to use for each line. summary : str The summary from the original docstring. description : str The long description from the original docstring. open_quote : str The type of quote used by the original docstring. Selected from QUOTE_TYPES. Returns ------- str The formatted docstring. """ # Compensate for triple quotes by temporarily prepending 3 spaces. # This temporary prepending is undone below. initial_indent = ( indentation if self.args.pre_summary_newline else 3 * " " + indentation ) pre_summary = "\n" + indentation if self.args.pre_summary_newline else "" summary = _wrappers.do_wrap_summary( _strings.do_normalize_summary(summary, self.args.non_cap), wrap_length=self.args.wrap_summaries, initial_indent=initial_indent, subsequent_indent=indentation, ).lstrip() description = _wrappers.do_wrap_description( description, indentation=indentation, wrap_length=self.args.wrap_descriptions, force_wrap=self.args.force_wrap, strict=self.args.non_strict, rest_sections=self.args.rest_section_adorns, style=self.args.style, ) post_description = "\n" if self.args.post_description_blank else "" return f'''\ {open_quote}{pre_summary}{summary} {description}{post_description} {indentation}"""\ ''' def _do_rewrite_docstring_blocks( self, tokens: list[tokenize.TokenInfo], ) -> None: """Replace all docstring blocks with properly formatted docstrings. Parameters ---------- tokens : list The tokenized Python source code. """ # print(tokens) _blocks = _classify.do_find_docstring_blocks(tokens) _skip_indices: set[int] = set() self.new_tokens = [] for _idx, _token in enumerate(tokens): if _idx in _skip_indices: continue _match = next(((s, d, t) for (s, d, t) in _blocks if d == _idx), None) if _match: _anchor_idx, _docstr_idx, _type = _match _last_idx = _do_skip_newlines(tokens, _docstr_idx) _skip_indices.update(range(_anchor_idx + 1, _last_idx)) _docstring_token = tokens[_docstr_idx] _blank_line_count = _get_newlines_by_type( tokens, _docstr_idx, ) if ( _util.is_in_range( self.args.line_range, _docstring_token.start[0], _docstring_token.end[0], ) and _util.has_correct_length( self.args.length_range, _docstring_token.start[0], _docstring_token.end[0], ) and not _patterns.is_string_constant(tokens[_docstr_idx - 1]) ): self._do_add_formatted_docstring( _docstring_token, tokens[_idx + 1], _type, _blank_line_count, ) else: self._do_add_unformatted_docstring(_docstring_token, _type) if ( ( self.new_tokens[-2].string == tokens[_idx + 1].string and _docstring_token.line == tokens[_idx + 1].line ) or tokens[_idx + 1].string == "\n" or tokens[_idx + 1].type in (tokenize.NEWLINE, tokenize.NL) ): _skip_indices.add(_idx + 1) continue else: _new_tok = _token # If it's a standalone STRING (not identified as a docstring block), # ensure .line ends with newline if _token.type == tokenize.STRING: _line = _token.line if not _line.endswith("\n"): _line += "\n" _new_tok = tokenize.TokenInfo( type=_token.type, string=_token.string, start=_token.start, end=_token.end, line=_line, ) self.new_tokens.append(_new_tok) self.new_tokens = _do_remove_preceding_blank_lines(self.new_tokens, _blocks) self.new_tokens = _do_update_token_indices(self.new_tokens) docformatter-1.7.8/src/docformatter/patterns/000077500000000000000000000000001517155121300213415ustar00rootroot00000000000000docformatter-1.7.8/src/docformatter/patterns/__init__.py000066400000000000000000000030031517155121300234460ustar00rootroot00000000000000#!/usr/bin/env python # # docformatter.patterns.__init__.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """This is the docformatter patterns package.""" # docformatter Local Imports from .fields import * # noqa F403 from .headers import * # noqa F403 from .lists import * # noqa F403 from .misc import * # noqa F403 from .rest import * # noqa F403 from .url import * # noqa F403 docformatter-1.7.8/src/docformatter/patterns/fields.py000066400000000000000000000145161517155121300231700ustar00rootroot00000000000000#!/usr/bin/env python # # docformatter.patterns.fields.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """This module provides docformatter's field list pattern recognition functions.""" # Standard Library Imports import re from re import Match from typing import Union # docformatter Package Imports from docformatter.constants import ( EPYTEXT_REGEX, GOOGLE_REGEX, NUMPY_REGEX, SPHINX_REGEX, ) def do_find_field_lists( text: str, style: str, ) -> tuple[list[tuple[int, int]], bool]: r"""Determine if docstring contains any field lists. Parameters ---------- text : str The docstring description to check for field list patterns. style : str The field list style used. Returns ------- tuple[list[tuple], bool] A tuple containing lists of tuples and a boolean. Each inner tuple contains the starting and ending position of each field list found in the description. The boolean indicates whether long field list lines should be wrapped. """ _field_idx = [] _wrap_parameters = False if style == "epytext": _field_idx = [ (_field.start(0), _field.end(0)) for _field in re.finditer(EPYTEXT_REGEX, text) ] _wrap_parameters = True elif style == "sphinx": _field_idx = [ (_field.start(0), _field.end(0)) for _field in re.finditer(SPHINX_REGEX, text) ] _wrap_parameters = True return _field_idx, _wrap_parameters def is_field_list( text: str, style: str, ) -> bool: """Determine if docstring contains field lists. Parameters ---------- text : str The docstring text. style : str The field list style to use. Returns ------- is_field_list : bool Whether the field list pattern for style was found in the docstring. """ split_lines = text.rstrip().splitlines() if style == "epytext": return any(is_epytext_field_list(line) for line in split_lines) elif style == "sphinx": return any(is_sphinx_field_list(line) for line in split_lines) return False def is_epytext_field_list(line: str) -> Union[Match[str], None]: """Check if the line is an Epytext field list. Parameters ---------- line : str The line to check for Epytext field list patterns. Returns ------- Match[str] | None A match object if the line matches an Epytext field list pattern, None otherwise. Notes ----- Epytext field lists have the following pattern: @param x: @type x: """ return re.match(EPYTEXT_REGEX, line) def is_google_field_list(line: str) -> Union[Match[str], None]: """Check if the line is a Google field list. Parameters ---------- line: str The line to check for Google field list patterns. Returns ------- Match[str] | None A match object if the line matches a Google field list pattern, None otherwise. Notes ----- Google field lists have the following pattern: x (int): Description of x. """ return re.match(GOOGLE_REGEX, line) def is_numpy_field_list(line: str) -> Union[Match[str], None]: """Check if the line is a NumPy field list. Parameters ---------- line: str The line to check for NumPy field list patterns. Returns ------- Match[str] | None A match object if the line matches a NumPy field list pattern, None otherwise. Notes ----- NumPy field lists have the following pattern: x : int Description of x. x Description of x. """ return re.match(NUMPY_REGEX, line) def is_sphinx_field_list(line: str) -> Union[Match[str], None]: """Check if the line is a Sphinx field list. Parameters ---------- line: str The line to check for Sphinx field list patterns. Returns ------- Match[str] | None A match object if the line matches a Sphinx field list pattern, None otherwise. Notes ----- Sphinx field lists have the following pattern: :parameter: description """ return re.match(SPHINX_REGEX, line) # TODO: Add a USER_DEFINED_REGEX to constants.py and use that instead of the # hardcoded patterns. def is_user_defined_field_list(line: str) -> Union[Match[str], None]: """Check if the line is a user-defined field list. Parameters ---------- line: str The line to check for user-defined field list patterns. Returns ------- Match[str] | None A match object if the line matches a user-defined field list pattern, None otherwise. Notes ----- User-defined field lists have the following pattern: parameter - description parameter -- description @parameter description These patterns were in the original docformatter code. These patterns do not conform to any common docstring styles. There is no documented reason they were included and are retained for historical purposes. """ return ( re.match(r"[\S ]+ - \S+", line) or re.match(r"\s*\S+\s+--\s+", line) or re.match(r"^ *@[a-zA-Z0-9_\- ]*(?:(?!:).)*$", line) ) docformatter-1.7.8/src/docformatter/patterns/headers.py000066400000000000000000000076511517155121300233370ustar00rootroot00000000000000#!/usr/bin/env python # # docformatter.patterns.headers.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """This module provides docformatter's header pattern recognition functions.""" # Standard Library Imports import re from re import Match from typing import Union # docformatter Package Imports from docformatter.constants import ( ALEMBIC_REGEX, NUMPY_SECTION_REGEX, REST_SECTION_REGEX, ) def is_alembic_header(line: str) -> Union[Match[str], None]: """Check if the line is an Alembic header. Parameters ---------- line : str The line to check for Alembic header patterns. Notes ----- Alembic headers have the following pattern: Revision ID: > Revises: Create Date: 2023-01-06 10:13:28.156709 Returns ------- bool True if the line matches an Alembic header pattern, False otherwise. """ return re.match(ALEMBIC_REGEX, line) def is_numpy_section_header(line: str) -> Union[Match[str], None]: r"""Check if the line is a NumPy section header. Parameters ---------- line : str The line to check for NumPy section header patterns. Notes ----- NumPy section headers have the following pattern: header\n---- The following NumPy section headers are recognized: * Parameters * Other Parameters * Receives * Returns * Yields * Raises * Warns * Warnings * Notes * See Also * Examples Returns ------- Match[str] | None A match object if the line matches a NumPy section header pattern, None otherwise. """ return re.match(NUMPY_SECTION_REGEX, line) def is_rest_section_header(line: str) -> Union[Match[str], None]: r"""Check if the line is a reST section header. Parameters ---------- line : str The line to check for reST section header patterns. Notes ----- reST section headers have the following patterns: ====\ndescription\n==== ----\ndescription\n---- description\n---- The following adornments used in Python documentation are supported (see https://devguide.python.org/documentation/markup/#sections): #, for parts *, for chapters =, for sections -, for subsections ^, for subsubsections The following additional docutils recommended adornments are supported (see https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#sections): ', single quote ", double quote +, plus sign _, underscore ~, tilde `, backtick ., period :, colon Returns ------- bool True if the line matches a reST section header pattern, False otherwise. """ return re.match(REST_SECTION_REGEX, line) docformatter-1.7.8/src/docformatter/patterns/lists.py000066400000000000000000000175461517155121300230660ustar00rootroot00000000000000#!/usr/bin/env python # # docformatter.patterns.lists.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """This module provides docformatter's list pattern recognition functions.""" # Standard Library Imports import re from re import Match from typing import Union # docformatter Package Imports from docformatter.constants import ( BULLET_REGEX, ENUM_REGEX, HEURISTIC_MIN_LIST_ASPECT_RATIO, OPTION_REGEX, ) # docformatter Local Imports from .fields import ( is_epytext_field_list, is_field_list, is_google_field_list, is_numpy_field_list, is_sphinx_field_list, is_user_defined_field_list, ) from .headers import is_alembic_header, is_numpy_section_header, is_rest_section_header from .misc import is_inline_math, is_literal_block def _create_multiline_windows(lines: list[str], window_size: int = 3) -> list[str]: r"""Create overlapping windows of consecutive lines. This allows pattern matching against multi-line constructs like NumPy section headers (e.g., "Parameters\\n----------") and reST section headers (e.g., "Title\\n====="). Parameters ---------- lines : list[str] The list of individual lines. window_size : int Number of consecutive lines to join (default 3). Returns ------- list[str] List of multi-line strings, each containing window_size consecutive lines joined with newlines. Notes ----- Example: lines = ['A', 'B', 'C', 'D'] _create_multiline_windows(lines, 2) # Returns: ['A\\nB', 'B\\nC', 'C\\nD'] """ if len(lines) < window_size: return ["\n".join(lines)] if lines else [] return [ "\n".join(lines[i : i + window_size]) for i in range(len(lines) - window_size + 1) ] def is_type_of_list( text: str, strict: bool, style: str, ) -> bool: """Determine if docstring line is a list. Parameters ---------- text : str The text to check for potential lists. strict : bool Whether to strictly adhere to the wrap length argument. If True, even heuristic lists will be wrapped. style : str The docstring style in use. One of 'epytext', 'sphinx', numpy', or 'googlw'. Returns ------- bool True if a list pattern is identified, False otherwise. """ split_lines = text.rstrip().splitlines() if is_heuristic_list(text, strict): return True if is_field_list(text, style): return False # Check for multi-line patterns (section headers) first. # These require looking at consecutive lines together. multiline_windows = _create_multiline_windows(split_lines, window_size=2) for window in multiline_windows: if is_rest_section_header(window) or is_numpy_section_header(window): return True # Check single-line patterns. return any( ( is_bullet_list(line) or is_enumerated_list(line) or is_option_list(line) or is_epytext_field_list(line) or is_sphinx_field_list(line) or is_numpy_field_list(line) or is_google_field_list(line) or is_user_defined_field_list(line) or is_literal_block(line) or is_inline_math(line) or is_alembic_header(line) ) for line in split_lines ) def is_bullet_list(line: str) -> Union[Match[str], None]: """Check if the line is a bullet list item. Parameters ---------- line : str The line to check for bullet list patterns. Returns ------- Match[str] | None A match object if the line matches a bullet list pattern, None otherwise. Notes ----- Bullet list items have the following pattern: - item * item + item See `_ """ return re.match(BULLET_REGEX, line) def is_definition_list(line: str) -> Union[Match[str], None]: """Check if the line is a definition list item. Parameters ---------- line : str The line to check for definition list patterns. Returns ------- Match[str] | None A match object if the line matches a definition list pattern, None otherwise. Notes ----- Definition list items have the following pattern: term: definition See `_ """ return re.match(ENUM_REGEX, line) def is_enumerated_list(line: str) -> Union[Match[str], None]: """Check if the line is an enumerated list item. Parameters ---------- line : str The line to check for enumerated list patterns. Returns ------- Match[str] | None A match object if the line matches an enumerated list pattern, None otherwise. Notes ----- Enumerated list items have the following pattern: 1. item 2. item See `_ """ return re.match(ENUM_REGEX, line) def is_heuristic_list(text: str, strict: bool) -> bool: """Check if the line is a heuristic list item. Heuristic lists are identified by a long number of lines with short columns. Parameters ---------- text : str The text to check for heuristic list patterns. strict: bool If True, the function will return False. If False, it will return True if the text has a high aspect ratio, indicating it is likely a list. Returns ------- Match[str] | None A match object if the line matches a heuristic list pattern, None otherwise. """ split_lines = text.rstrip().splitlines() # TODO: Find a better way of doing this. Conversely, create a logger and log # potential lists for the user to decide if they are lists or not. # Very large number of lines but short columns probably means a list of # items. if ( len(split_lines) / max([len(line.strip()) for line in split_lines] + [1]) > HEURISTIC_MIN_LIST_ASPECT_RATIO ) and not strict: return True return False def is_option_list(line: str) -> Union[Match[str], None]: """Check if the line is an option list item. Parameters ---------- line : str The line to check for option list patterns. Returns ------- Match[str] | None A match object if the line matches an option list pattern, None otherwise. Notes ----- Option list items have the following pattern: -a, --all: Show all items. -h, --help: Show help message. See `_ """ return re.match(OPTION_REGEX, line) docformatter-1.7.8/src/docformatter/patterns/misc.py000066400000000000000000000101131517155121300226420ustar00rootroot00000000000000#!/usr/bin/env python # # docformatter.patterns.misc.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """This module provides docformatter's miscellaneous pattern recognition functions.""" # Standard Library Imports import re import tokenize from re import Match from typing import Union # docformatter Package Imports from docformatter.constants import LITERAL_REGEX, URL_REGEX # TODO: Create INLINE_MATH_REGEX in constants.py and use it here. def is_inline_math(line: str) -> Union[Match[str], None]: """Check if the line is an inline math expression. Parameters ---------- line : str The line to check for inline math patterns. Returns ------- Match[str] | None A match object if the line matches an inline math pattern, None otherwise. Notes ----- Inline math expressions have the following pattern: c :math:`[0, `]` """ return re.match(r" *\w *:[a-zA-Z0-9_\- ]*:", line) def is_literal_block(line: str) -> Union[Match[str], None]: """Check if the line is a literal block. Parameters ---------- line : str The line to check for literal block patterns. Returns ------- Match[str] | None A match object if the line matches a literal block pattern, None otherwise. Notes ----- Literal blocks have the following pattern: :: code """ return re.match(LITERAL_REGEX, line) def is_probably_beginning_of_sentence(line: str) -> Union[Match[str], None, bool]: """Determine if the line begins a sentence. Parameters ---------- line : str The line to be tested. Returns ------- bool True if this token is the beginning of a sentence, False otherwise. """ # Check heuristically for a parameter list. for token in ["@", "-", r"\*"]: if re.search(rf"\s{token}\s", line): return True stripped_line = line.strip() is_beginning_of_sentence = re.match(r"^[-@\)]", stripped_line) is_pydoc_ref = re.match(r"^:\w+:", stripped_line) return is_beginning_of_sentence and not is_pydoc_ref def is_some_sort_of_code(text: str) -> bool: """Return True if the text looks like code. Parameters ---------- text : str The text to check for code patterns. Returns ------- bool True if the text contains and code patterns, False otherwise. """ return any( len(word) > 50 and not re.match(URL_REGEX, word) # noqa: PLR2004 for word in text.split() ) def is_string_constant(token: tokenize.TokenInfo) -> bool: """Determine if docstring token is actually a string constant. Parameters ---------- token : TokenInfo The token immediately preceding the docstring token. Returns ------- bool True if the doctring token is actually string constant, False otherwise. """ if token.type == tokenize.OP and token.string == "=": return True return False docformatter-1.7.8/src/docformatter/patterns/rest.py000066400000000000000000000047131517155121300226750ustar00rootroot00000000000000#!/usr/bin/env python # # docformatter.patterns.rest.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """This module provides docformatter's reST directive pattern recognition functions.""" # Standard Library Imports import re # docformatter Package Imports from docformatter.constants import REST_DIRECTIVE_REGEX, REST_INLINE_REGEX def do_find_rest_directives( text: str, ) -> list[tuple[int, int]]: """Determine if docstring contains any reST directives. Parameters ---------- text : str The docstring text to test. indent : int The number of spaces the reST directive line is indented. Returns ------- bool True if the docstring is a reST directive, False otherwise. """ _rest_iter = re.finditer(REST_DIRECTIVE_REGEX, text, flags=re.MULTILINE) return [(_rest.start(0), _rest.end(0)) for _rest in _rest_iter] def do_find_inline_rest_markup(text: str) -> list[tuple[int, int]]: """Determine if docstring contains any inline reST markup. Parameters ---------- text : str The docstring text to test. Returns ------- bool True if the docstring is a reST directive, False otherwise. """ _rest_iter = re.finditer(REST_INLINE_REGEX, text, flags=re.MULTILINE) return [(_rest.start(0), _rest.end(0)) for _rest in _rest_iter] docformatter-1.7.8/src/docformatter/patterns/url.py000066400000000000000000000055361517155121300225260ustar00rootroot00000000000000#!/usr/bin/env python # # docformatter.patterns.url.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """This module provides docformatter's URL pattern recognition functions.""" # Standard Library Imports import contextlib import re from typing import List, Tuple # docformatter Package Imports from docformatter.constants import URL_REGEX, URL_SKIP_REGEX def do_find_links(text: str) -> List[Tuple[int, int]]: r"""Determine if docstring contains any links. Parameters ---------- text : str The docstring description to check for link patterns. Returns ------- list A list of tuples with each tuple containing the starting and ending position of each URL found in the description. """ _url_iter = re.finditer(URL_REGEX, text) return [(_url.start(0), _url.end(0)) for _url in _url_iter] def do_skip_link(text: str, index: Tuple[int, int]) -> bool: """Check if the identified URL is other than a complete link. Parameters ---------- text : str The description text containing the link. index : tuple The index in the text of the starting and ending position of the identified link. Returns ------- _do_skip : bool Whether to skip this link and simpley treat it as a standard text word. Notes ----- Is the identified link simply: 1. The URL scheme pattern such as 's3://' or 'file://' or 'dns:'. 2. The beginning of a URL link that has been wrapped by the user. """ _do_skip = re.search(URL_SKIP_REGEX, text[index[0] : index[1]]) is not None with contextlib.suppress(IndexError): _do_skip = _do_skip or (text[index[0]] == "<" and text[index[1]] != ">") return _do_skip docformatter-1.7.8/src/docformatter/strings.py000066400000000000000000000342671517155121300215600ustar00rootroot00000000000000#!/usr/bin/env python # # docformatter.strings.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """This module provides docformatter string manipulation functions.""" # Standard Library Imports import contextlib import re import textwrap from typing import Iterable, List, Optional, Tuple, Union # docformatter Package Imports import docformatter.patterns as _patterns import docformatter.util as _util import docformatter.wrappers as _wrappers from docformatter.constants import ( ABBREVIATIONS, QUOTE_TYPES, RAW_QUOTE_TYPES, UCODE_QUOTE_TYPES, ) def description_to_list( description: str, indentation: str, wrap_length: int, ) -> List[str]: """Convert the description to a list of wrap length lines. Parameters ---------- description : str The docstring description. indentation : str The indentation (number of spaces or tabs) to place in front of each line. wrap_length : int The column to wrap each line at. Returns ------- _wrapped_lines : list A list containing each line of the description wrapped at wrap_length. """ # This is a description containing only one paragraph. if len(re.findall(r"\n\n", description)) <= 0: return textwrap.wrap( textwrap.dedent(description), width=wrap_length, initial_indent=indentation, subsequent_indent=indentation, ) # This is a description containing multiple paragraphs. _wrapped_lines = [] for _line in description.split("\n\n"): _wrapped_line = textwrap.wrap( textwrap.dedent(_line), width=wrap_length, initial_indent=indentation, subsequent_indent=indentation, ) if _wrapped_line: _wrapped_lines.extend(_wrapped_line) _wrapped_lines.append("") with contextlib.suppress(IndexError): if not _wrapped_lines[-1] and not _wrapped_lines[-2]: _wrapped_lines.pop(-1) if ( description[-len(indentation) - 1 : -len(indentation)] == "\n" and description[-len(indentation) - 2 : -len(indentation)] != "\n\n" ): _wrapped_lines.pop(-1) return _wrapped_lines def do_clean_excess_whitespace(text: str, indentation: str) -> str: r"""Strip newlines and multiple whitespace from a string. This function deals with situations such as: `Get\n Cookies.txt str: """Determine the shortest indentation in a list of lines. Parameters ---------- lines : list A list of lines to check indentation. Returns ------- indentation : str The shortest (smallest number of spaces) indentation in the list of lines. """ assert not isinstance(lines, str) indentation = None for line in lines: if line.strip(): non_whitespace_index = len(line) - len(line.lstrip()) _indent = line[:non_whitespace_index] if indentation is None or len(_indent) < len(indentation): indentation = _indent return indentation or "" def do_normalize_line(line: str, newline: str) -> str: """Return line with fixed ending, if ending was present in line. Otherwise, does nothing. Parameters ---------- line : str The line to normalize. newline : str The newline character to use for line endings. Returns ------- normalized_line : str The supplied line with line endings replaced by the newline. """ stripped = line.rstrip("\n\r") return stripped + newline if stripped != line else line def do_normalize_line_endings(lines, newline): """Return fixed line endings. All lines will be modified to use the most common line ending. """ return "".join([do_normalize_line(line, newline) for line in lines]) def do_normalize_summary(summary: str, noncap: Optional[List[str]] = None) -> str: """Return normalized docstring summary. A normalized docstring summary will have the first word capitalized and a period at the end. Parameters ---------- summary : str The summary string. noncap : list A user-provided list of words not to capitalize when they appear as the first word in the summary. Returns ------- summary : str The normalized summary string. """ if noncap is None: noncap = [] # Remove trailing whitespace summary = summary.rstrip() # Add period at end of sentence. if ( summary and (summary[-1].isalnum() or summary[-1] in ['"', "'"]) and (not summary.startswith("#")) ): summary += "." with contextlib.suppress(IndexError): # Look for underscores, periods in the first word, this would typically # indicate the first word is a variable name, file name, or some other # non-standard English word. The search the list of user-defined # words not to capitalize. If none of these exist capitalize the # first word of the summary. if ( all(char not in summary.split(" ", 1)[0] for char in ["_", "."]) and summary.split(" ", 1)[0] not in noncap ): summary = summary[0].upper() + summary[1:] return summary def do_reindent(text, indentation): """Return reindented text that matches indentation.""" if "\t" not in indentation: text = text.expandtabs() text = textwrap.dedent(text) return ( "\n".join( [(indentation + line).rstrip() for line in text.splitlines()] ).rstrip() + "\n" ) def do_split_description( text: str, indentation: str, wrap_length: int, style: str, ) -> Union[List[str], Iterable]: """Split the description into a list of lines. Parameters ---------- text : str The docstring description. indentation : str The indentation (number of spaces or tabs) to place in front of each line. wrap_length : int The column to wrap each line at. style : str The docstring style to use for dealing with parameter lists. Returns ------- _lines : list A list containing each line of the description with any links put back together. """ _lines: List[str] = [] _text_idx = 0 # Check if the description contains any URLs. _url_idx = _patterns.do_find_links(text) # Check if the description contains any field lists. _field_idx, _wrap_fields = _patterns.do_find_field_lists( text, style, ) # Field list wrapping takes precedence over URL wrapping. _url_idx = _util.prefer_field_over_url( _field_idx, _url_idx, ) if not _url_idx and not (_field_idx and _wrap_fields): return description_to_list( text, indentation, wrap_length, ) if _url_idx: _lines, _text_idx = _wrappers.do_wrap_urls( text, _url_idx, 0, indentation, wrap_length, ) if _field_idx: _lines, _text_idx = _wrappers.do_wrap_field_lists( text, _field_idx, _lines, _text_idx, indentation, wrap_length, ) else: # Finally, add everything after the last URL or field list directive. _lines += _wrappers.do_close_description(text, _text_idx, indentation) return _lines def do_split_first_sentence(text): """Split text into first sentence and the rest. Return a tuple (sentence, rest). """ sentence = "" rest = text delimiter = "" previous_delimiter = "" while rest: split = re.split(r"(\s)", rest, maxsplit=1) word = split[0] if len(split) == 3: # noqa PLR2004 delimiter = split[1] rest = split[2] else: assert len(split) == 1 delimiter = "" rest = "" sentence += previous_delimiter + word if sentence.endswith(ABBREVIATIONS): # Ignore false end of sentence. pass elif sentence.endswith((".", "?", "!")): break elif sentence.endswith(":") and delimiter == "\n": # Break on colon if it ends the line. This is a heuristic to detect # the beginning of some parameter list after wards. break previous_delimiter = delimiter delimiter = "" return sentence, delimiter + rest def do_split_summary(lines) -> List[str]: """Split multi-sentence summary into the first sentence and the rest.""" if not lines or not lines[0].strip(): return lines text = lines[0].strip() tokens = re.split(r"(\s+)", text) # Keep whitespace for accurate rejoining sentence = [] i = 0 while i < len(tokens): token = tokens[i] sentence.append(token) if token.endswith(".") and not any( "".join(sentence).strip().endswith(abbr) for abbr in ABBREVIATIONS ): i += 1 break i += 1 rest = tokens[i:] first_sentence = "".join(sentence).strip() rest_text = "".join(rest).strip() lines[0] = first_sentence # If there is remaining text, it should become the beginning of the description # in a multiline docstring. Thus, insert a newline and then the remaining text to # the list of lines. if rest_text: _pos = 1 if len(lines) >= 3 else 0 # noqa: PLR2004 _leading_spaces = " " * (len(lines[_pos]) - len(lines[_pos].lstrip())) _internal_spaces = " " * (_pos) lines.insert(1, "") lines[_pos + 1] = ( f"{_leading_spaces}{rest_text}{_internal_spaces}{lines[_pos + 1].strip()}" ) return lines def do_split_summary_and_description(contents): """Split docstring into summary and description. Return tuple (summary, description). """ split_lines = contents.rstrip().splitlines() split_lines = do_split_summary(split_lines) for index in range(1, len(split_lines)): # Empty line separation would indicate the rest is the description or # symbol on second line probably is a description with a list. if not split_lines[index].strip() or ( index + 1 < len(split_lines) and _patterns.is_probably_beginning_of_sentence(split_lines[index + 1]) ): return ( "\n".join(split_lines[:index]).strip(), "\n".join(split_lines[index:]).rstrip(), ) # Break on first sentence. split = do_split_first_sentence(contents) if split[0].strip() and split[1].strip(): return ( split[0].strip(), do_find_shortest_indentation(split[1].splitlines()[1:]) + split[1].strip(), ) return contents, "" def do_strip_docstring(docstring: str) -> Tuple[str, str]: """Return contents of docstring and opening quote type. Strips the docstring of its triple quotes, trailing white space, and line returns. Determine the type of docstring quote (either string, raw, or Unicode) and returns the opening quotes, including the type identifier, with single quotes replaced by double quotes. Parameters ---------- docstring: str The docstring, including the opening and closing triple quotes. Returns ------- (docstring, open_quote) : tuple The docstring with the triple quotes removed. The opening quote type with single quotes replaced by double quotes. """ docstring = docstring.strip() for quote in QUOTE_TYPES: if quote in RAW_QUOTE_TYPES + UCODE_QUOTE_TYPES and ( docstring.startswith(quote) and docstring.endswith(quote[1:]) ): return docstring.split(quote, 1)[1].rsplit(quote[1:], 1)[ 0 ].strip(), quote.replace("'", '"') elif docstring.startswith(quote) and docstring.endswith(quote): return docstring.split(quote, 1)[1].rsplit(quote, 1)[ 0 ].strip(), quote.replace("'", '"') raise ValueError( "docformatter only handles triple-quoted (single or double) strings" ) def do_strip_leading_blank_lines(text): """Return text with leading blank lines removed.""" split = text.splitlines() found = next((index for index, line in enumerate(split) if line.strip()), 0) return "\n".join(split[found:]) docformatter-1.7.8/src/docformatter/util.py000066400000000000000000000124701517155121300210340ustar00rootroot00000000000000#!/usr/bin/env python # # docformatter.util.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """This module provides docformatter utility functions.""" # Standard Library Imports import os import re import sysconfig from typing import List, Tuple unicode = str _PYTHON_LIBS = set(sysconfig.get_paths().values()) def find_py_files(sources, recursive, exclude=None): """Find Python source files. Parameters ---------- sources : list Paths to files and/or directories to search. recursive : bool Drill down directories if True. exclude : list Which directories and files are excluded. Returns ------- list of files found. """ def is_hidden(name): """Return True if file 'name' is .hidden.""" return os.path.basename(os.path.abspath(name)).startswith(".") def is_excluded(name, excluded): """Return True if file 'name' is excluded.""" return ( any(re.search(re.escape(str(e)), name, re.IGNORECASE) for e in excluded) if excluded else False ) for _name in sorted(sources): if recursive and os.path.isdir(_name): for root, dirs, children in os.walk(unicode(_name)): if is_excluded(root, exclude): break files = sorted( [ _file for _file in children if not is_hidden(_file) and not is_excluded(_file, exclude) and _file.endswith(".py") ] ) for filename in files: yield os.path.join(root, filename) elif ( _name.endswith(".py") and not is_hidden(_name) and not is_excluded(_name, exclude) ): yield _name def has_correct_length(length_range, start, end): """Determine if the line under test is within the desired docstring length. This function is used with the --docstring-length min_rows max_rows argument. Parameters ---------- length_range: list The file row range passed to the --docstring-length argument. start: int The row number where the line under test begins in the source file. end: int The row number where the line under tests ends in the source file. Returns ------- correct_length: bool True if the docstring has the correct length or length range is None, otherwise False """ if length_range is None: return True min_length, max_length = length_range docstring_length = end + 1 - start return min_length <= docstring_length <= max_length def is_in_range(line_range, start, end): """Determine if ??? is within the desired range. This function is used with the --range start_row end_row argument. Parameters ---------- line_range: list The line number range passed to the --range argument. start: int The row number where the line under test begins in the source file. end: int The row number where the line under tests ends in the source file. Returns ------- in_range : bool True if in range or range is None, else False """ if line_range is None: return True return any( line_range[0] <= line_no <= line_range[1] for line_no in range(start, end + 1) ) def prefer_field_over_url( field_idx: List[Tuple[int, int]], url_idx: List[Tuple[int, int]], ): """Remove URL indices that overlap with field list indices. Parameters ---------- field_idx : list The list of field list index tuples. url_idx : list The list of URL index tuples. Returns ------- url_idx : list The url_idx list with any tuples that have indices overlapping with field list indices removed. """ if not field_idx: return url_idx nonoverlapping_urls = [] any_param_start = min(e[0] for e in field_idx) for _key, _value in enumerate(url_idx): if _value[1] < any_param_start: nonoverlapping_urls.append(_value) return nonoverlapping_urls docformatter-1.7.8/src/docformatter/wrappers/000077500000000000000000000000001517155121300213445ustar00rootroot00000000000000docformatter-1.7.8/src/docformatter/wrappers/__init__.py000066400000000000000000000027071517155121300234630ustar00rootroot00000000000000#!/usr/bin/env python # # docformatter.wrappers.__init__.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """This is the docformatter wrappers package.""" # docformatter Local Imports from .description import * # noqa F403 from .fields import * # noqa F403 from .summary import * # noqa F403 from .url import * # noqa F403 docformatter-1.7.8/src/docformatter/wrappers/description.py000066400000000000000000000103141517155121300242400ustar00rootroot00000000000000#!/usr/bin/env python # # docformatter.wrappers.description.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """This module provides docformatter's description wrapper functions.""" # Standard Library Imports import contextlib from typing import List # docformatter Package Imports import docformatter.patterns as _patterns import docformatter.strings as _strings def do_wrap_description( # noqa: PLR0913 text, indentation, wrap_length, force_wrap, strict, rest_sections, style: str = "sphinx", ): """Return line-wrapped description text. We only wrap simple descriptions. We leave doctests, multi-paragraph text, and bulleted lists alone. Parameters ---------- text : str The unwrapped description text. indentation : str The indentation string. wrap_length : int The line length at which to wrap long lines. force_wrap : bool Whether to force docformatter to wrap long lines when normally they would remain untouched. strict : bool Whether to strictly follow reST syntax to identify lists. rest_sections : str A regular expression used to find reST section header adornments. style : str The name of the docstring style to use when dealing with parameter lists (default is sphinx). Returns ------- str The description wrapped at wrap_length characters. """ text = _strings.do_strip_leading_blank_lines(text) # TODO: Don't wrap the doctests, but wrap the remainder of the docstring. # Do not modify docstrings with doctests at all. if ">>>" in text: return text text = _strings.do_reindent(text, indentation).rstrip() # TODO: Don't wrap the code section or the lists, but wrap everything else. # Ignore possibly complicated cases. if wrap_length <= 0 or ( not force_wrap and ( _patterns.is_some_sort_of_code(text) or _patterns.do_find_rest_directives(text) or _patterns.is_type_of_list(text, strict, style) ) ): return text lines = _strings.do_split_description(text, indentation, wrap_length, style) return indentation + "\n".join(lines).strip() def do_close_description( text: str, text_idx: int, indentation: str, ) -> List[str]: """Wrap any description following the last URL or field list. Parameters ---------- text : str The docstring text. text_idx : int The index of the last URL or field list match. indentation : str The indentation string to use with docstrings. Returns ------- _split_lines : list The text input split into individual lines. """ _split_lines = [] with contextlib.suppress(IndexError): _split_lines = ( text[text_idx + 1 :] if text[text_idx] == "\n" else text[text_idx:] ).splitlines() for _idx, _line in enumerate(_split_lines): if _line not in ["", "\n", f"{indentation}"]: _split_lines[_idx] = f"{indentation}{_line.strip()}" return _split_lines docformatter-1.7.8/src/docformatter/wrappers/fields.py000066400000000000000000000131111517155121300231610ustar00rootroot00000000000000#!/usr/bin/env python # # docformatter.wrappers.fields.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """This module provides docformatter's field list wrapper functions.""" # Standard Library Imports import re import textwrap from typing import List, Tuple # docformatter Package Imports import docformatter.strings as _strings from docformatter.constants import DEFAULT_INDENT def do_wrap_field_lists( # noqa: PLR0913 text: str, field_idx: List[Tuple[int, int]], lines: List[str], text_idx: int, indentation: str, wrap_length: int, ) -> Tuple[List[str], int]: """Wrap field lists in the long description. Parameters ---------- text : str The long description text. field_idx : list The list of field list indices found in the description text. lines : list The list of formatted lines in the description that come before the first parameter list item. text_idx : int The index in the description of the end of the last parameter list item. indentation : str The string to use to indent each line in the long description. wrap_length : int The line length at which to wrap long lines in the description. Returns ------- lines, text_idx : tuple A list of the long description lines and the index in the long description where the last parameter list item ended. """ lines.extend( _strings.description_to_list( text[text_idx : field_idx[0][0]], indentation, wrap_length, ) ) for _idx, _field_idx in enumerate(field_idx): _field_name = text[_field_idx[0] : _field_idx[1]] _field_body = _do_join_field_body( text, field_idx, _idx, ) if len(f"{_field_name}{_field_body}") <= (wrap_length - len(indentation)): _field = f"{_field_name}{_field_body}" lines.append(f"{indentation}{_field}") else: lines.extend( _do_wrap_field(_field_name, _field_body, indentation, wrap_length) ) text_idx = _field_idx[1] return lines, text_idx def _do_join_field_body(text: str, field_idx: list[tuple[int, int]], idx: int) -> str: """Join the filed body lines into a single line that can be wrapped. Parameters ---------- text : str The docstring long description text that contains field lists. field_idx : list The list of tuples containing the found field list start and end position. idx : int The index of the tuple in the field_idx list to extract the field body. Returns ------- _field_body : str The field body collapsed into a single line. """ try: _field_body = text[field_idx[idx][1] : field_idx[idx + 1][0]].strip() except IndexError: _field_body = text[field_idx[idx][1] :].strip() _field_body = " ".join( [_line.strip() for _line in _field_body.splitlines()] ).strip() # Add a space before the field body unless the field body is a link. if not _field_body.startswith("`") and _field_body: _field_body = f" {_field_body}" # Is there a blank line between field lists? Keep it if so. if text[field_idx[idx][1] : field_idx[idx][1] + 2] == "\n\n": _field_body = "\n" return _field_body def _do_wrap_field(field_name, field_body, indentation, wrap_length): """Wrap complete field at wrap_length characters. Parameters ---------- field_name : str The name text of the field. field_body : str The body text of the field. indentation : str The string to use for indentation of the first line in the field. wrap_length : int The number of characters at which to wrap the field. Returns ------- _wrapped_field : str The field wrapped at wrap_length characters. """ if len(indentation) > DEFAULT_INDENT: _subsequent = indentation + int(0.5 * len(indentation)) * " " else: _subsequent = 2 * indentation _wrapped_field = textwrap.wrap( textwrap.dedent(f"{field_name}{field_body}"), width=wrap_length, initial_indent=indentation, subsequent_indent=_subsequent, ) for _idx, _field in enumerate(_wrapped_field): _indent = indentation if _idx == 0 else _subsequent _wrapped_field[_idx] = f"{_indent}{re.sub(' +', ' ', _field.strip())}" return _wrapped_field docformatter-1.7.8/src/docformatter/wrappers/summary.py000066400000000000000000000053511517155121300234170ustar00rootroot00000000000000#!/usr/bin/env python # # docformatter.wrappers.summary.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """This module provides docformatter's summary wrapper functions.""" # Standard Library Imports import re import textwrap def do_unwrap_summary(summary: str) -> str: r"""Return summary with newlines removed in preparation for wrapping. Parameters ---------- summary : str The summary text from the docstring. Returns ------- str The summary text with newline (\n) characters replaced by a single space. """ return re.sub(r"\s*\n\s*", " ", summary) def do_wrap_summary( summary: str, initial_indent: str, subsequent_indent: str, wrap_length: int, ) -> str: """Return line-wrapped summary text. If the wrap_length is any value less than or equal to zero, the raw, unwrapped summary text will be returned. Parameters ---------- summary : str The summary text from the docstring. initial_indent : str The indentation string for the first line of the summary. subsequent_indent : str The indentation string for all the other lines of the summary. wrap_length : int The column position to wrap the summary lines. Returns ------- str The summary text from the docstring wrapped at wrap_length columns. """ if wrap_length > 0: return textwrap.fill( do_unwrap_summary(summary), width=wrap_length, initial_indent=initial_indent, subsequent_indent=subsequent_indent, ).strip() else: return summary docformatter-1.7.8/src/docformatter/wrappers/url.py000066400000000000000000000071711517155121300225260ustar00rootroot00000000000000#!/usr/bin/env python # # docformatter.wrappers.url.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """This module provides docformatter's URL wrapper functions.""" # Standard Library Imports import contextlib from typing import Iterable, List, Tuple # docformatter Package Imports import docformatter.patterns as _patterns import docformatter.strings as _strings def do_wrap_urls( text: str, url_idx: Iterable, text_idx: int, indentation: str, wrap_length: int, ) -> Tuple[List[str], int]: """Wrap URLs in the long description. Parameters ---------- text : str The long description text. url_idx : list The list of URL indices found in the description text. text_idx : int The index in the description of the end of the last URL. indentation : str The string to use to indent each line in the long description. wrap_length : int The line length at which to wrap long lines in the description. Returns ------- _lines, _text_idx : tuple A list of the long description lines and the index in the long description where the last URL ended. """ _lines = [] for _url in url_idx: # Skip URL if it is simply a quoted pattern. if _patterns.do_skip_link(text, _url): continue # If the text including the URL is longer than the wrap length, # we need to split the description before the URL, wrap the pre-URL # text, and add the URL as a separate line. if len(text[text_idx : _url[1]]) > (wrap_length - len(indentation)): # Wrap everything in the description before the first URL. _lines.extend( _strings.description_to_list( text[text_idx : _url[0]], indentation, wrap_length, ) ) with contextlib.suppress(IndexError): if text[_url[0] - len(indentation) - 2] != "\n" and not _lines[-1]: _lines.pop(-1) # Add the URL making sure that the leading quote is kept with a quoted URL. _text = f"{text[_url[0]: _url[1]]}" with contextlib.suppress(IndexError): if _lines[0][-1] == '"': _lines[0] = _lines[0][:-2] _text = f'"{text[_url[0] : _url[1]]}' _lines.append(f"{_strings.do_clean_excess_whitespace(_text, indentation)}") text_idx = _url[1] return _lines, text_idx docformatter-1.7.8/tests/000077500000000000000000000000001517155121300153635ustar00rootroot00000000000000docformatter-1.7.8/tests/__init__.py000066400000000000000000000035621517155121300175020ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests._init__.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Standard Library Imports import random import string def generate_random_docstring( max_indentation_length=32, max_word_length=20, max_words=50, ): """Generate single-line docstring.""" if random.randint(0, 1): words = [] else: words = [ generate_random_word(random.randint(0, max_word_length)) for _ in range(random.randint(0, max_words)) ] indentation = random.randint(0, max_indentation_length) * " " quote = '"""' if random.randint(0, 1) else "'''" return quote + indentation + " ".join(words) + quote def generate_random_word(word_length): return "".join(random.sample(string.ascii_letters, word_length)) docformatter-1.7.8/tests/_data/000077500000000000000000000000001517155121300164335ustar00rootroot00000000000000docformatter-1.7.8/tests/_data/pyproject.toml000066400000000000000000000001151517155121300213440ustar00rootroot00000000000000[tool.docformatter] wrap-summaries = 79 wrap-descriptions = 72 blank = false docformatter-1.7.8/tests/_data/setup.cfg000066400000000000000000000001101517155121300202440ustar00rootroot00000000000000[docformatter] blank = False wrap-summaries = 79 wrap-descriptions = 72 docformatter-1.7.8/tests/_data/string_files/000077500000000000000000000000001517155121300211235ustar00rootroot00000000000000docformatter-1.7.8/tests/_data/string_files/classify_functions.toml000066400000000000000000000077741517155121300257440ustar00rootroot00000000000000[is_module_docstring] instring = ''' """This is a module docstring.""" ''' expected = true [is_class_docstring] instring = ''' class A: """Class-level docstring.""" ''' expected = true [is_method_docstring] instring = ''' def foo(): """Method docstring.""" ''' expected = true [is_function_docstring] instring = ''' def foo(): """Function docstring.""" ''' expected = true [is_attribute_docstring] instring = ''' class A: x = 1 """Attribute docstring.""" ''' expected = true [is_not_attribute_docstring] instring = ''' #!/usr/bin/env python import os from typing import Iterator """Don't remove this comment, it's cool.""" IMPORTANT_CONSTANT = "potato" ''' expected = false [is_code_line] type=1 string='x = 42\n' start='(3,10)' end='(3,20)' line='x = 42\n' expected = true [is_closing_quotes] prev_type=5 prev_string=' ' prev_start='(3,10)' prev_end='(3,40)' prev_line=' """\n' type=4 string='"""\n' start='(3,10)' end='(3,40)' line=' """\n' expected = true [is_definition_line_class] type = 1 string = 'class foo():\n' start = "(3,10)" end = "(3,25)" line = 'class foo():\n' expected = true [is_definition_line_function] type = 1 string = 'def foo():\n' start = "(3,10)" end = "(3,25)" line = 'def foo():\n' expected = true [is_definition_line_async_function] type = 1 string = 'async def foo():\n' start = "(3,10)" end = "(3,25)" line = 'async def foo():\n' expected = true [is_not_definition_line_function] type = 1 string = 'definitely\n' start = "(3,10)" end = "(3,20)" line = 'definitely\n' expected = false [is_nested_definition_line_class] type = 1 string = ' class foo():\n' start = "(3,10)" end = "(3,25)" line = ' class foo():\n' expected = true [is_nested_definition_line_function] type = 1 string = ' def foo():\n' start = "(3,10)" end = "(3,25)" line = ' def foo():\n' expected = true [is_nested_definition_line_async_function] type = 1 string = ' async def foo():\n' start = "(3,10)" end = "(3,25)" line = ' async def foo():\n' expected = true [is_not_nested_definition_line_function] type = 1 string = ' definitely\n' start = "(3,10)" end = "(3,20)" line = ' definitely\n' expected = false [is_inline_comment] type = 64 string = "# This is an inline comment" start = "(3,10)" end = "(3,30)" line = '"""This is a docstring with an inline comment""" # This is an inline comment' expected = true [is_string_variable] prev_type = 55 prev_string = 'x = """This is a string variable"""' prev_start = "(3,10)" prev_end = "(3,40)" prev_line = 'x = """This is a string variable"""' type = 3 string = 'x = """This is a string variable"""' start = "(3,10)" end = "(3,40)" line = 'x = """This is a string variable"""' expected = true [is_newline_continuation] prev_type = 3 prev_string = '"""\n' prev_start = "(3,10)" prev_end = "(3,40)" prev_line = '"""\n' type = 4 string = '\n' start = "(3,10)" end = "(3,40)" line = '"""\n' expected = true [is_line_following_indent] prev_type = 5 prev_string = ' ' prev_start = "(3,10)" prev_end = "(3,40)" prev_line = ' """\n' type = 4 string = '"""\n' start = "(3,10)" end = "(3,40)" line = ' """\n' expected = true [is_f_string] prev_type = 61 prev_string = 'f"""' prev_start = "(3,4)" prev_end = "(3,7)" prev_line = 'f"""This is an f-string with a {variable} inside."""' type = 62 string = 'This is an f-string with a {variable} inside."""' start = "(3,7)" end = "(3,55)" line = 'f"""This is an f-string with a {variable} inside."""' expected = true expected313 = false [find_module_docstring] instring = '''"""Module docstring.""" import os''' [find_class_docstring] instring = ''' class A: """Class docstring.""" pass ''' [find_function_docstring] instring = ''' def foo(): """Function docstring.""" pass ''' [find_function_docstring_with_decorator] instring = ''' @decorator def f(): """Docstring.""" pass ''' [find_attribute_docstring] instring = ''' x = 1 """Doc for x.""" ''' [find_multiple_docstrings] instring = ''' """Module.""" class A: """Class.""" def f(self): """Method.""" pass ''' docformatter-1.7.8/tests/_data/string_files/description_wrappers.toml000066400000000000000000000057411517155121300262750ustar00rootroot00000000000000[do_close_description] instring = """ http://www.google.com. This is the part of the long description that follows a URL or something like that. This stuff should get wrapped according to the rules in place at the time.""" expected = [' This is the part of the long description that follows a URL or', ' something like that. This stuff should get wrapped according to the rules in place', ' at the time.'] [do_wrap_description] instring = """ \nThis is the long description from a docstring. It follows the summary and will be wrapped at X characters, where X is the wrap_length argument to docformatter. """ expected = """ This is the long description from a docstring. It follows the summary and will be wrapped at X characters, where X is the wrap_length argument to docformatter.""" [do_wrap_description_with_doctest] instring = """ This is a long description from a docstring that contains a simple little doctest. The description shouldn't get wrapped at all. >>> print(x) >>> 42 """ expected = """ This is a long description from a docstring that contains a simple little doctest. The description shouldn't get wrapped at all. >>> print(x) >>> 42""" [do_wrap_description_with_list] instring = """ This is a long description from a docstring that will contain a list. The description shouldn't get wrapped at all. 1. Item one 2. Item two 3. Item 3 """ expected = """ This is a long description from a docstring that will contain a list. The description shouldn't get wrapped at all. 1. Item one 2. Item two 3. Item 3""" [do_wrap_description_with_heuristic_list] instring = """ This is a long description from a docstring that will contain an heuristic list. The description shouldn't get wrapped at all. Example: Item one Item two Item 3 """ expected = """ This is a long description from a docstring that will contain an heuristic list. The description shouldn't get wrapped at all. Example: Item one Item two Item 3""" [do_wrap_description_with_heuristic_list_force_wrap] instring = """ This is a long description from a docstring that will contain an heuristic list and is passed force_wrap = True. The list portion of the description should also get wrapped. Example: Item one Item two Item 3 Item 4 Item 5 Item 6 """ expected = """ This is a long description from a docstring that will contain an heuristic list and is passed force_wrap = True. The list portion of the description should also get wrapped. Example: Item one Item two Item 3 Item 4 Item 5 Item 6""" [do_wrap_description_with_directive] instring = """ This is a long docstring containing some reST directives. .. note:: This is a note in the reST dialog. """ expected = """ This is a long docstring containing some reST directives. .. note:: This is a note in the reST dialog.""" docformatter-1.7.8/tests/_data/string_files/do_format_code.toml000066400000000000000000000633431517155121300247750ustar00rootroot00000000000000[one_line] source='''def foo(): """ Hello foo. """''' expected='''def foo(): """Hello foo.""" ''' [module_docstring] source='''#!/usr/env/bin python """This is a module docstring. 1. One 2. Two """ """But this is not."""''' expected='''#!/usr/env/bin python """This is a module docstring. 1. One 2. Two """ """But this is not."""''' [newline_module_variable] source=''' CONST = 123 """docstring for CONST.""" ''' expected=''' CONST = 123 """Docstring for CONST.""" ''' [class_docstring] source=''' class TestClass: """This is a class docstring. :cvar test_int: a class attribute. .. py:method:: big_method() """ ''' expected=''' class TestClass: """This is a class docstring. :cvar test_int: a class attribute. .. py:method:: big_method() """ ''' [newline_class_variable] source=''' class TestClass: """This is a class docstring.""" test_var = 0 """This is a class variable docstring.""" test_var2 = 1 """This is a second class variable docstring.""" ''' expected=''' class TestClass: """This is a class docstring.""" test_var = 0 """This is a class variable docstring.""" test_var2 = 1 """This is a second class variable docstring.""" ''' [class_attribute_wrap] source='''class TestClass: """This is a class docstring.""" test_int = 1 """This is a very, very, very long docstring that should really be reformatted nicely by docformatter."""''' expected='''class TestClass: """This is a class docstring.""" test_int = 1 """This is a very, very, very long docstring that should really be reformatted nicely by docformatter.""" ''' [newline_outside_docstring] source=''' def new_function(): """Description of function.""" found = next( (index for index, line in enumerate(split) if line.strip()), 0 ) return "\n".join(split[found:]) ''' expected=''' def new_function(): """Description of function.""" found = next( (index for index, line in enumerate(split) if line.strip()), 0 ) return "\n".join(split[found:]) ''' [preserve_line_ending] source=''' def foo():\r """\r Hello foo. This is a docstring.\r """\r ''' expected='''def foo():\r """\r Hello foo. This is a docstring.\r """\r ''' [non_docstring] source='''x = """This is not a docstring."""''' expected='''x = """This is not a docstring."""''' [tabbed_indentation] source='''def foo(): """ Hello foo. """ if True: x = 1''' expected='''def foo(): """Hello foo.""" if True: x = 1''' [mixed_indentation] source='''def foo(): """ Hello foo. """ if True: x = 1''' expected='''def foo(): """Hello foo.""" if True: x = 1''' [escaped_newlines] source='''def foo(): """ Hello foo. """ x =\ 1''' expected='''def foo(): """Hello foo.""" x =\ 1''' # Python 3.13+ seems to handle this differently. expected313='''def foo(): """Hello foo.""" x =\ 1''' [code_comments] source='''def foo(): """ Hello foo. """ # My comment # My comment with escape \ 123''' expected='''def foo(): """Hello foo.""" # My comment # My comment with escape \ 123''' [inline_comment] source='''def foo(): """ Hello foo. """ def test_method_no_chr_92(): the501(92) # Comment''' expected='''def foo(): """Hello foo.""" def test_method_no_chr_92(): the501(92) # Comment''' [raw_lowercase] source='''def foo(): r""" Hello raw foo. """''' expected='''def foo(): r"""Hello raw foo.""" ''' [raw_uppercase] source='''def foo(): R""" Hello Raw foo. """''' expected='''def foo(): R"""Hello Raw foo.""" ''' [raw_lowercase_single] source="""def foo(): r''' Hello raw foo. '''""" expected='''def foo(): r"""Hello raw foo.""" ''' [raw_uppercase_single] source="""def foo(): R''' Hello Raw foo. '''""" expected='''def foo(): R"""Hello Raw foo.""" ''' [unicode_lowercase] source='''def foo(): u""" Hello unicode foo. """''' expected='''def foo(): u"""Hello unicode foo.""" ''' [unicode_uppercase] source='''def foo(): U""" Hello Unicode foo. """''' expected='''def foo(): U"""Hello Unicode foo.""" ''' [unicode_lowercase_single] source="""def foo(): u''' Hello unicode foo. '''""" expected='''def foo(): u"""Hello unicode foo.""" ''' [unicode_uppercase_single] source="""def foo(): U''' Hello Unicode foo. '''""" expected='''def foo(): U"""Hello Unicode foo.""" ''' [nested_triple] source="""def foo(): '''Hello foo. \"\"\"abc\"\"\" '''""" expected="""def foo(): '''Hello foo. \"\"\"abc\"\"\" ''' """ [multiple_sentences] source='''def foo(): """ Hello foo. This is a docstring. """''' expected='''def foo(): """Hello foo. This is a docstring. """ ''' [multiple_sentences_same_line] source='''def foo(): """ Hello foo. This is a docstring. """''' expected='''def foo(): """Hello foo. This is a docstring. """ ''' [multiline_summary] source='''def foo(): """ Hello foo. This is a docstring. """''' expected='''def foo(): """Hello foo. This is a docstring. """ ''' [empty_lines] source='''def foo(): """ Hello foo and this is a docstring. More stuff. """''' expected='''def foo(): """Hello foo and this is a docstring. More stuff. """ ''' [class_empty_lines] source='''class Foo: """ Hello foo and this is a docstring. More stuff. """''' expected='''class Foo: """Hello foo and this is a docstring. More stuff. """ ''' [class_empty_lines_2] source='''def foo(): class Foo: """Summary.""" pass''' expected='''def foo(): class Foo: """Summary.""" pass''' [method_empty_lines] source='''class Foo: def foo(self): """Summary.""" pass''' expected='''class Foo: def foo(self): """Summary.""" pass''' [trailing_whitespace] source='''def foo(): """ Hello foo and this is a docstring. More stuff. """''' expected='''def foo(): """Hello foo and this is a docstring. More stuff. """ ''' [parameter_list] source='''def foo(): """Test one - first two - second """''' expected='''def foo(): """Test. one - first two - second """ ''' [single_quote] source="""def foo(): 'Just a regular string' """ expected="""def foo(): 'Just a regular string' """ [double_quote] source="""def foo(): "Just a regular string" """ expected="""def foo(): "Just a regular string" """ [nested_triple_quote] source='''def foo(): 'Just a """foo""" string' ''' expected='''def foo(): 'Just a """foo""" string' ''' [first_line_assignment] source='''def foo(): x = """Just a regular string. Alpha.""" ''' expected='''def foo(): x = """Just a regular string. Alpha.""" ''' [regular_strings] source='''def foo(): """ Hello foo and this is a docstring. More stuff. """ x = """My non-docstring This should not be touched.""" """More stuff that should not be touched """''' expected='''def foo(): """Hello foo and this is a docstring. More stuff. """ x = """My non-docstring This should not be touched.""" """More stuff that should not be touched """''' [syntax_error] source='''""" ''' expected='''""" ''' [slash_r] source='''"""\r''' expected='''"""\r''' [slash_r_slash_n] source='''"""\r\n''' expected='''"""\r\n''' [strip_blank_lines] source=''' class TestClass: """This is a class docstring.""" class_attribute = 1 def test_method_1(self): """This is a method docstring. With no blank line after it. """ pass def test_method_2(self): """This is a method docstring. With a long description followed by multiple blank lines. """ pass''' expected=''' class TestClass: """This is a class docstring.""" class_attribute = 1 def test_method_1(self): """This is a method docstring. With no blank line after it. """ pass def test_method_2(self): """This is a method docstring. With a long description followed by multiple blank lines. """ pass''' [range_miss] source=''' def f(x): """ This is a docstring. That should be on more lines""" pass def g(x): """ Badly indented docstring""" pass''' expected=''' def f(x): """ This is a docstring. That should be on more lines""" pass def g(x): """ Badly indented docstring""" pass''' [range_hit] source=''' def f(x): """ This is a docstring. That should be on more lines""" pass def g(x): """ Badly indented docstring""" pass''' expected=''' def f(x): """This is a docstring. That should be on more lines """ pass def g(x): """ Badly indented docstring""" pass''' [length_ignore] source=''' def f(x): """This is a docstring. That should be on less lines """ pass def g(x): """ Badly indented docstring""" pass''' expected=''' def f(x): """This is a docstring. That should be on less lines """ pass def g(x): """Badly indented docstring.""" pass''' [issue_51] source='''def my_func(): """Summary of my function.""" pass''' expected='''def my_func(): """Summary of my function.""" pass''' [issue_51_2] source=''' def crash_rocket(location): # pragma: no cover """This is a docstring following an in-line comment.""" return location''' expected=''' def crash_rocket(location): # pragma: no cover """This is a docstring following an in-line comment.""" return location''' [issue_79] source='''def function2(): """Hello yeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeet -v."""''' expected='''def function2(): """Hello yeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeet -v.""" ''' [issue_97] source='''def pytest_addoption(parser: pytest.Parser) -> " None: register_toggle.pytest_addoption(parser) ''' expected='''def pytest_addoption(parser: pytest.Parser) -> " None: register_toggle.pytest_addoption(parser) ''' [issue_97_2] source='''def pytest_addoption(parser: pytest.Parser) -> None: # pragma: no cover register_toggle.pytest_addoption(parser) ''' expected='''def pytest_addoption(parser: pytest.Parser) -> None: # pragma: no cover register_toggle.pytest_addoption(parser) ''' [issue_130] source=''' class TestClass: """This is a class docstring.""" def test_method(self): """This is a method docstring. With a long description followed by two blank lines. """ pass ''' expected=''' class TestClass: """This is a class docstring.""" def test_method(self): """This is a method docstring. With a long description followed by two blank lines. """ pass ''' [issue_139] source=''' class TestClass: """This is a class docstring. :cvar test_int: a class attribute. ..py:method:: big_method() """ ''' expected=''' class TestClass: """This is a class docstring. :cvar test_int: a class attribute. ..py:method:: big_method() """ ''' [issue_139_2] source=""" class TestClass: variable = 1 """ expected=""" class TestClass: variable = 1 """ [issue_156] source=''' def test_wps3_process_step_io_data_or_href(): """Validates that \'data\' literal values and \'href\' file references are both handled as input for workflow steps corresponding to a WPS-3 process.""" def mock_wps_request(method, url, *_, **kwargs): nonlocal test_reached_parse_inputs method = method.upper() ''' expected=''' def test_wps3_process_step_io_data_or_href(): """Validates that \'data\' literal values and \'href\' file references are both handled as input for workflow steps corresponding to a WPS-3 process.""" def mock_wps_request(method, url, *_, **kwargs): nonlocal test_reached_parse_inputs method = method.upper() ''' [issue_156_2] source='''class AcceptHeader(ExtendedSchemaNode): # ok to use name in this case because target key in the mapping must # be that specific value but cannot have a field named with this format name = "Accept" schema_type = String missing = drop default = ContentType.APP_JSON # defaults to JSON for easy use within browsers class AcceptLanguageHeader(ExtendedSchemaNode): # ok to use name in this case because target key in the mapping must # be that specific value but cannot have a field named with this format name = "Accept-Language" schema_type = String missing = drop default = AcceptLanguage.EN_CA # FIXME: oneOf validator for supported languages (?)''' expected='''class AcceptHeader(ExtendedSchemaNode): # ok to use name in this case because target key in the mapping must # be that specific value but cannot have a field named with this format name = "Accept" schema_type = String missing = drop default = ContentType.APP_JSON # defaults to JSON for easy use within browsers class AcceptLanguageHeader(ExtendedSchemaNode): # ok to use name in this case because target key in the mapping must # be that specific value but cannot have a field named with this format name = "Accept-Language" schema_type = String missing = drop default = AcceptLanguage.EN_CA # FIXME: oneOf validator for supported languages (?)''' [issue_156_173] source=''' class Foo: @abstractmethod def bar(self): """This is a description.""" @abstractmethod def baz(self): """This is a second description.""" ''' expected=''' class Foo: @abstractmethod def bar(self): """This is a description.""" @abstractmethod def baz(self): """This is a second description.""" ''' [issue_157_7] source='''def hanging_rest_link(): """ `Source of this snippet `_. """ def sub_func_test(): def long_line_link(): """Get the Python type of a Click parameter. See the list of `custom types provided by Click `_. """''' expected='''def hanging_rest_link(): """ `Source of this snippet `_. """ def sub_func_test(): def long_line_link(): """Get the Python type of a Click parameter. See the list of `custom types provided by Click `_. """ ''' [issue_157_8] source='''def mixed_links(): """Implements the minimal code necessary to locate and call the ``mpm`` CLI on the system. Once ``mpm`` is located, we can rely on it to produce the main output of the plugin. The output must supports both `Xbar dialect `_ and `SwiftBar dialect `_. """ XKCD_MANAGER_ORDER = ("pip", "brew", "npm", "dnf", "apt", "steamcmd") """Sequence of package managers as defined by `XKCD #1654: Universal Install Script `_. See the corresponding :issue:`implementation rationale in issue #10 <10>`. """ HASH_HEADERS = ( "Date", "From", "To", ) """ Default ordered list of headers to use to compute the unique hash of a mail. By default we choose to exclude: ``Cc`` Since ``mailman`` apparently `sometimes trims list members `_ from the ``Cc`` header to avoid sending duplicates. Which means that copies of mail reflected back from the list server will have a different ``Cc`` to the copy saved by the MUA at send-time. ``Bcc`` Because copies of the mail saved by the MUA at send-time will have ``Bcc``, but copies reflected back from the list server won't. ``Reply-To`` Since a mail could be ``Cc``'d to two lists with different ``Reply-To`` munging options set. """''' expected='''def mixed_links(): """Implements the minimal code necessary to locate and call the ``mpm`` CLI on the system. Once ``mpm`` is located, we can rely on it to produce the main output of the plugin. The output must supports both `Xbar dialect `_ and `SwiftBar dialect `_. """ XKCD_MANAGER_ORDER = ("pip", "brew", "npm", "dnf", "apt", "steamcmd") """Sequence of package managers as defined by `XKCD #1654: Universal Install Script `_. See the corresponding :issue:`implementation rationale in issue #10 <10>`. """ HASH_HEADERS = ( "Date", "From", "To", ) """Default ordered list of headers to use to compute the unique hash of a mail. By default we choose to exclude: ``Cc`` Since ``mailman`` apparently `sometimes trims list members `_ from the ``Cc`` header to avoid sending duplicates. Which means that copies of mail reflected back from the list server will have a different ``Cc`` to the copy saved by the MUA at send-time. ``Bcc`` Because copies of the mail saved by the MUA at send-time will have ``Bcc``, but copies reflected back from the list server won't. ``Reply-To`` Since a mail could be ``Cc``'d to two lists with different ``Reply-To`` munging options set. """ ''' [issue_157_9] source='''def load_conf(): """Fetch parameters values from configuration file and merge them with the defaults. User configuration is `merged to the context default_map as Click does `_. This allow user's config to only overrides defaults. Values sets from direct command line parameters, environment variables or interactive prompts, takes precedence over any values from the config file. """ strict_selection_match = False """ Install sub-command try each user-selected manager until it find one providing the package we seek to install, after which the process stop. This mean not all managers will be called, so we allow the CLI output checks to partially match. """ platforms = {"LINUX", "MACOS", "WSL2"} """Homebrew core is now compatible with `Linux and Windows Subsystem for Linux (WSL) 2 `_. """''' expected='''def load_conf(): """Fetch parameters values from configuration file and merge them with the defaults. User configuration is `merged to the context default_map as Click does `_. This allow user's config to only overrides defaults. Values sets from direct command line parameters, environment variables or interactive prompts, takes precedence over any values from the config file. """ strict_selection_match = False """Install sub-command try each user-selected manager until it find one providing the package we seek to install, after which the process stop. This mean not all managers will be called, so we allow the CLI output checks to partially match. """ platforms = {"LINUX", "MACOS", "WSL2"} """Homebrew core is now compatible with `Linux and Windows Subsystem for Linux (WSL) 2 `_. """ ''' [issue_157_10] source='''"""Patch and tweak `Python's standard library mail box constructors. `_ to set sane defaults. Also forces out our own message factories to add deduplication tools and utilities. """ """Patch and tweak `Python's standard library mail box constructors `_ to set sane defaults. Also forces out our own message factories to add deduplication tools and utilities. """ def generate_platforms_graph( graph_id: str, description: str, groups: frozenset ) -> str: """Generates an `Euler diagram `_ of platform and their grouping. Euler diagrams are `not supported by mermaid yet `_ so we fallback on a flowchart without arrows. Returns a ready to use and properly indented MyST block. """ def load_conf(self, ctx, param, path_pattern): """Fetch parameters values from configuration file and merge them with the defaults. User configuration is `merged to the context default_map as Click does `_. This allow user's config to only overrides defaults. Values sets from direct command line parameters, environment variables or interactive prompts, takes precedence over any values from the config file. """ def pytest_addoption(parser): """Add custom command line options. Based on `Pytest's documentation examples `_. By default, runs non-destructive tests and skips destructive ones. """''' expected='''"""Patch and tweak `Python's standard library mail box constructors. `_ to set sane defaults. Also forces out our own message factories to add deduplication tools and utilities. """ """Patch and tweak `Python's standard library mail box constructors `_ to set sane defaults. Also forces out our own message factories to add deduplication tools and utilities. """ def generate_platforms_graph( graph_id: str, description: str, groups: frozenset ) -> str: """Generates an `Euler diagram `_ of platform and their grouping. Euler diagrams are `not supported by mermaid yet `_ so we fallback on a flowchart without arrows. Returns a ready to use and properly indented MyST block. """ def load_conf(self, ctx, param, path_pattern): """Fetch parameters values from configuration file and merge them with the defaults. User configuration is `merged to the context default_map as Click does `_. This allow user's config to only overrides defaults. Values sets from direct command line parameters, environment variables or interactive prompts, takes precedence over any values from the config file. """ def pytest_addoption(parser): """Add custom command line options. Based on `Pytest's documentation examples `_. By default, runs non-destructive tests and skips destructive ones. """ ''' [issue_176] source='''def Class1: """Class.""" #noqa attribute = a """Attr.""" def Class2: """Class.""" attribute = a """Attr.""" def Class3: """Class docstring. With long description. """ #noqa attribute = a """Attr."""''' expected='''def Class1: """Class.""" #noqa attribute = a """Attr.""" def Class2: """Class.""" attribute = a """Attr.""" def Class3: """Class docstring. With long description. """ #noqa attribute = a """Attr.""" ''' [issue_176_black] source='''class C: """Class.""" #noqa attr: int """Attr."""''' expected='''class C: """Class.""" #noqa attr: int """Attr."""''' [issue_187] source=''' #!/usr/bin/env python """a.py""" ''' expected=''' #!/usr/bin/env python """a.py.""" ''' [issue_203] source=''' #!/usr/bin/env python import os from typing import Iterator """Don't remove this comment, it's cool.""" IMPORTANT_CONSTANT = "potato" ''' expected=''' #!/usr/bin/env python import os from typing import Iterator """Don't remove this comment, it's cool.""" IMPORTANT_CONSTANT = "potato" ''' [issue_243] source='''def foo(bar): """Return `foo` using `bar`. Description.""" ''' expected='''def foo(bar): """Return `foo` using `bar`. Description. """ ''' [two_lines_between_stub_classes] source='''class Foo: """Foo class.""" class Bar: """Bar class.""" ''' expected='''class Foo: """Foo class.""" class Bar: """Bar class.""" ''' [two_lines_between_stub_classes_with_preceding_comment] source='''class Foo: """Foo class.""" # A comment for class Bar class Bar: """Bar class.""" ''' expected='''class Foo: """Foo class.""" # A comment for class Bar class Bar: """Bar class.""" ''' [ellipses_is_code_line] source='''class Foo: def bar() -> str: """Bar.""" ... def baz() -> None: """Baz.""" ... ''' expected='''class Foo: def bar() -> str: """Bar.""" ... def baz() -> None: """Baz.""" ... ''' [do_not_break_f_string_double_quotes] source='''foo = f""" bar """ ''' expected='''foo = f""" bar """ ''' [do_not_break_f_string_single_quotes] source="""foo = f''' bar ''' """ expected="""foo = f''' bar ''' """ [issue_331_black_module_docstring] source='''"""A.""" pass ''' expected='''"""A.""" pass ''' docformatter-1.7.8/tests/_data/string_files/do_format_docstrings.toml000066400000000000000000001034661517155121300262430ustar00rootroot00000000000000[one_line] source='''""" Hello. """''' expected='''"""Hello."""''' [summary_end_quote] source='''""" "Hello" """''' expected='''""""Hello"."""''' [bad_indentation] source='''"""Hello. This should be indented but it is not. The next line should be indented too. And this too. """''' expected='''"""Hello. This should be indented but it is not. The next line should be indented too. And this too. """''' [too_much_indentation] source='''"""Hello. This should be dedented. 1. This too. 2. And this. 3. And this. """''' expected='''"""Hello. This should be dedented. 1. This too. 2. And this. 3. And this. """''' [trailing_whitespace] source='''"""Hello. This should be not have trailing whitespace. The next line should not have trailing whitespace either. """''' expected='''"""Hello. This should be not have trailing whitespace. The next line should not have trailing whitespace either. """''' [empty_docstring] source='''""""""''' expected='''""""""''' [no_summary_period] source='''""" Hello """''' expected='''"""Hello."""''' [single_quotes] source="""''' Hello. '''""" expected='''"""Hello."""''' [single_quotes_multiline] source="""''' Return x factorial. This uses math.factorial. '''""" expected='''"""Return x factorial. This uses math.factorial. """''' [skip_underlined_summary] source='''""" Foo bar ------- This is more. """''' expected='''""" Foo bar ------- This is more. """''' [no_blank] source='''""" Hello. Description. """''' expected='''"""Hello. Description. """''' [presummary_newline] source='''""" Hello. Description. """''' expected='''""" Hello. Description. """''' [summary_multiline] source='''"""This one-line docstring will be multi-line"""''' expected='''""" This one-line docstring will be multi-line. """''' [presummary_space] source='''"""This one-line docstring will have a leading space."""''' expected='''""" This one-line docstring will have a leading space."""''' # Examples to test when passing --black to docformatter. [quote_no_space_black] source='''""" This one-line docstring will not have a leading space."""''' expected='''"""This one-line docstring will not have a leading space."""''' [quote_space_black] source='''""""This" quote starting one-line docstring will have a leading space."""''' expected='''""" "This" quote starting one-line docstring will have a leading space."""''' [quote_space_multiline_black] source='''""""This" quote starting one-line docstring will have a leading space. This long description will be wrapped at 88 characters because we passed the --black option and 88 characters is the default wrap length. """''' expected='''""" "This" quote starting one-line docstring will have a leading space. This long description will be wrapped at 88 characters because we passed the --black option and 88 characters is the default wrap length. """''' # Examples to test with Epytext style docstrings. [epytext] source='''"""Return line-wrapped description text. We only wrap simple descriptions. We leave doctests, multi-paragraph text, and bulleted lists alone. See http://www.docformatter.com/. @param text: the text argument. @param indentation: the super long description for the indentation argument that will require docformatter to wrap this line. @param wrap_length: the wrap_length argument @param force_wrap: the force_warp argument. @return: really long description text wrapped at n characters and a very long description of the return value so we can wrap this line abcd efgh ijkl mnop qrst uvwx yz. """''' expected='''"""Return line-wrapped description text. We only wrap simple descriptions. We leave doctests, multi-paragraph text, and bulleted lists alone. See http://www.docformatter.com/. @param text: the text argument. @param indentation: the super long description for the indentation argument that will require docformatter to wrap this line. @param wrap_length: the wrap_length argument @param force_wrap: the force_warp argument. @return: really long description text wrapped at n characters and a very long description of the return value so we can wrap this line abcd efgh ijkl mnop qrst uvwx yz. """''' [epytext_numpy] source='''"""Return line-wrapped description text. We only wrap simple descriptions. We leave doctests, multi-paragraph text, and bulleted lists alone. See http://www.docformatter.com/. @param text: the text argument. @param indentation: the super long description for the indentation argument that will require docformatter to wrap this line. @param wrap_length: the wrap_length argument @param force_wrap: the force_warp argument. @return: really long description text wrapped at n characters and a very long description of the return value so we can wrap this line abcd efgh ijkl mnop qrst uvwx yz. """''' expected='''"""Return line-wrapped description text. We only wrap simple descriptions. We leave doctests, multi-paragraph text, and bulleted lists alone. See http://www.docformatter.com/. @param text: the text argument. @param indentation: the super long description for the indentation argument that will require docformatter to wrap this line. @param wrap_length: the wrap_length argument @param force_wrap: the force_warp argument. @return: really long description text wrapped at n characters and a very long description of the return value so we can wrap this line abcd efgh ijkl mnop qrst uvwx yz. """''' # Examples to test with Sphinx style docstrings. [sphinx] source='''"""Return line-wrapped description text. We only wrap simple descriptions. We leave doctests, multi-paragraph text, and bulleted lists alone. See http://www.docformatter.com/. :param str text: the text argument. :param str indentation: the super long description for the indentation argument that will require docformatter to wrap this line. :param int wrap_length: the wrap_length argument :param bool force_wrap: the force_warp argument. :return: really long description text wrapped at n characters and a very long description of the return value so we can wrap this line abcd efgh ijkl mnop qrst uvwx yz. :rtype: str """''' expected='''"""Return line-wrapped description text. We only wrap simple descriptions. We leave doctests, multi-paragraph text, and bulleted lists alone. See http://www.docformatter.com/. :param str text: the text argument. :param str indentation: the super long description for the indentation argument that will require docformatter to wrap this line. :param int wrap_length: the wrap_length argument :param bool force_wrap: the force_warp argument. :return: really long description text wrapped at n characters and a very long description of the return value so we can wrap this line abcd efgh ijkl mnop qrst uvwx yz. :rtype: str """''' [sphinx_numpy] source='''"""Return line-wrapped description text. We only wrap simple descriptions. We leave doctests, multi-paragraph text, and bulleted lists alone. See http://www.docformatter.com/. :param str text: the text argument. :param str indentation: the super long description for the indentation argument that will require docformatter to wrap this line. :param int wrap_length: the wrap_length argument :param bool force_wrap: the force_warp argument. :return: really long description text wrapped at n characters and a very long description of the return value so we can wrap this line abcd efgh ijkl mnop qrst uvwx yz. :rtype: str """''' expected='''"""Return line-wrapped description text. We only wrap simple descriptions. We leave doctests, multi-paragraph text, and bulleted lists alone. See http://www.docformatter.com/. :param str text: the text argument. :param str indentation: the super long description for the indentation argument that will require docformatter to wrap this line. :param int wrap_length: the wrap_length argument :param bool force_wrap: the force_warp argument. :return: really long description text wrapped at n characters and a very long description of the return value so we can wrap this line abcd efgh ijkl mnop qrst uvwx yz. :rtype: str """''' # Examples to test when formatting lists. [numbered_list] source='''"""Hello. 1. This should be indented but it is not. The next line should be indented too. But this is okay. """''' expected='''"""Hello. 1. This should be indented but it is not. The next line should be indented too. But this is okay. """''' [parameter_dash] source='''"""Hello. foo - This is a foo. This is a foo. This is a foo. This is a foo. This is. bar - This is a bar. This is a bar. This is a bar. This is a bar. This is. """''' expected='''"""Hello. foo - This is a foo. This is a foo. This is a foo. This is a foo. This is. bar - This is a bar. This is a bar. This is a bar. This is a bar. This is. """''' [parameter_colon] source='''"""Hello. foo: This is a foo. This is a foo. This is a foo. This is a foo. This is. bar: This is a bar. This is a bar. This is a bar. This is a bar. This is. """''' expected='''"""Hello. foo: This is a foo. This is a foo. This is a foo. This is a foo. This is. bar: This is a bar. This is a bar. This is a bar. This is a bar. This is. """''' [many_short_columns] source='''""" one two three four five six seven eight nine ten eleven """''' expected='''""" one two three four five six seven eight nine ten eleven """''' # Examples to test when formatter URLs. [inline] source='''"""This is a docstring with a link. Here is an elaborate description containing a link. `Area Under the Receiver Operating Characteristic Curve (ROC AUC) `_. """''' expected='''"""This is a docstring with a link. Here is an elaborate description containing a link. `Area Under the Receiver Operating Characteristic Curve (ROC AUC) `_. """''' [inline_short] source='''"""This is yanf with a short link. See `the link `_ for more details. """''' expected='''"""This is yanf with a short link. See `the link `_ for more details. """''' [inline_long] source='''"""Helpful docstring. A larger description that starts here. https://github.com/apache/kafka/blob/2.5/clients/src/main/java/org/apache/kafka/common/requests/DescribeConfigsResponse.java A larger description that ends here. """''' expected='''"""Helpful docstring. A larger description that starts here. https://github.com/apache/kafka/blob/2.5/clients/src/main/java/org/apache/kafka/common/requests/DescribeConfigsResponse.java A larger description that ends here. """''' [only_link] source='''""" `Source of this snippet `_. """''' expected='''""" `Source of this snippet `_. """''' # Examples to test when wrapping. [weird_punctuation] source='''"""Creates and returns four was awakens to was created tracked ammonites was the fifty, arithmetical four was pyrotechnic to pyrotechnic physicists. `four' falsified x falsified ammonites to awakens to. `created' to ancestor was four to x dynamo to was four ancestor to physicists(). """''' expected='''"""Creates and returns four was awakens to was created tracked ammonites was the fifty, arithmetical four was pyrotechnic to pyrotechnic physicists. `four' falsified x falsified ammonites to awakens to. `created' to ancestor was four to x dynamo to was four ancestor to physicists(). """''' [description_wrap] source='''"""Hello. This should be indented but it is not. The next line should be indented too. But this is okay. """''' expected='''"""Hello. This should be indented but it is not. The next line should be indented too. But this is okay. """''' [ignore_doctest] source='''"""Hello. >>> 4 4 """''' expected='''"""Hello. >>> 4 4 """''' [ignore_summary_doctest] source='''""" >>> 4 4 """''' expected='''""" >>> 4 4 """''' [same_indentation_doctest] source='''"""Foo bar bing bang. >>> tests = DocTestFinder().find(_TestClass) >>> runner = DocTestRunner(verbose=False) >>> tests.sort(key = lambda test: test.name) """''' expected='''"""Foo bar bing bang. >>> tests = DocTestFinder().find(_TestClass) >>> runner = DocTestRunner(verbose=False) >>> tests.sort(key = lambda test: test.name) """''' [force_wrap] source='''""" num_iterations is the number of updates - instead of a better definition of convergence. """''' expected='''"""num_iterations is the number of updates - instead of a better definition of convergence."""''' [summary_wrap_tab] source=''' """Some summary x x x x."""''' expected='''"""Some summary x x x x."""''' [one_line_wrap_newline] source='''"""This one-line docstring will be multi-line because it's quite long."""''' expected='''"""This one-line docstring will be multi-line because it's quite long. """''' [one_line_no_wrap] source='''"""This one-line docstring will not be wrapped and quotes will be in-line."""''' expected='''"""This one-line docstring will not be wrapped and quotes will be in-line."""''' # Add examples from docformatter issues on GitHub. [issue_75] source='''"""This is another docstring with `a link`_. .. a link: http://www.reliqual.com/wiki/how_to_use_ramstk/verification_and_validation_module/index.html. """''' expected='''"""This is another docstring with `a link`_. .. a link: http://www.reliqual.com/wiki/how_to_use_ramstk/verification_and_validation_module/index.html. """''' [issue_75_2] source='''"""This is another docstring with a link. See http://www.reliqual.com/wiki/how_to_use_ramstk/verification_and_validation_module/index.html for additional information. """''' expected='''"""This is another docstring with a link. See http://www.reliqual.com/wiki/how_to_use_ramstk/verification_and_validation_module/index.html for additional information. """''' [issue_75_3] source='''"""This is yanf with a short link. See http://www.reliaqual.com for examples. """''' expected='''"""This is yanf with a short link. See http://www.reliaqual.com for examples. """''' [issue_127] source='''"""My awesome function. This line is quite long. In fact is it longer than one hundred and twenty characters so it should be wrapped but it is not. It doesn't wrap because of this line and the blank line in between! Delete them and it will wrap. """''' expected='''"""My awesome function. This line is quite long. In fact is it longer than one hundred and twenty characters so it should be wrapped but it is not. It doesn't wrap because of this line and the blank line in between! Delete them and it will wrap. """''' [issue_140] source='''"""This is a docstring with a link that causes a wrap. See `the link `_ for more details. """''' expected='''"""This is a docstring with a link that causes a wrap. See `the link `_ for more details. """''' [issue_140_2] source='''"""Helpful docstring. A larger description that starts here. https://github.com/apache/kafka/blob/2.5/clients/src/main/java/org/apache/kafka/common/requests/DescribeConfigsResponse.java A larger description that ends here. """''' expected='''"""Helpful docstring. A larger description that starts here. https://github.com/apache/kafka/blob/2.5/clients/src/main/java/org/apache/kafka/common/requests/DescribeConfigsResponse.java A larger description that ends here. """''' [issue_140_3] source='''"""Do something. See https://www.postgresql.org/docs/current/static/role-removal.html """''' expected='''"""Do something. See https://www.postgresql.org/docs/current/static/role-removal.html """''' [issue_145] source='''""" .. _linspace API: https://numpy.org/doc/stable/reference/generated/numpy.linspace.html .. _arange API: https://numpy.org/doc/stable/reference/generated/numpy.arange.html .. _logspace API: https://numpy.org/doc/stable/reference/generated/numpy.logspace.html """''' expected='''""" .. _linspace API: https://numpy.org/doc/stable/reference/generated/numpy.linspace.html .. _arange API: https://numpy.org/doc/stable/reference/generated/numpy.arange.html .. _logspace API: https://numpy.org/doc/stable/reference/generated/numpy.logspace.html """''' [issue_150] source='''""" Translates incoming json to a processable Entity. Stackoverflow reference: """''' expected='''"""Translates incoming json to a processable Entity. Stackoverflow reference: """''' [issue_157] source='''""".. code-block:: shell-session ► apm --version apm 2.6.2 npm 6.14.13 node 12.14.1 x64 atom 1.58.0 python 2.7.16 git 2.33.0 """''' expected='''""".. code-block:: shell-session ► apm --version apm 2.6.2 npm 6.14.13 node 12.14.1 x64 atom 1.58.0 python 2.7.16 git 2.33.0 """''' [issue_157_url] source='''"""Get the Python type of a Click parameter. See the list of `custom types provided by Click `_. """''' expected='''"""Get the Python type of a Click parameter. See the list of `custom types provided by Click `_. """''' [issue_157_2] source='''"""Fetch parameters values from configuration file and merge them with the defaults. User configuration is `merged to the context default_map as Click does `_. This allow user's config to only overrides defaults. Values sets from direct command line parameters, environment variables or interactive prompts, takes precedence over any values from the config file. """''' expected='''"""Fetch parameters values from configuration file and merge them with the defaults. User configuration is `merged to the context default_map as Click does `_. This allow user's config to only overrides defaults. Values sets from direct command line parameters, environment variables or interactive prompts, takes precedence over any values from the config file. """''' [issue_157_3] source='''"""Introspects current CLI and list its parameters and metadata. .. important:: Click doesn't keep a list of all parsed arguments and their origin. So we need to emulate here what's happening during CLI invokation. But can't even to that because the raw, pre-parsed arguments are not available anywhere. """''' expected='''"""Introspects current CLI and list its parameters and metadata. .. important:: Click doesn't keep a list of all parsed arguments and their origin. So we need to emulate here what's happening during CLI invokation. But can't even to that because the raw, pre-parsed arguments are not available anywhere. """''' [issue_157_4] source='''"""Search on local file system or remote URL files matching the provided pattern. ``pattern`` is considered as an URL only if it is parseable as such and starts with ``http://`` or ``https://``. .. important:: This is a straight `copy of the functools.cache implementation `_, which is only `available in the standard library starting with Python v3.9 `. """''' expected='''"""Search on local file system or remote URL files matching the provided pattern. ``pattern`` is considered as an URL only if it is parseable as such and starts with ``http://`` or ``https://``. .. important:: This is a straight `copy of the functools.cache implementation `_, which is only `available in the standard library starting with Python v3.9 `. """''' [issue_157_5] source='''"""Locate and call the ``mpm`` CLI. The output must supports both `Xbar dialect `_ and `SwiftBar dialect `_. """''' expected='''"""Locate and call the ``mpm`` CLI. The output must supports both `Xbar dialect `_ and `SwiftBar dialect `_. """''' [issue_157_6] source='''"""Install one or more packages. Installation will proceed first with packages unambiguously tied to a manager. You can have an influence on that with more precise package specifiers (like purl) and/or tighter selection of managers. For other untied packages, mpm will try to find the best manager to install it with. Their installation will be attempted with each manager, in the order they were selected. If we have the certainty, by the way of a search operation, that this package is not available from this manager, we'll skip the installation and try the next available manager. """''' expected='''"""Install one or more packages. Installation will proceed first with packages unambiguously tied to a manager. You can have an influence on that with more precise package specifiers (like purl) and/or tighter selection of managers. For other untied packages, mpm will try to find the best manager to install it with. Their installation will be attempted with each manager, in the order they were selected. If we have the certainty, by the way of a search operation, that this package is not available from this manager, we'll skip the installation and try the next available manager. """''' [issue_157_11] source='''"""Fetch parameters values from configuration file and merge them with the defaults. User configuration is `merged to the context default_map as Click does `_. This allow user's config to only overrides defaults. Values sets from direct command line parameters, environment variables or interactive prompts, takes precedence over any values from the config file. """''' expected='''"""Fetch parameters values from configuration file and merge them with the defaults. User configuration is `merged to the context default_map as Click does `_. This allow user's config to only overrides defaults. Values sets from direct command line parameters, environment variables or interactive prompts, takes precedence over any values from the config file. """''' [issue_159] source='''"""Blah blah. This will normally be used with https://aaaaaaaa.bbb.ccccccccc.com/xxxxx/xxx_xxxxxxxxxxx to generate the xxx """''' expected='''"""Blah blah. This will normally be used with https://aaaaaaaa.bbb.ccccccccc.com/xxxxx/xxx_xxxxxxxxxxx to generate the xxx """''' [issue_180] source='''"""Django settings for webapp project. Generated by 'django-admin startproject' using Django 4.1.1. For more information on this file, see https://docs.djangoproject.com/en/4.1/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.1/ref/settings/ """''' expected='''"""Django settings for webapp project. Generated by 'django-admin startproject' using Django 4.1.1. For more information on this file, see https://docs.djangoproject.com/en/4.1/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.1/ref/settings/ """''' [issue_189] source='''"""This method doesn't do anything. https://example.com/this-is-just-a-long-url/designed-to-trigger/the-wrapping-of-the-description """''' expected='''"""This method doesn't do anything. https://example.com/this-is-just-a-long-url/designed-to-trigger/the-wrapping-of-the-description """''' [issue_193] source='''""" eBay kinda suss """''' expected='''"""eBay kinda suss."""''' [issue_199] source='''""" This is a short desription. Here is a link to the github issue https://github.com/PyCQA/docformatter/issues/199 This is a long description. """''' expected='''"""This is a short desription. Here is a link to the github issue https://github.com/PyCQA/docformatter/issues/199 This is a long description. """''' [issue_210] source='''"""Short description. This graphics format generates terminal escape codes that transfer PNG data to a TTY using the `kitty graphics protocol`__. __ https://sw.kovidgoyal.net/kitty/graphics-protocol/ """''' expected='''"""Short description. This graphics format generates terminal escape codes that transfer PNG data to a TTY using the `kitty graphics protocol`__. __ https://sw.kovidgoyal.net/kitty/graphics-protocol/ """''' [issue_215] source='''"""Create or return existing HTTP session. :return: Requests :class:`~requests.Session` object """''' expected='''"""Create or return existing HTTP session. :return: Requests :class:`~requests.Session` object """''' [issue_217_222] source='''"""Base for all Commands. :param logger: Logger for console and logfile. :param console: Facilitates console interaction and input solicitation. :param tools: Cache of tools populated by Commands as they are required. :param apps: Dictionary of project's Apps keyed by app name. :param base_path: Base directory for Briefcase project. :param data_path: Base directory for Briefcase tools, support packages, etc. :param is_clone: Flag that Command was triggered by the user's requested Command; for instance, RunCommand can invoke UpdateCommand and/or BuildCommand. """''' expected='''"""Base for all Commands. :param logger: Logger for console and logfile. :param console: Facilitates console interaction and input solicitation. :param tools: Cache of tools populated by Commands as they are required. :param apps: Dictionary of project's Apps keyed by app name. :param base_path: Base directory for Briefcase project. :param data_path: Base directory for Briefcase tools, support packages, etc. :param is_clone: Flag that Command was triggered by the user's requested Command; for instance, RunCommand can invoke UpdateCommand and/or BuildCommand. """''' [issue_218] source='''"""Construct a candidate project URL from the bundle and app name. It's not a perfect guess, but it's better than having "https://example.com". :param bundle: The bundle identifier. :param app_name: The app name. :returns: The candidate project URL """''' expected='''"""Construct a candidate project URL from the bundle and app name. It's not a perfect guess, but it's better than having "https://example.com". :param bundle: The bundle identifier. :param app_name: The app name. :returns: The candidate project URL """''' [issue_224] source='''""" Add trackers to a torrent. :raises NotFound404Error: :param torrent_hash: hash for torrent :param urls: tracker URLs to add to torrent :return: None """''' expected='''"""Add trackers to a torrent. :raises NotFound404Error: :param torrent_hash: hash for torrent :param urls: tracker URLs to add to torrent :return: None """''' [issue_228] source='''"""Configure application requirements by writing a requirements.txt file. :param app: The app configuration :param requires: The full list of requirements :param requirements_path: The full path to a requirements.txt file that will be written. """''' expected='''"""Configure application requirements by writing a requirements.txt file. :param app: The app configuration :param requires: The full list of requirements :param requirements_path: The full path to a requirements.txt file that will be written. """''' [issue_229] source='''"""CC. :meth:`!X` """''' expected='''"""CC. :meth:`!X` """''' [issue_229_2] source='''"""CC. :math: `-` """''' expected='''"""CC. :math: `-` """''' [issue_230] source='''"""CC. :math:`-` :param d: blabla :param list(str) l: more blabla. """''' expected= '''"""CC. :math:`-` :param d: blabla :param list(str) l: more blabla. """''' [issue_232] source='''def function: """ :param x: X :param y: Y """''' expected='''def function: """ :param x: X :param y: Y """''' [issue_234] source=''' """CC. :math:`f(0) = 1`. XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXX """''' expected='''"""CC. :math:`f(0) = 1`. XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXX """''' [issue_235] source='''"""CC. C. C, :math:`[0, 1]`. """''' expected='''"""CC. C. C, :math:`[0, 1]`. """''' [issue_239] source='''"""CC. C. C c :math:`[0, 1]`. """''' expected='''"""CC. C. C c :math:`[0, 1]`. """''' [issue_239_sphinx] source='''""" Summary. :raises InvalidRequest400Error: :raises NotFound404Error: :raises Conflict409Error: :param param: asdf """''' expected='''"""Summary. :raises InvalidRequest400Error: :raises NotFound404Error: :raises Conflict409Error: :param param: asdf """''' [issue_245] source='''"""Some f. :param a: Some param. :raises my.package.MyReallySrsError: Bad things happened. """''' expected='''"""Some f. :param a: Some param. :raises my.package.MyReallySrsError: Bad things happened. """''' [issue_250] source=''' """CC. c. c c :math:`[0, 1]`. """''' expected='''"""CC. c. c c :math:`[0, 1]`. """''' [issue_253] source='''""" My test fixture. :param caplog: Pytest caplog fixture. :yield: Until test complete, then run cleanup. """''' expected='''""" My test fixture. :param caplog: Pytest caplog fixture. :yield: Until test complete, then run cleanup. """''' [issue_259] source = '''""" "xxxxxxxxxxxxx". blah. """''' expected = '''""""xxxxxxxxxxxxx". blah. """''' [issue_259_black] source = '''""" "xxxxxxxxxxxxx". blah. """''' expected = '''""" "xxxxxxxxxxxxx". blah. """''' [issue_259_pre_summary_space] source = '''""" "xxxxxxxxxxxxx". blah. """''' expected = '''""" "xxxxxxxxxxxxx". blah. """''' [issue_259_pre_summary_newline] source = '''""" "xxxxxxxxxxxxx". blah. """''' expected = '''""" "xxxxxxxxxxxxx". blah. """''' [issue_263_sphinx] # the `xx.\n\n` ensures there are a summary and a description sections # the `:param a:` creates a field # the `b`s create text that is long enough to trigger a line wrap without being so long that they count as code # the `s3://cccc.` is a url source='''"""xx. :param a: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb s3://cccc. """''' expected='''"""xx. :param a: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb s3://cccc. """''' [issue_263_epytext] source='''"""xx. @param a: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb s3://cccc. """''' expected='''"""xx. @param a: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb s3://cccc. """''' [issue_271] source='''""" My test fixture. :ivar id: A unique identifier for the element, automatically generated upon instantiation. :vartype id: str :ivar created: Timestamp when the element was created, defaults to the current time. :vartype created: datetime :cvar modified: Timestamp when the element was last modified, can be None if not modified. :vartype modified: Optional[datetime] :cvar in_project: List of projects this element is part of. Direct modification is restricted. :vartype in_project: list[Project] :param caplog: Pytest caplog fixture. :yield: Until test complete, then run cleanup. """''' expected='''""" My test fixture. :ivar id: A unique identifier for the element, automatically generated upon instantiation. :vartype id: str :ivar created: Timestamp when the element was created, defaults to the current time. :vartype created: datetime :cvar modified: Timestamp when the element was last modified, can be None if not modified. :vartype modified: Optional[datetime] :cvar in_project: List of projects this element is part of. Direct modification is restricted. :vartype in_project: list[Project] :param caplog: Pytest caplog fixture. :yield: Until test complete, then run cleanup. """''' docformatter-1.7.8/tests/_data/string_files/encoding_functions.toml000066400000000000000000000014161517155121300257000ustar00rootroot00000000000000[find_newline_only_cr] instring = ["print 1\r", "print 2\r", "print3\r"] expected = "\r" [find_newline_only_lf] instring = ["print 1\n", "print 2\n", "print3\n"] expected = "\n" [find_newline_only_crlf] instring = ["print 1\r\n", "print 2\r\n", "print3\r\n"] expected = "\r\n" [find_newline_cr1_and_lf2] instring = ["print 1\n", "print 2\r", "print3\n"] expected = "\n" [find_newline_cr1_and_crlf2] instring = ["print 1\r\n", "print 2\r", "print3\r\n"] expected = "\r\n" [find_newline_should_default_to_lf_empty] instring = [] expected = "\n" [find_newline_should_default_to_lf_blank] instring = ["", ""] expected = "\n" [find_dominant_newline] instring = ['def foo():\r\n', ' """\r\n', ' Hello\r\n', ' foo. This is a docstring.\r\n', ' """\r\n'] expected = "\n" docformatter-1.7.8/tests/_data/string_files/field_patterns.toml000066400000000000000000000015141517155121300250240ustar00rootroot00000000000000[is_epytext_field_list] instring = """@param param1: Description of param1\n @return: Description of return value\n""" style = "epytext" expected = true [is_sphinx_field_list_epytext_style] instring = """:param param1: Description of param1\n :return: Description of return value\n""" style = "epytext" expected = false [is_sphinx_field_list] instring = """:param param1: Description of param1\n :return: Description of return value\n""" style = "sphinx" expected = true [is_epytext_field_list_sphinx_style] instring = """@param param1: Description of param1\n @return: Description of return value\n""" style = "sphinx" expected = false [is_numpy_field_list] instring = """Parameters\n ----------\n param1 : type\n Description of param1\n Returns\n -------\n type\n Description of return value\n""" style = "numpy" expected = false docformatter-1.7.8/tests/_data/string_files/field_wrappers.toml000066400000000000000000000102101517155121300250200ustar00rootroot00000000000000[do_join_field_body] instring = """ We only wrap simple descriptions. We leave doctests, multi-paragraph text, and bulleted lists alone. See http://www.docformatter.com/. :param str text: the text argument. :param str indentation: the super long description for the indentation argument that will require docformatter to wrap this line. :param int wrap_length: the wrap_length argument. :param bool force_wrap: the force_warp argument. :return: really long description text wrapped at n characters and a very long description of the return value so we can wrap this line abcd efgh ijkl mnop qrst uvwx yz. :rtype: str """ expected = " the text argument." [do_join_field_body_2] instring = """ We only wrap simple descriptions. We leave doctests, multi-paragraph text, and bulleted lists alone. See http://www.docformatter.com/. :param str text: the text argument. :param str indentation: the super long description for the indentation argument that will require docformatter to wrap this line. :param int wrap_length: the wrap_length argument. :param bool force_wrap: the force_warp argument. :return: really long description text wrapped at n characters and a very long description of the return value so we can wrap this line abcd efgh ijkl mnop qrst uvwx yz. :rtype: str """ expected = " the super long description for the indentation argument that will require docformatter to wrap this line." [do_join_field_body_3] instring = """ We only wrap simple descriptions. We leave doctests, multi-paragraph text, and bulleted lists alone. See http://www.docformatter.com/. :param str text: the text argument. :param str indentation: the super long description for the indentation argument that will require docformatter to wrap this line. :param int wrap_length: the wrap_length argument. :param bool force_wrap: the force_warp argument. :return: really long description text wrapped at n characters and a very long description of the return value so we can wrap this line abcd efgh ijkl mnop qrst uvwx yz. :rtype: str """ expected = " the wrap_length argument." [do_wrap_field] instring = [":param bundle:", " The bundle identifier."] expected = [" :param bundle: The bundle identifier."] [do_wrap_long_field] instring = [":param long:", " A very long description of a parameter that is going to need to be wrapped at 72 characters or else."] expected = [" :param long: A very long description of a parameter that is going to", " need to be wrapped at 72 characters or else."] [do_wrap_field_list] instring = """ We only wrap simple descriptions. We leave doctests, multi-paragraph text, and bulleted lists alone. See http://www.docformatter.com/. :param str text: the text argument. :param str indentation: the super long description for the indentation argument that will require docformatter to wrap this line. :param int wrap_length: the wrap_length argument :param bool force_wrap: the force_warp argument. :return: really long description text wrapped at n characters and a very long description of the return value so we can wrap this line abcd efgh ijkl mnop qrst uvwx yz. :rtype: str """ lines = [" We only wrap simple descriptions. We leave doctests, multi-paragraph text, and", " bulleted lists alone. See", " http://www.docformatter.com/."] expected = [ [" We only wrap simple descriptions. We leave doctests, multi-paragraph text, and", " bulleted lists alone. See", " http://www.docformatter.com/.", "", " :param str text: the text argument.", " :param str indentation: the super long description for the", " indentation argument that will require docformatter to wrap this", " line.", " :param int wrap_length: the wrap_length argument", " :param bool force_wrap: the force_warp argument.", " :return: really long description text wrapped at n characters and a", " very long description of the return value so we can wrap this", " line abcd efgh ijkl mnop qrst uvwx yz.", " :rtype: str"], 606] docformatter-1.7.8/tests/_data/string_files/format_functions.toml000066400000000000000000000140511517155121300254010ustar00rootroot00000000000000# In this file, token lists have the following information: # [type, string, start, end, line] # for creating a TokenInfo() object. [module_docstring_followed_by_string] expected = 1 [module_docstring_followed_by_code] expected = 1 [module_docstring_followed_by_comment_then_code] expected = 1 [module_docstring_followed_by_comment_then_string] expected = 1 [module_docstring_in_black] expected = 1 [class_docstring_followed_by_statement] source = ''' class MyClass: """Class docstring.""" x = 42 ''' expected = 1 [class_docstring_followed_by_def] source = ''' class MyClass: """Class docstring.""" def method(self): pass ''' expected = 1 [class_docstring_with_decorator] source = ''' class A: """Docstring.""" @classmethod def foo(cls): pass ''' expected = 1 [class_docstring_with_class_variable] source = ''' class A: """Docstring.""" version = "1.0" ''' expected = 1 [function_with_expr] source = ''' def foo(): """Docstring.""" return 42 ''' expected = 0 [function_with_inner_def] source = ''' def foo(): """Docstring.""" def inner(): pass ''' expected = 1 [function_with_inner_async_def] source='''def foo(): """ Hello foo. This is a docstring. """ async def inner(): pass ''' expected = 1 [function_with_decorator_and_def] source = ''' def foo(): """Docstring.""" @staticmethod def inner(): pass ''' expected = 1 [function_with_decorator_and_async_def] source = ''' def foo(): """Docstring.""" @log async def inner(): pass ''' expected = 1 [function_docstring_with_inner_class] source = ''' def foo(): """Function docstring.""" class Bar: pass ''' expected = 1 [attribute_docstring_single_line] source = '''class MyClass: x = 1 """"This is an attribute.""" ''' expected = 1 [attribute_docstring_multi_line] source = '''class MyClass: x = 1 """"This is an attribute. It has a multi-line docstring. """ ''' expected = 1 [attribute_docstring_outside_class] source = '''x = 1 """This is an attribute outside a class.""" class MyClass: pass ''' expected = 2 [attribute_docstring_inside_method] source = '''class MyClass: def method(self): x = 1 """This is an attribute inside a method.""" ''' expected = 1 [attribute_docstring_with_comment] source = '''class MyClass: x = 1 """This is an attribute.""" # This is a comment. ''' expected = 1 [attribute_docstring_multiple_assignments] source = '''class MyClass: x = y = 2 """This is an attribute with multiple assignments.""" ''' expected = 1 [attribute_docstring_equiv_expr] source = '''class MyClass: x = 1 """This is an attribute.""" y = x + 1 ''' expected = 1 [get_newlines_by_type_module_docstring] source = '"""Module docstring."""' expected = 0 [get_newlines_by_type_module_docstring_black] source = '"""Module docstring."""' expected = 0 [get_newlines_by_type_class_docstring] source = ''' class MyClass: """Class docstring.""" x = 42 ''' expected = 1 [get_newlines_by_type_function_docstring] source = ''' def foo(): """Docstring.""" return 42 ''' expected = 0 [get_newlines_by_type_attribute_docstring] source = '''x = 1 """Docstring for x.""" ''' expected = 0 [get_num_rows_columns] token = [5, " ", [3, 10], [3, 40], ''' This is the last line in the docstring.""" '''] expected = [3, 17] [get_start_end_indices] prev_token = [ 3, '''"""Hello foo and this is a docstring.\n\n More stuff.\n """''', [2, 4], [2, 7], ''' """Hello foo and this is a docstring.\n\n More stuff.\n """\n'''] token = [ 4, "\n", [7, 7], [7, 8], ''' """Hello foo and this is a docstring.\n\n More stuff.\n """\n'''] expected = [[2, 7], [2, 8]] [do_remove_preceding_blank_lines_module] source = '''#!/usr/bin/env python """This is a module docstring.""" ''' expected = ["#!/usr/bin/env python", "\n", "\n", '"""This is a module docstring."""', "\n", "", ] [do_remove_preceding_blank_lines_class] source = ''' class TestClass: """This is a class docstring. :cvar test_int: a class attribute. ..py.method: big_method() """ ''' expected = [" ", "class", "TestClass", ":", "\n", " ", '''"""This is a class docstring. :cvar test_int: a class attribute. ..py.method: big_method() """''', "\n", "\n", "", "", "", ] [do_remove_preceding_blank_lines_function] source = ''' def test_function(): """This is a function docstring.""" pass ''' expected = ["def", "test_function", "(", ")", ":", "\n", '"""This is a function docstring."""', "\n", "pass", "\n", "" ] [do_remove_preceding_blank_lines_attribute] source = ''' CONST = 123 """Docstring for CONST.""" ''' expected = [ " ", "CONST", "=", "123", "\n", '"""Docstring for CONST."""', "\n", "", "", ] [get_unmatched_start_end_indices] prev_token = [5, " ", [2, 0], [2, 4], ''' """This is a docstring.\n'''] token = [ 3, '''"""This is a docstring.\n\n\n That should be on less lines\n"""''', [3, 4], [6, 7], ''' """This is a docstring.\n\n\n That should be on less lines\n """'''] expected = [[2, 4], [6, 7]] [do_update_token_indices] tokens = [ [1, 'def', [1, 0], [1, 3], 'def foo():\n'], [1, 'foo', [1, 4], [1, 7], 'def foo():\n'], [55, '(', [1, 7], [1, 8], 'def foo():\n'], [55, ')', [1, 8], [1, 9], 'def foo():\n'], [55, ':', [1, 9], [1, 10], 'def foo():\n'], [4, '\n', [1, 10], [1, 11], 'def foo():\n'], [5, ' ', [3, 0], [3, 4], ' """Hello foo."""\n'], [3, '"""Hello foo."""', [3, 4], [5, 7], ' """Hello foo."""\n'], [4, '\n', [5, 7], [5, 8], ' """Hello foo."""\n'], [6, '', [6, 0], [6, 0], ''], [0, '', [6, 0], [6, 0], ''] ] expected = [ [[1, 0], [1, 3]], [[1, 4], [1, 7]], [[1, 7], [1, 8]], [[1, 8], [1, 9]], [[1, 9], [1, 10]], [[1, 10], [1, 11]], [[2, 0], [2, 4]], [[2, 4], [2, 7]], [[2, 7], [2, 8]], [[3, 0], [3, 0]], [[3, 0], [3, 0]] ] docformatter-1.7.8/tests/_data/string_files/format_methods.toml000066400000000000000000000076511517155121300250440ustar00rootroot00000000000000# In this file, token lists have the following information: # [type, string, start, end, line] # for creating a TokenInfo() object. [do_add_unformatted_docstring] token = [ 3, '''"""This is a docstring.\n\n\n That should be on less lines\n"""''', [3, 4], [6, 7], ''' """This is a docstring.\n\n\n That should be on less lines\n """'''] [do_add_formatted_docstring] token = [ 3, '''"""This is a docstring.\n"""''', [3, 4], [6, 7], ''' """This is a docstring.\n """'''] next_token = [5, "\b", [2, 0], [2, 4], ''' """This is a docstring.\n'''] [do_format_oneline_docstring] source = "This is a one-line docstring." expected = '"""This is a one-line docstring."""' [do_format_oneline_docstring_that_ends_in_quote] source ='"Hello"' expected = '''""""Hello"."""''' [do_format_oneline_docstring_with_wrap] source = "This is a long one-line summary that will need to be wrapped because we're going to pass the --wrap-summaries argument." expected = ''' """This is a long one-line summary that will need to be wrapped because we're going to pass the --wrap-summaries argument."""''' [do_format_oneline_docstring_with_quotes_newline] source = "This is a long one-line summary that will have the closing quotes on a separate line because we're going to pass the --close-quotes-on-newline argument." expected = ''' """This is a long one-line summary that will have the closing quotes on a separate line because we're going to pass the --close-quotes-on-newline argument. """''' [do_format_oneline_docstring_make_multiline] source = "This is one-line docstring and we're going to pass the --make-summary-multi-line argument to see what happens." expected = ''' """ This is one-line docstring and we're going to pass the --make-summary- multi-line argument to see what happens. """''' [do_format_multiline_docstring] source = [ "This is the summary of a multiline docstring.", "This is the long description part of the same multiline docstring."] expected = '''"""This is the summary of a multiline docstring. This is the long description part of the same multiline docstring. """''' [do_format_multiline_docstring_pre_summary_newline] source = [ "This is the summary of a multiline docstring.", "This is the long description part of the same multiline docstring."] expected = '''""" This is the summary of a multiline docstring. This is the long description part of the same multiline docstring. """''' [do_format_multiline_docstring_post_description_blank] source = [ "This is the summary of a multiline docstring.", "This is the long description part of the same multiline docstring."] expected = '''"""This is the summary of a multiline docstring. This is the long description part of the same multiline docstring. """''' [do_rewrite_docstring_blocks] tokens = [ [1, "def", [1, 0], [1, 3], "def foo():\n"], [1, "foo", [1, 4], [1, 7], "def foo():\n"], [55, "(", [1, 7], [1, 8], "def foo():\n"], [55, ")", [1, 8], [1, 9], "def foo():\n"], [55, ":", [1, 9], [1, 10], "def foo():\n"], [4, "\n", [1, 10], [1, 11], "def foo():\n"], [5, " ", [3, 0], [3, 4], ''' """Hello foo."""\n'''], [3, '"""Hello foo."""', [3, 4], [5, 7], ''' """Hello foo."""\n'''], [4, "\n", [5, 7], [5, 8], ''' """Hello foo."""\n'''], [6, "", [6, 0], [6, 0], ""], [0, "", [6, 0], [6, 0], ""] ] expected = [ [1, "def", [1, 0], [1, 3], "def foo():\n"], [1, "foo", [1, 4], [1, 7], "def foo():\n"], [55, "(", [1, 7], [1, 8], "def foo():\n"], [55, ")", [1, 8], [1, 9], "def foo():\n"], [55, ":", [1, 9], [1, 10], "def foo():\n"], [4, "\n", [1, 10], [1, 11], "def foo():\n"], [5, " ", [2, 0], [2, 4], ''' """Hello foo.""" '''], [3, '"""Hello foo."""', [2, 4], [2, 7], ''' """Hello foo.""" '''], [4, "\n", [2, 7], [2, 8], ''' """Hello foo.""" '''], [6, "", [3, 0], [3, 0], ""], [0, "", [3, 0], [3, 0], ""] ] docformatter-1.7.8/tests/_data/string_files/header_patterns.toml000066400000000000000000000074551517155121300252030ustar00rootroot00000000000000[is_alembic_header] instring = "Revision ID: >" expected = "Revision ID: " [is_not_alembic_header_epytext] instring = "@not alembic header: some non-alembic stuff" expected = "None" [is_not_alembic_header_google] instring = "not alembic header: some non-alembic stuff" expected = "None" [is_not_alembic_header_numpy] instring = "not alembic header : some non-alembic stuff" expected = "None" [is_numpy_section_header_parameters] instring = "Parameters\n----------\nsome parameters" expected = "Parameters\n----------" [is_numpy_section_header_returns] instring = "Returns\n----------\nsome return values" expected = "Returns\n----------" [is_numpy_section_header_yields] instring = "Yields\n----------\nsome yield value" expected = "Yields\n----------" [is_numpy_section_header_raises] instring = "Raises\n----------\nsome errors raised" expected = "Raises\n----------" [is_numpy_section_header_receives] instring = "Receives\n----------\nsome values to receive" expected = "Receives\n----------" [is_numpy_section_header_other_parameters] instring = "Other Parameters\n----------\nsome other parameters" expected = "Other Parameters\n----------" [is_numpy_section_header_warns] instring = "Warns\n----------\nthe little used Warns section" expected = "Warns\n----------" [is_numpy_section_header_warnings] instring = "Warnings\n----------\nthe little used Warnings section" expected = "Warnings\n----------" [is_numpy_section_header_see_also] instring = "See Also\n----------\nother stuff you should look at" expected = "See Also\n----------" [is_numpy_section_header_examples] instring = "Examples\n----------\nsome examples" expected = "Examples\n----------" [is_numpy_section_header_notes] instring = "Notes\n----------\nsome notes" expected = "Notes\n----------" [is_not_numpy_section_header] instring = "Section\n----------\na section that is not standard" expected = "None" [is_not_numpy_section_header_wrong_dashes] instring = "Parameters\n**********\na section with standard name but wrong dashes" expected = "None" [is_rest_section_header_pound] instring = "######\nPart 1\n######\nsome part" expected = "######\nPart 1\n######" [is_rest_section_header_star] instring = "*********\nChapter 1\n*********\nsome chapter" expected = "*********\nChapter 1\n*********" [is_rest_section_header_equal] instring = "Section 1\n=========\nsome section" expected = "Section 1\n=========" [is_rest_section_header_dash] instring = "Subsection 1\n------------\nsome subsection" expected = "Subsection 1\n------------" [is_rest_section_header_circumflex] instring = "Subsubsection 1\n^^^^^^^^^^^^^^^\nsome subsubsection" expected = "Subsubsection 1\n^^^^^^^^^^^^^^^" [is_rest_section_header_single_quote] instring = "Section 2\n'''''''''\nanother section" expected = "Section 2\n'''''''''" [is_rest_section_header_double_quote] instring = '''Subsection 2 """""""""""" another subsection ''' expected = '''Subsection 2 """"""""""""''' [is_rest_section_header_plus] instring = "Subsubsection 2\n+++++++++++++++\nanother subsubsection" expected = "Subsubsection 2\n+++++++++++++++" [is_rest_section_header_underscore] instring = "______\nPart 3\n______\nyet another part" expected = "______\nPart 3\n______" [is_rest_section_header_tilde] instring = "Section 3\n~~~~~~~~~\nyet another section" expected = "Section 3\n~~~~~~~~~" [is_rest_section_header_colon] instring = "Subsection 3\n::::::::::::\nyet another subsection" expected = "Subsection 3\n::::::::::::" [is_rest_section_header_backtick] instring = "Subsubsection 3\n```````````````\nyet another subsubsection" expected = "Subsubsection 3\n```````````````" [is_rest_section_header_period] instring = "Part 4\n......\nthe fourth part" expected = "Part 4\n......" [is_not_rest_section_header_unknown_adornments] instring = "??????\nPart 5\n??????\na part with unknown adornments" expected = "None" docformatter-1.7.8/tests/_data/string_files/list_patterns.toml000066400000000000000000000104461517155121300247200ustar00rootroot00000000000000[is_bullet_list] instring = """* parameter\n - parameter\n + parameter\n""" strict = false style = "sphinx" expected = true [is_enum_list] instring = """1. parameter\n 2. parameter\n 3. parameter\n""" strict = false style = "sphinx" expected = true [is_option_list] instring = """ -a include all the stuff\n --config the path to the configuration file\n -h, --help show this help\n""" strict = false style = "sphinx" expected = true [is_option_list_indented] instring = """ -a include all the stuff\n --config the path to the configuration file\n -h, --help show this help\n""" strict = false style = "sphinx" expected = true [is_list_with_single_hyphen] instring = """\ Keyword arguments: real - the real part (default 0.0) imag - the imaginary part (default 0.0) """ strict = false style = "sphinx" expected = true [is_list_with_double_hyphen] instring = """\ Keyword arguments: real -- the real part (default 0.0) imag -- the imaginary part (default 0.0) """ strict = false style = "sphinx" expected = true [is_list_with_at_sign] instring = """\ Keyword arguments: @real the real part (default 0.0) @imag the imaginary part (default 0.0) """ strict = false style = "sphinx" expected = true [is_heuristic_list] instring = "Example:\nrelease-1.1/\nrelease-1.2/\nrelease-1.3/\nrelease-1.4/\nrelease-1.4.1/\nrelease-1.5/\n" strict = false style = "sphinx" expected = true [is_type_of_list_strict_wrap] instring = "Launch\nthe\nrocket." strict = true style = "numpy" expected = false [is_type_of_list_non_strict_wrap] # See issue #67. instring = "Launch\nthe\nrocket." strict = false style = "numpy" expected = true [is_type_of_list_alembic_header] # See issue #242. instring = """Add some column. Revision ID: > Revises: Create Date: 2023-01-06 10:13:28.156709 """ strict = false style = "numpy" expected = true [is_not_list_sphinx_style] # See requirement docformatter_10.4 instring = """\ Using Sphinx parameter list :param str arg1: the first argument. :param int arg2: the second argument. """ strict = false style = "sphinx" expected = false [is_sphinx_list_numpy_style] # See requirements docformatter_10.2.1 and docformatter_10.3.1 instring = """\ Using Sphinx parameter list :param str arg1: the first argument. :param int arg2: the second argument. """ strict = false style = "numpy" expected = true [is_numpy_list_sphinx_style] # See requirement docformatter_10.4.1 instring = """\ Using Numpy parameter list Parameters ---------- arg1 : str The first argument. arg2 : int The second argument. """ strict = false style = "sphinx" expected = true [is_google_list_numpy_style] instring = """\ Args: stream (BinaryIO): Binary stream (usually a file object). """ strict = true style = "numpy" expected = true [is_literal_block] instring = """\ This is a description. Example code:: config(par=value) Example code2:: with config(par=value) as f: pass """ strict = false style = "numpy" expected = true [is_reST_header] instring = """\ =============================== Example of creating an example. =============================== .. currentmodule:: my_project In this example, we illustrate how to create an example. """ strict = false style = "numpy" expected = true [is_sphinx_field_list] instring = """\ This is a description. :parameter arg1: the first argument. :parameter arg2: the second argument. """ strict = false style = "sphinx" expected = false [is_epytext_field_list] instring = """\ This is a description. @param arg1: the first argument. @param arg2: the second argument. """ strict = false style = "epytext" expected = false [is_numpy_section_in_docstring_issue_338] # See GitHub issue #338 instring = """Do a number of things. Parameters ---------- n How many things to do. colors True to paint the things in bright colors, False to keep them dull and grey. Returns ------- int How many things were actually done. """ strict = false style = "numpy" expected = true [is_rest_section_in_docstring] instring = """This is a description. Section Title ============= Some content under the section. """ strict = false style = "numpy" expected = true docformatter-1.7.8/tests/_data/string_files/misc_patterns.toml000066400000000000000000000014111517155121300246700ustar00rootroot00000000000000[is_some_sort_of_code] instring = """ __________=__________(__________,__________,__________, __________[ '___'],__________,__________,__________, __________,______________=__________) """ expected = true [is_some_sort_of_code_python] instring = """ def is_some_sort_of_code(): x = 1 y = 42 return x + y """ expected = true [is_probably_beginning_of_sentence] instring = "- This is part of a list." expected = true [is_not_probably_beginning_of_sentence] instring = "(this just continues an existing sentence)." expected = "None" [is_probably_beginning_of_sentence_pydoc_ref] instring = ":see:MyClass This is not the start of a sentence." expected = "None" docformatter-1.7.8/tests/_data/string_files/rest_patterns.toml000066400000000000000000000036571517155121300247300ustar00rootroot00000000000000[is_double_dot_directive] instring = """ This is a docstring that contains a reST directive. .. directive type:: directive :modifier: The directive type description. This is the part of the docstring that follows the reST directive. """ expected = [53, 136] [is_double_dot_directive_indented] instring = """ ``pattern`` is considered as an URL only if it is parseable as such and starts with ``http://`` or ``https://``. .. important:: This is a straight `copy of the functools.cache implementation `_, which is only `available in the standard library starting with Python v3.9 `. """ expected = [114, 499] [is_inline_directive] instring = """ These are some reST directives that need to be retained even if it means not wrapping the line they are found on. Constructs and returns a :class:`QuadraticCurveTo `. Register ``..click:example::`` and ``.. click:run::`` directives, augmented with ANSI coloring. """ expected = [145, 183] [is_double_backtick_directive] instring = """ By default we choose to exclude: ``Cc`` Since ``mailman`` apparently `sometimes trims list members `_ from the ``Cc`` header to avoid sending duplicates. Which means that copies of mail reflected back from the list server will have a different ``Cc`` to the copy saved by the MUA at send-time. ``Bcc`` Because copies of the mail saved by the MUA at send-time will have ``Bcc``, but copies reflected back from the list server won't. ``Reply-To`` Since a mail could be ``Cc``'d to two lists with different ``Reply-To`` munging options set. """ expected = [38, 44] docformatter-1.7.8/tests/_data/string_files/string_functions.toml000066400000000000000000000263071517155121300254260ustar00rootroot00000000000000[do_reindent] instring = '''""" This should be dedented. 1. This too. 2. And this. """''' expected = ''' """ This should be dedented. 1. This too. 2. And this. """ ''' [do_reindent_should_expand_tabs_to_indentation] instring = "\tThis should be dedented.\n\t1. This too.\n\t2. And this." expected = " This should be dedented.\n 1. This too.\n 2. And this.\n" [do_reindent_with_no_indentation_expand_tabs] instring = "The below should be indented with spaces:\n\n\t1. This too.\n\t2. And this." expected = "The below should be indented with spaces:\n\n 1. This too.\n 2. And this.\n" [do_reindent_should_maintain_indentation] instring = ''' """ This should maintain its indentation. 1. This too. 2. And this. """''' expected = ''' """ This should maintain its indentation. 1. This too. 2. And this. """ ''' [do_reindent_tab_indentation] instring = "\tThis should be indented with a tab.\n\n\tSo should this." expected = "\tThis should be indented with a tab.\n\n\tSo should this.\n" [do_normalize_summary] instring = "This is a sentence " expected = "This is a sentence." [do_normalize_summary_multiline] instring = "This \n\t is\na sentence " expected = "This \n\t is\na sentence." [do_normalize_summary_question_mark] instring = "This is a question? " expected = "This is a question?" [do_normalize_summary_exclamation_point] instring = "This is a bold statement! " expected = "This is a bold statement!" [do_normalize_summary_with_title] # See issue #56. instring = "# This is a markup title " expected = "# This is a markup title" [do_normalize_summary_capitalize_first_letter] # See issue #76 instring = "this is a summary that needs to be capped" expected = "This is a summary that needs to be capped." [do_normalize_summary_with_proprer_noun] instring = "don't lower case I'm" expected = "Don't lower case I'm." [do_normalize_summary_capitalize_first_letter_with_period] # See issue #184. See requirement docformatter_4.5.1. instring = "this is a summary that needs to be capped." expected = "This is a summary that needs to be capped." [do_normalize_summary_dont_capitalize_first_letter_if_variable] instring = "num_iterations should not be capitalized in this summary" expected = "num_iterations should not be capitalized in this summary." [do_normalize_line] instring = "This is a line with carriage return\r" expected = "This is a line with carriage return\n" [do_normalize_line_endings] instring = ["This is a line with carriage return\r\n", "This is another line with carriage return\r"] expected = "This is a line with carriage return\nThis is another line with carriage return\n" [description_to_list] instring = "This is a long description that is going to be converted to a list.\n\nThere's lots of reasons one might want to turn a text block into a list.\nThis is just one of those reasons.\n" expected = [ " This is a long description that is going to be converted to a list.", "", " There's lots of reasons one might want to turn a text block into a", " list. This is just one of those reasons.", "", ] [do_split_first_sentence] instring = "This is a sentence. More stuff. And more stuff. .!@#$%" expected = ["This is a sentence.", " More stuff. And more stuff. .!@#$%"] [do_split_first_sentence_2] instring = "This e.g. sentence. More stuff. And more stuff. .!@#$%" expected = ["This e.g. sentence.", " More stuff. And more stuff. .!@#$%",] [do_split_first_sentence_3] instring = "This is the first:\none\ntwo" expected = ["This is the first:", "\none\ntwo"] [do_split_summary] # See issue #283. instring = ["This is a sentence.", ""] expected = ["This is a sentence.", ""] [do_split_summary_2] # See issue #283. instring = ["This e.g. a sentence.", ""] expected = ["This e.g. a sentence.", ""] [do_split_multi_sentence_summary] # See issue #283. instring = ["This is a sentence. This is another.", ""] expected = ["This is a sentence.", "This is another.", ""] [do_split_multi_sentence_summary_2] # See issue #283. instring = ["This e.g. a sentence. This is another.", ""] expected = ["This e.g. a sentence.", "This is another.", ""] [do_split_description_url_outside_param] instring = "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm https://mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm\n:param a:mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm" expected = [" mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", " https://mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", " :param a: mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm"] [do_split_description_single_url_in_param] instring = ":param a: mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm\n https://mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmma" expected = [" :param a: mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", " https://mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmma"] [do_split_description_single_url_in_multiple_params] instring = "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm\n\n:param a: mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm\n https://mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmma\n:param b: mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm\n https://mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmb" expected = [" mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", "", " :param a: mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", " https://mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmma", " :param b: mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", " https://mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmb"] [do_split_description_multiple_urls_in_param] instring = "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm\n:param a: mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm\n https://mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm0\n https://mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm1" expected = [' mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm', ' :param a: mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm', ' https://mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm0', ' https://mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm1'] [do_split_summary_and_description] instring = "This is the first. This is the second. This is the third." expected = ["This is the first.", "This is the second. This is the third.",] [do_split_summary_and_description_complex] instring = "This is the first\n\nThis is the second. This is the third." expected = ["This is the first", "\nThis is the second. This is the third."] [do_split_summary_and_description_more_complex] instring = "This is the first.\nThis is the second. This is the third." expected = ["This is the first.", "This is the second. This is the third."] [do_split_summary_and_description_with_list] instring = "This is the first\n- one\n- two" expected = ["This is the first", "- one\n- two"] [do_split_summary_and_description_with_list_of_parameters] instring = "This is the first\none - one\ntwo - two" expected = ["This is the first", "one - one\ntwo - two"] [do_split_summary_and_description_with_capital] instring = "This is the first\nWashington" expected = ["This is the first\nWashington", ""] [do_split_summary_and_description_with_list_on_other_line] instring = " Test\n test\n @blah" expected = ["Test", " test\n @blah"] [do_split_summary_and_description_with_other_symbol] instring = "This is the first\n@ one\n@ two" expected = ["This is the first", "@ one\n@ two"] [do_split_summary_and_description_with_colon] instring = "This is the first:\none\ntwo" expected = ["This is the first:", "one\ntwo"] [do_split_summary_and_description_with_exclamation] instring = "This is the first!\none\ntwo" expected = ["This is the first!", "one\ntwo"] [do_split_summary_and_description_with_question_mark] instring = "This is the first?\none\ntwo" expected = ["This is the first?", "one\ntwo"] [do_split_summary_and_description_with_double_quote] instring = 'This is the first\n"one".' expected = ['This is the first\n"one".', ""] [do_split_summary_and_description_with_single_quote] instring = "This is the first\n'one'." expected = ["This is the first\n'one'.", ""] [do_split_summary_and_description_with_double_backtick] instring = "This is the first\n``one``." expected = ["This is the first\n``one``.", ""] [do_split_summary_and_description_with_punctuation] instring = " Try this and this and this and this and this and this and this at\n https://example.com/\n\n Parameters\n ----------\n email : string" expected = ["Try this and this and this and this and this and this and this at\n https://example.com/", "\n Parameters\n ----------\n email : string"] [do_split_summary_and_description_without_punctuation] instring = "Try this and this and this and this and this and this and this at this other line\n\n Parameters\n ----------\n email : string" expected = ["Try this and this and this and this and this and this and this at this other line", "\n Parameters\n ----------\n email : string"] [do_split_summary_and_description_with_abbreviation] instring = "Test e.g. now, Test foo, bar, etc. now, Test i.e. now, Test Dr. now, Test Mr. now, Test Mrs. now, Test Ms. now" expected = ["Test e.g. now, Test foo, bar, etc. now, Test i.e. now, Test Dr. now, Test Mr. now, Test Mrs. now, Test Ms. now", ""] [do_split_summary_and_description_with_url] instring ="Sequence of package managers as defined by `XKCD #1654: Universal Install Script `_.\n\nSee the corresponding :issue:`implementation rationale in issue #10 <10>`." expected = ["Sequence of package managers as defined by `XKCD #1654: Universal Install Script `_.", "\nSee the corresponding :issue:`implementation rationale in issue #10 <10>`."] [do_strip_docstring] instring = '''"""This is a docstring. With some indentation. """''' expected = ["This is a docstring.\n\n With some indentation.", '"""'] [do_strip_docstring_triple_single_quotes] instring = """'''This is a docstring. With some indentation. '''""" expected = ["This is a docstring.\n\n With some indentation.", '"""'] [do_strip_docstring_empty_string] instring = '''""""""''' expected = ["", '"""'] [do_strip_docstring_raw_string] instring = '''r""" foo"""''' expected = ["foo", 'r"""'] [do_strip_docstring_raw_string_2] instring = '''R""" foo """''' expected = ["foo", 'R"""'] [do_strip_docstring_unicode_string] instring = '''u"""bar """''' expected = ["bar", 'u"""'] [do_strip_docstring_unicode_string_2] instring = '''U""" bar """''' expected = ["bar", 'U"""'] [do_strip_docstring_with_unknown] instring = "foo" expected = [] raises = "ValueError" [do_strip_docstring_with_single_quotes] # See issue #66 instring = "'hello\\''" expected = [] raises = "ValueError" [do_strip_docstring_with_double_quotes] # See issue #66 instring = '"hello\\""' expected = [] raises = "ValueError" [do_strip_leading_blank_lines] instring = "\n\n\nThis is a line with leading blank lines" expected = "This is a line with leading blank lines" [do_clean_excess_whitespace] instring = "`Get\n Cookies.txt ` while browsing.""" expected = true docformatter-1.7.8/tests/_data/string_files/url_wrappers.toml000066400000000000000000000122751517155121300245540ustar00rootroot00000000000000[elaborate_inline_url] instring = """ Here is an elaborate description containing a link. `Area Under the Receiver Operating Characteristic Curve (ROC AUC) `_.""" expected = [ [" Here is an elaborate description containing a link. `Area Under the", " Receiver Operating Characteristic Curve (ROC AUC)", " `_"], 226] [short_inline_url] instring = "See `the link `_ for more details." expected = [[], 0] [long_inline_url] instring = """ A larger description that starts here. https://github.com/apache/kafka/blob/2.5/clients/src/main/java/org/apache/kafka/common/requests/DescribeConfigsResponse.java A larger description that ends here. """ expected = [ [" A larger description that starts here.", " https://github.com/apache/kafka/blob/2.5/clients/src/main/java/org/apache/kafka/common/requests/DescribeConfigsResponse.java"], 168] [simple_url] instring = "See http://www.reliqual.com/wiki/how_to_use_ramstk/verification_and_validation_module/index.html for additional information." expected = [ [" See", " http://www.reliqual.com/wiki/how_to_use_ramstk/verification_and_validation_module/index.html for"], 100] [short_url] instring = "See http://www.reliaqual.com for examples." expected = [[], 0] [inline_url_retain_space] instring = """ A larger description that starts here. https://github.com/apache/kafka/blob/2.5/clients/src/main/java/org/apache/kafka/common/requests/DescribeConfigsResponse.java A larger description that ends here.""" expected = [ [" A larger description that starts here.", " https://github.com/apache/kafka/blob/2.5/clients/src/main/java/org/apache/kafka/common/requests/DescribeConfigsResponse.java"], 171] [keep_inline_url_together] instring = """ See the list of `custom types provided by Click `_.""" expected = [ [" See the list of", " `custom types provided by Click `_."], 133] [inline_url_two_paragraphs] instring = """ User configuration is `merged to the context default_map as Click does `_. This allow user's config to only overrides defaults. Values sets from direct command line parameters, environment variables or interactive prompts, takes precedence over any values from the config file.""" expected = [ [" User configuration is", " `merged to the context default_map as Click does `_"], 153] [url_no_delete_words] instring = """ This will normally be used with https://aaaaaaaa.bbb.ccccccccc.com/xxxxx/xxx_xxxxxxxxxxx to generate the xxx""" expected = [ [" This will normally be used with", " https://aaaaaaaa.bbb.ccccccccc.com/xxxxx/xxx_xxxxxxxxxx"], 92] [no_newline_after_url] instring = """ Generated by 'django-admin startproject' using Django 4.1.1. For more information on this file, see https://docs.djangoproject.com/en/4.1/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.1/ref/settings/""" expected = [ [" Generated by 'django-admin startproject' using Django 4.1.1.", "", " For more information on this file, see", " https://docs.djangoproject.com/en/4.1/topics/settings/", "", " For the full list of settings and their values, see", " https://docs.djangoproject.com/en/4.1/ref/settings/"], 280] [only_url_in_description] instring = " https://example.com/this-is-just-a-long-url/designed-to-trigger/the-wrapping-of-the-description" expected = [ [" https://example.com/this-is-just-a-long-url/designed-to-trigger/the-wrapping-of-the-description"], 99] [no_indent_string_on_newline] instring = """ Here is a link to the github issue https://github.com/PyCQA/docformatter/issues/199 This is a long description.""" expected = [ [" Here is a link to the github issue", " https://github.com/PyCQA/docformatter/issues/19"], 91] [short_anonymous_url] instring = """ This graphics format generates terminal escape codes that transfer PNG data to a TTY using the `kitty graphics protocol`__. __ https://sw.kovidgoyal.net/kitty/graphics-protocol/""" expected = [ [" This graphics format generates terminal escape codes that transfer", " PNG data to a TTY using the `kitty graphics protocol`__.", "", " __ https://sw.kovidgoyal.net/kitty/graphics-protocol/"], 190] [quoted_url] instring = """ It's not a perfect guess, but it's better than having "https://example.com". :param bundle: The bundle identifier. :param app_name: The app name. :returns: The candidate project URL""" expected = [ [" It's not a perfect guess, but it's better than having", ' "https://example.com"'], 80] docformatter-1.7.8/tests/_data/string_files/utility_functions.toml000066400000000000000000000052371517155121300256220ustar00rootroot00000000000000[has_correct_length_none] length_range = "None" start = 1 end = 9 expected = true [has_correct_length_start_in_range] length_range = [1, 3] start = 3 end = 5 expected = true [has_correct_length_end_in_range] length_range = [1, 10] start = 5 end = 10 expected = true [has_correct_length_both_in_range] length_range = [1, 10] start = 3 end = 7 expected = true [has_correct_length_start_out_of_range] length_range = [5, 16] start = 3 end = 5 expected = false [has_correct_length_end_out_of_range] length_range = [1, 10] start = 5 end = 20 expected = false [has_correct_length_both_out_of_range] length_range = [1, 10] start = 11 end = 27 expected = false [is_in_range_none] line_range = "None" start = 1 end = 9 expected = true [is_in_range_start_in_range] line_range = [1, 4] start = 3 end = 5 expected = true [is_in_range_end_in_range] line_range = [1, 4] start = 4 end = 10 expected = true [is_in_range_both_in_range] line_range = [2, 10] start = 1 end = 2 expected = true [is_in_range_out_of_range] line_range = [10, 20] start = 1 end = 9 expected = false [find_py_file] sources = ["test_python_file.py"] exclude = [] expected = ["test_python_file.py"] [find_py_file_recursive] sources = [ "/root/folder_one/one.py", "/root/folder_one/folder_three/three.py", "/root/folder_two/two.py", ] exclude = [] expected = [ "/root/folder_one/folder_three/three.py", "/root/folder_one/one.py", "/root/folder_two/two.py", ] [skip_hidden_py_file] sources = ["not_hidden.py", ".hidden_file.py"] exclude = [".hidden_file.py"] expected = ["not_hidden.py"] [skip_hidden_py_file_recursive] sources = ["/root/not_hidden.py", "/root/.hidden_file.py"] exclude = [".hidden_file.py"] expected = ["/root/not_hidden.py"] [ignore_non_py_file] sources = ["one.py", "two.py", "three.toml"] exclude = [] expected = ["one.py", "two.py"] [ignore_non_py_file_recursive] sources = ["one.py", "two.py", "three.toml", "subdir/four.py", "subdir/five.txt"] exclude = [] expected = ["one.py", "subdir/four.py", "two.py"] [exclude_py_file] sources = ["one.py", "two.py", "three.py", "four.py"] exclude = ["three.py"] expected = ["four.py", "one.py", "two.py"] [exclude_py_file_recursive] sources = ["/root/one.py", "/root/two.py", "/root/folder_three/three.py", "/root/four.py"] exclude = ["three.py"] expected = ["/root/four.py", "/root/one.py", "/root/two.py"] [exclude_multiple_files] sources = ["one.py", "two.py", "three.py", "four.py"] exclude = ["three.py", "four.py"] expected = ["one.py", "two.py"] [exclude_multiple_files_recursive] sources = ["/root/one.py", "/root/two.py", "/root/folder_three/three.py", "/root/four.py"] exclude = ["three.py", "four.py"] expected = ["/root/one.py", "/root/two.py"] docformatter-1.7.8/tests/_data/tox.ini000066400000000000000000000001101517155121300177360ustar00rootroot00000000000000[docformatter] wrap-descriptions = 72 wrap-summaries = 79 blank = False docformatter-1.7.8/tests/conftest.py000066400000000000000000000157231517155121300175720ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.conftest.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """docformatter test suite configuration file.""" # Standard Library Imports import argparse import os import shutil import subprocess import sys import tempfile # Third Party Imports import pytest # Root directory is up one because we're in tests/. ROOT_DIRECTORY = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) @pytest.fixture(scope="function") def temporary_directory(directory=".", prefix=""): """Create temporary directory and yield its path.""" temp_directory = tempfile.mkdtemp(prefix=prefix, dir=directory) try: yield temp_directory finally: shutil.rmtree(temp_directory) @pytest.fixture(scope="function") def temporary_file(contents, file_directory=".", file_prefix=""): """Write contents to temporary file and yield it.""" f = tempfile.NamedTemporaryFile( suffix=".py", prefix=file_prefix, delete=False, dir=file_directory ) try: f.write(contents.encode()) f.close() yield f.name finally: os.remove(f.name) @pytest.fixture(scope="function") def temporary_pyproject_toml( config, config_file_directory="/tmp", ): """Write contents to temporary configuration and yield it.""" f = open(f"{config_file_directory}/pyproject.toml", "wb") try: f.write(config.encode()) f.close() yield f.name finally: os.remove(f.name) @pytest.fixture(scope="function") def temporary_setup_cfg( config, config_file_directory="/tmp", ): """Write contents to temporary configuration and yield it.""" f = open(f"{config_file_directory}/setup.cfg", "wb") try: f.write(config.encode()) f.close() yield f.name finally: os.remove(f.name) @pytest.fixture(scope="function") def run_docformatter(arguments, temporary_file): """Run subprocess with same Python path as parent. Return subprocess object. """ if "DOCFORMATTER_COVERAGE" in os.environ and int( os.environ["DOCFORMATTER_COVERAGE"] ): DOCFORMATTER_COMMAND = [ "coverage", "run", "--branch", "--parallel", "--omit=*/site-packages/*", os.environ["VIRTUAL_ENV"] + "/bin/docformatter", ] else: DOCFORMATTER_COMMAND = [ os.environ["VIRTUAL_ENV"] + "/bin/docformatter", ] # pragma: no cover if "-" not in arguments: arguments.append(temporary_file) environ = os.environ.copy() environ["PYTHONPATH"] = os.pathsep.join(sys.path) return subprocess.Popen( DOCFORMATTER_COMMAND + arguments, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, env=environ, ) @pytest.fixture(scope="function") def test_args(args): """Create a set of arguments to use with tests. To pass no arguments, just an empty file name: @pytest.mark.parametrize("args", [[""]]) To pass an argument AND empty file name: @pytest.mark.parametrize("args", [["--wrap-summaries", "79", ""]]) """ parser = argparse.ArgumentParser( description="parser object for docformatter tests", prog="docformatter", ) changes = parser.add_mutually_exclusive_group() changes.add_argument( "-i", "--in-place", action="store_true", ) changes.add_argument( "-c", "--check", action="store_true", ) parser.add_argument( "-r", "--recursive", action="store_true", default=False, ) parser.add_argument( "-e", "--exclude", nargs="*", ) parser.add_argument( "-n", "--non-cap", nargs="*", ) parser.add_argument( "-s", "--style", default="sphinx", ) parser.add_argument( "--rest-section-adorns", default=r"[=\-`:'\"~^_*+#<>]{4,}", ) parser.add_argument( "--wrap-summaries", default=79, type=int, metavar="length", ) parser.add_argument( "--wrap-descriptions", default=72, type=int, metavar="length", ) parser.add_argument( "--force-wrap", action="store_true", default=False, ) parser.add_argument( "--tab-width", type=int, dest="tab_width", metavar="width", default=1, ) parser.add_argument( "--blank", dest="post_description_blank", action="store_true", default=False, ) parser.add_argument( "--pre-summary-newline", action="store_true", default=False, ) parser.add_argument( "--pre-summary-space", action="store_true", default=False, ) parser.add_argument( "--black", action="store_true", default=False, ) parser.add_argument( "--make-summary-multi-line", action="store_true", default=False, ) parser.add_argument( "--close-quotes-on-newline", action="store_true", default=False, ) parser.add_argument( "--range", metavar="line", dest="line_range", default=None, type=int, nargs=2, ) parser.add_argument( "--docstring-length", metavar="length", dest="length_range", default=None, type=int, nargs=2, ) parser.add_argument( "--non-strict", action="store_true", default=False, ) parser.add_argument( "--config", ) parser.add_argument( "--version", action="version", version="test version", ) parser.add_argument( "files", nargs="+", ) yield parser.parse_args(args) docformatter-1.7.8/tests/formatter/000077500000000000000000000000001517155121300173665ustar00rootroot00000000000000docformatter-1.7.8/tests/formatter/__init__.py000066400000000000000000000000001517155121300214650ustar00rootroot00000000000000docformatter-1.7.8/tests/formatter/test_do_format_code.py000066400000000000000000000127521517155121300237520ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.formatter.test_do_format_code.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Module for testing the Formattor _do_format_code method.""" # Standard Library Imports import contextlib import sys with contextlib.suppress(ImportError): if sys.version_info >= (3, 11): # Standard Library Imports import tomllib else: # Third Party Imports import tomli as tomllib # Third Party Imports import pytest # docformatter Package Imports from docformatter import Formatter NO_ARGS = [""] with open("tests/_data/string_files/do_format_code.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) @pytest.mark.integration @pytest.mark.order(7) @pytest.mark.parametrize( "test_key, args", [ ("one_line", NO_ARGS), ("module_docstring", NO_ARGS), ("newline_module_variable", NO_ARGS), ("class_docstring", NO_ARGS), ("newline_class_variable", NO_ARGS), ("newline_outside_docstring", NO_ARGS), pytest.param( "preserve_line_ending", NO_ARGS, marks=pytest.mark.skipif( sys.platform != "win32", reason="Not running on Windows" ), ), ("non_docstring", NO_ARGS), ("tabbed_indentation", NO_ARGS), ("mixed_indentation", NO_ARGS), ("escaped_newlines", NO_ARGS), ("code_comments", NO_ARGS), ("inline_comment", NO_ARGS), ("raw_lowercase", NO_ARGS), ("raw_uppercase", NO_ARGS), ("raw_lowercase_single", NO_ARGS), ("raw_uppercase_single", NO_ARGS), ("unicode_lowercase", NO_ARGS), ("unicode_uppercase", NO_ARGS), ("unicode_lowercase_single", NO_ARGS), ("unicode_uppercase_single", NO_ARGS), ("nested_triple", NO_ARGS), ("multiple_sentences", NO_ARGS), ("multiple_sentences_same_line", NO_ARGS), ("multiline_summary", NO_ARGS), ("empty_lines", NO_ARGS), ("class_empty_lines", NO_ARGS), ("class_empty_lines_2", NO_ARGS), ("method_empty_lines", NO_ARGS), ("trailing_whitespace", NO_ARGS), ("parameter_list", NO_ARGS), ("single_quote", NO_ARGS), ("double_quote", NO_ARGS), ("nested_triple_quote", NO_ARGS), ("first_line_assignment", NO_ARGS), ("regular_strings", NO_ARGS), ("syntax_error", NO_ARGS), ("slash_r", NO_ARGS), ("slash_r_slash_n", NO_ARGS), ("strip_blank_lines", ["--black", ""]), ("range_miss", ["--range", "1", "1", ""]), ("range_hit", ["--range", "1", "2", ""]), ("length_ignore", ["--docstring-length", "1", "1", ""]), ("class_attribute_wrap", NO_ARGS), ("issue_51", NO_ARGS), ("issue_51_2", NO_ARGS), ( "issue_79", NO_ARGS + [ "--wrap-summaries", "100", "--wrap-descriptions", "100", ], ), ("issue_97", NO_ARGS), ("issue_97_2", NO_ARGS), ("issue_130", NO_ARGS), ("issue_139", NO_ARGS), ("issue_139_2", NO_ARGS), ("issue_156", NO_ARGS), ("issue_156_2", NO_ARGS), ("issue_156_173", NO_ARGS), ("issue_157_7", ["--wrap-descriptions", "88", ""]), ("issue_157_8", ["--wrap-descriptions", "88", ""]), ("issue_157_9", ["--wrap-descriptions", "88", ""]), ("issue_157_10", ["--wrap-descriptions", "88", ""]), ("issue_176", NO_ARGS), ("issue_176_black", NO_ARGS), ("issue_187", NO_ARGS), ("issue_203", NO_ARGS), ("issue_243", NO_ARGS), ("two_lines_between_stub_classes", NO_ARGS), ("two_lines_between_stub_classes_with_preceding_comment", NO_ARGS), ("ellipses_is_code_line", NO_ARGS), ("do_not_break_f_string_double_quotes", NO_ARGS), ("do_not_break_f_string_single_quotes", NO_ARGS), ("issue_331_black_module_docstring", ["--black", ""]), ], ) def test_do_format_code(test_key, test_args, args): uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) source = TEST_STRINGS[test_key]["source"] expected = TEST_STRINGS[test_key]["expected"] result = uut._do_format_code(source) assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" docformatter-1.7.8/tests/formatter/test_do_format_docstring.py000066400000000000000000000202161517155121300250260ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.formatter.test_do_format_docstring.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Module for testing the Formatter _do_format_docstring method.""" # Standard Library Imports import contextlib import itertools import random import sys with contextlib.suppress(ImportError): if sys.version_info >= (3, 11): # Standard Library Imports import tomllib else: # Third Party Imports import tomli as tomllib # Third Party Imports import pytest # docformatter Package Imports from docformatter.format import Formatter # docformatter Local Imports from .. import generate_random_docstring NO_ARGS = [""] WRAP_DESC_72 = ["--wrap-descriptions", "72", ""] WRAP_DESC_88 = ["--wrap-descriptions", "88", ""] WRAP_BOTH_88 = ["--wrap-descriptions", "88", "--wrap-summaries", "88", ""] with open("tests/_data/string_files/do_format_docstrings.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) @pytest.mark.integration @pytest.mark.order(4) @pytest.mark.parametrize( "test_key, args", [ ("one_line", NO_ARGS), ("summary_end_quote", NO_ARGS), ("bad_indentation", ["--wrap-descriptions", "44", ""]), ("too_much_indentation", NO_ARGS), ("trailing_whitespace", ["--wrap-descriptions", "52", ""]), ("empty_docstring", NO_ARGS), ("no_summary_period", NO_ARGS), ("single_quotes", NO_ARGS), ("single_quotes_multiline", NO_ARGS), pytest.param( "skip_underlined_summary", NO_ARGS, marks=pytest.mark.skip( reason="LEGACY: Underlined summaries should now be processed as " "section headers." ), ), ("no_blank", NO_ARGS), ("presummary_newline", ["--pre-summary-newline", ""]), ("summary_multiline", ["--make-summary-multi-line", ""]), ("presummary_space", ["--pre-summary-space", ""]), ("quote_no_space_black", ["--black", ""]), ("quote_space_black", ["--black", ""]), ("quote_space_multiline_black", ["--black", ""]), ("epytext", ["--style", "epytext"] + WRAP_BOTH_88), ("epytext_numpy", ["--style", "numpy"] + WRAP_BOTH_88), ("sphinx", ["--style", "sphinx"] + WRAP_BOTH_88), ("sphinx_numpy", ["--style", "numpy"] + WRAP_BOTH_88), ("numbered_list", WRAP_DESC_72), ("parameter_dash", WRAP_DESC_72), ("parameter_colon", ["--style", "numpy"] + WRAP_DESC_72), ("many_short_columns", NO_ARGS), ("inline", WRAP_DESC_72), ("inline_short", WRAP_DESC_72), ("inline_long", WRAP_DESC_72), ("only_link", WRAP_DESC_72), ("weird_punctuation", ["--wrap-summaries", "79", ""]), ("description_wrap", WRAP_DESC_72), ("ignore_doctest", WRAP_DESC_72), ("ignore_summary_doctest", WRAP_DESC_72), ("same_indentation_doctest", WRAP_DESC_72), ( "force_wrap", ["--wrap-descriptions", "72", "--wrap-summaries", "50", "--force-wrap", ""], ), ("summary_wrap_tab", ["--wrap-summaries", "30", "--tab-width", "4", ""]), ( "one_line_wrap_newline", ["--wrap-summaries", "69", "--close-quotes-on-newline", ""], ), ( "one_line_no_wrap", ["--wrap-summaries", "88", "--close-quotes-on-newline", ""], ), ("issue_75", WRAP_DESC_72), ("issue_75_2", WRAP_DESC_72), ("issue_75_3", WRAP_DESC_72), ("issue_127", ["--wrap-descriptions", "120", "--wrap-summaries", "120", ""]), ("issue_140", WRAP_DESC_72), ("issue_140_2", WRAP_DESC_72), ("issue_140_3", WRAP_DESC_72), ("issue_145", WRAP_DESC_72), ("issue_150", WRAP_DESC_72), ("issue_157", NO_ARGS), ("issue_157_url", WRAP_DESC_88), ("issue_157_2", WRAP_DESC_88), ("issue_157_3", WRAP_DESC_88), ("issue_157_4", WRAP_DESC_88), ("issue_157_5", WRAP_DESC_88), ("issue_157_6", WRAP_DESC_88), ("issue_157_11", WRAP_DESC_88), ("issue_159", WRAP_BOTH_88), ("issue_159", WRAP_BOTH_88), ("issue_180", WRAP_BOTH_88), ("issue_189", WRAP_DESC_72), ("issue_193", ["--non-cap", "eBay", "iPad", "-c", ""]), ("issue_199", NO_ARGS), ("issue_210", NO_ARGS), ("issue_218", NO_ARGS), ("issue_230", ["--style", "sphinx"] + WRAP_BOTH_88), ("issue_215", WRAP_BOTH_88), ("issue_217_222", WRAP_BOTH_88), ("issue_224", WRAP_BOTH_88), ("issue_228", WRAP_BOTH_88), ("issue_229", WRAP_BOTH_88), ("issue_229_2", WRAP_BOTH_88), ("issue_234", WRAP_BOTH_88), ("issue_235", WRAP_BOTH_88), ("issue_239", [""]), ("issue_239_sphinx", WRAP_BOTH_88), ("issue_245", WRAP_BOTH_88), ("issue_250", WRAP_BOTH_88), ( "issue_253", [ "--wrap-descriptions", "120", "--wrap-summaries", "120", "--pre-summary-newline", "--black", "", ], ), ("issue_263_sphinx", NO_ARGS), ("issue_263_epytext", ["-s", "epytext"] + NO_ARGS), ("issue_271", ["--pre-summary-newline"] + WRAP_BOTH_88), ("issue_259", NO_ARGS), ("issue_259_black", ["--black"] + NO_ARGS), ("issue_259_pre_summary_space", ["--pre-summary-space"] + NO_ARGS), ("issue_259_pre_summary_newline", ["--pre-summary-newline"] + NO_ARGS), ], ) def test_do_format_docstring(test_key, test_args, args): uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) source = TEST_STRINGS[test_key]["source"] expected = TEST_STRINGS[test_key]["expected"] if test_key == "summary_wrap_tab": _indentation = "\t\t" else: _indentation = " " result = uut._do_format_docstring( _indentation, source, ) assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" @pytest.mark.integration @pytest.mark.parametrize("args", [[""]]) def test_do_format_docstring_random_with_wrap( test_args, args, ): uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) # This function uses `random` so make sure each run of this test is # repeatable. random.seed(0) min_line_length = 50 for max_length, num_indents in itertools.product( range(min_line_length, 100), range(20) ): indentation = " " * num_indents uut.args.wrap_summaries = max_length formatted_text = indentation + uut._do_format_docstring( indentation=indentation, docstring=generate_random_docstring(max_word_length=min_line_length // 2), ) for line in formatted_text.split("\n"): # It is not the formatter's fault if a word is too long to # wrap. if len(line.split()) > 1: assert len(line) <= max_length docformatter-1.7.8/tests/formatter/test_format_functions.py000066400000000000000000000246021517155121300243630ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.formatter.test_format_functions.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Module for testing formatting functions.""" # Standard Library Imports import contextlib import sys import tokenize from io import BytesIO, StringIO with contextlib.suppress(ImportError): if sys.version_info >= (3, 11): # Standard Library Imports import tomllib else: # Third Party Imports import tomli as tomllib # Third Party Imports import pytest # docformatter Package Imports from docformatter import format as _format with open("tests/_data/string_files/format_functions.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) def _get_tokens(source): return list(tokenize.tokenize(BytesIO(source.encode()).readline)) def _get_docstring_token_and_index(tokens): for i, tok in enumerate(tokens): if tok.type == tokenize.STRING: return i raise ValueError("No docstring found in token stream.") @pytest.mark.unit @pytest.mark.parametrize( "test_key", [ "module_docstring_followed_by_string", "module_docstring_followed_by_code", "module_docstring_followed_by_comment_then_code", "module_docstring_followed_by_comment_then_string", "module_docstring_in_black", ], ) def test_module_docstring_newlines(test_key): expected = TEST_STRINGS[test_key]["expected"] result = _format._get_module_docstring_newlines() assert ( result == expected ), f"\nFailed {test_key}:\nExpected {expected}\nGot {result}" @pytest.mark.unit @pytest.mark.order(4) @pytest.mark.parametrize( "test_key, classifier", [ ( "class_docstring_followed_by_statement", _format._get_class_docstring_newlines, ), ("class_docstring_followed_by_def", _format._get_class_docstring_newlines), ("class_docstring_with_decorator", _format._get_class_docstring_newlines), ("class_docstring_with_class_variable", _format._get_class_docstring_newlines), ("function_with_expr", _format._get_function_docstring_newlines), ("function_with_inner_def", _format._get_function_docstring_newlines), ("function_with_inner_async_def", _format._get_function_docstring_newlines), ("function_with_decorator_and_def", _format._get_function_docstring_newlines), ( "function_with_decorator_and_async_def", _format._get_function_docstring_newlines, ), ( "function_docstring_with_inner_class", _format._get_function_docstring_newlines, ), ("attribute_docstring_single_line", _format._get_attribute_docstring_newlines), ("attribute_docstring_multi_line", _format._get_attribute_docstring_newlines), ( "attribute_docstring_outside_class", _format._get_attribute_docstring_newlines, ), ( "attribute_docstring_inside_method", _format._get_attribute_docstring_newlines, ), ("attribute_docstring_with_comment", _format._get_attribute_docstring_newlines), ( "attribute_docstring_multiple_assignments", _format._get_attribute_docstring_newlines, ), ("attribute_docstring_equiv_expr", _format._get_attribute_docstring_newlines), ], ) def test_get_docstring_newlines(test_key, classifier): source = TEST_STRINGS[test_key]["source"] expected = TEST_STRINGS[test_key]["expected"] tokens = _get_tokens(source) index = _get_docstring_token_and_index(tokens) result = classifier(tokens, index) assert ( result == expected ), f"\nFailed {test_key}:\nExpected {expected}\nGot {result}" @pytest.mark.unit @pytest.mark.parametrize( "test_key", [ "get_num_rows_columns", ], ) def test_get_num_rows_columns(test_key): token = tokenize.TokenInfo( type=TEST_STRINGS[test_key]["token"][0], string=TEST_STRINGS[test_key]["token"][1], start=TEST_STRINGS[test_key]["token"][2], end=TEST_STRINGS[test_key]["token"][3], line=TEST_STRINGS[test_key]["token"][4], ) expected = TEST_STRINGS[test_key]["expected"] result = _format._get_num_rows_columns(token) assert ( result[0] == expected[0] ), f"\nFailed {test_key}\nExpected {expected[0]} rows\nGot {result[0]} rows" assert ( result[1] == expected[1] ), f"\nFailed {test_key}\nExpected {expected[1]} columns\nGot {result[1]} columns" @pytest.mark.unit @pytest.mark.parametrize( "test_key", [ "get_start_end_indices", ], ) def test_get_start_end_indices(test_key): prev_token = tokenize.TokenInfo( type=TEST_STRINGS[test_key]["prev_token"][0], string=TEST_STRINGS[test_key]["prev_token"][1], start=TEST_STRINGS[test_key]["prev_token"][2], end=TEST_STRINGS[test_key]["prev_token"][3], line=TEST_STRINGS[test_key]["prev_token"][4], ) token = tokenize.TokenInfo( type=TEST_STRINGS[test_key]["token"][0], string=TEST_STRINGS[test_key]["token"][1], start=TEST_STRINGS[test_key]["token"][2], end=TEST_STRINGS[test_key]["token"][3], line=TEST_STRINGS[test_key]["token"][4], ) expected = TEST_STRINGS[test_key]["expected"] result = _format._get_start_end_indices(token, prev_token, 3, 17) for i in 0, 1: for j in 0, 1: assert ( result[i][j] == expected[i][j] ), f"\nFailed {test_key}\nExpected {expected[i][j]}\nGot {result[i][j]}" @pytest.mark.unit @pytest.mark.parametrize( "test_key, block", [ ("do_remove_preceding_blank_lines_module", [(0, 4, "module")]), ("do_remove_preceding_blank_lines_class", [(0, 7, "class")]), ("do_remove_preceding_blank_lines_function", [(0, 9, "function")]), ("do_remove_preceding_blank_lines_attribute", [(1, 6, "attribute")]), ], ) def test_do_remove_preceding_blank_lines(test_key, block): source = TEST_STRINGS[test_key]["source"] expected = TEST_STRINGS[test_key]["expected"] tokens = list(tokenize.generate_tokens(StringIO(source, newline="").readline)) result = _format._do_remove_preceding_blank_lines(tokens, block) for _idx in range(len(result)): assert ( result[_idx].string == expected[_idx] ), f"\nFailed {test_key}\nExpected {expected[_idx]}\nGot {result[_idx].string}" @pytest.mark.integration @pytest.mark.order(5) @pytest.mark.parametrize( "test_key", [ "get_newlines_by_type_module_docstring", "get_newlines_by_type_module_docstring_black", "get_newlines_by_type_class_docstring", "get_newlines_by_type_function_docstring", "get_newlines_by_type_attribute_docstring", ], ) def test_get_newlines_by_type(test_key): source = TEST_STRINGS[test_key]["source"] expected = TEST_STRINGS[test_key]["expected"] tokens = _get_tokens(source) index = _get_docstring_token_and_index(tokens) result = _format._get_newlines_by_type(tokens, index) assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" @pytest.mark.integration @pytest.mark.order(4) @pytest.mark.parametrize( "test_key", [ "get_unmatched_start_end_indices", ], ) def test_get_unmatched_start_end_indices(test_key): prev_token = tokenize.TokenInfo( type=TEST_STRINGS[test_key]["prev_token"][0], string=TEST_STRINGS[test_key]["prev_token"][1], start=TEST_STRINGS[test_key]["prev_token"][2], end=TEST_STRINGS[test_key]["prev_token"][3], line=TEST_STRINGS[test_key]["prev_token"][4], ) token = tokenize.TokenInfo( type=TEST_STRINGS[test_key]["token"][0], string=TEST_STRINGS[test_key]["token"][1], start=TEST_STRINGS[test_key]["token"][2], end=TEST_STRINGS[test_key]["token"][3], line=TEST_STRINGS[test_key]["token"][4], ) expected = TEST_STRINGS[test_key]["expected"] result = _format._get_unmatched_start_end_indices(token, prev_token, 4) for i in 0, 1: for j in 0, 1: assert ( result[i][j] == expected[i][j] ), f"\nFailed {test_key}\nExpected {expected[i][j]}\nGot {result[i][j]}" @pytest.mark.integration @pytest.mark.order(5) @pytest.mark.parametrize( "test_key", [ "do_update_token_indices", ], ) def test_do_update_token_indices(test_key): tokens = [] for token in TEST_STRINGS[test_key]["tokens"]: tokens.append( tokenize.TokenInfo( type=token[0], string=token[1], start=token[2], end=token[3], line=token[4], ) ) expected = TEST_STRINGS[test_key]["expected"] result = _format._do_update_token_indices(tokens) for idx, _expected in enumerate(expected): # We convert the start and end tuples to lists because we can't store tuples # in a TOML file. assert list(result[idx].start) == _expected[0], ( f"\nFailed {test_key} start index\n" f"Expected {expected[0]}\nGot {result[idx].start}" ) assert list(result[idx].end) == _expected[1], ( f"\nFailed {test_key} end index\n" f"Expected {expected[1]}\nGot {result[idx].end}" ) docformatter-1.7.8/tests/formatter/test_format_methods.py000066400000000000000000000206551517155121300240220ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.formatter.test_format_methods.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Module for testing various Formatter class methods.""" # Standard Library Imports import contextlib import sys import tokenize from io import BytesIO from tokenize import TokenInfo with contextlib.suppress(ImportError): if sys.version_info >= (3, 11): # Standard Library Imports import tomllib else: # Third Party Imports import tomli as tomllib # Third Party Imports import pytest # docformatter Package Imports from docformatter.format import Formatter with open("tests/_data/string_files/format_methods.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) def _get_tokens(source): return list(tokenize.tokenize(BytesIO(source.encode()).readline)) def _get_docstring_token_and_index(tokens): for i, tok in enumerate(tokens): if tok.type == tokenize.STRING: return i raise ValueError("No docstring found in token stream.") @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_do_add_blank_lines(args): uut = Formatter( args, sys.stderr, sys.stdin, sys.stdout, ) uut._do_add_blank_lines(2, 2, 2) assert uut.new_tokens == [ TokenInfo(type=4, string="\n", start=(2, 0), end=(2, 1), line="\n"), TokenInfo(type=4, string="\n", start=(3, 0), end=(3, 1), line="\n"), ] @pytest.mark.unit @pytest.mark.parametrize( "test_key", [ "do_add_unformatted_docstring", ], ) @pytest.mark.parametrize("args", [[""]]) def test_do_add_unformatted_docstring(test_key, args): uut = Formatter( args, sys.stderr, sys.stdin, sys.stdout, ) token = tokenize.TokenInfo( type=TEST_STRINGS[test_key]["token"][0], string=TEST_STRINGS[test_key]["token"][1], start=tuple(TEST_STRINGS[test_key]["token"][2]), end=tuple(TEST_STRINGS[test_key]["token"][3]), line=TEST_STRINGS[test_key]["token"][4], ) expected = [ TokenInfo( type=3, string='"""This is a docstring.\\n\\n\\n That should be on less lines\\n"""', start=(3, 4), end=(6, 7), line=' """This is a docstring.\\n\\n\\n That should be on less lines\\n"""', ), TokenInfo( type=4, string="\n", start=(6, 7), end=(6, 8), line=' """This is a docstring.\\n\\n\\n That should be on less lines\\n"""', ), ] uut._do_add_unformatted_docstring(token, "function") assert ( uut.new_tokens == expected ), f"\nFailed {test_key}\nExpected {expected}\nGot {uut.new_tokens}" @pytest.mark.integration @pytest.mark.order(5) @pytest.mark.parametrize( "test_key, args", [ ("do_add_formatted_docstring", [""]), ], ) def test_do_add_formatted_docstring(test_key, test_args, args): uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) token = tokenize.TokenInfo( type=TEST_STRINGS[test_key]["token"][0], string=TEST_STRINGS[test_key]["token"][1], start=tuple(TEST_STRINGS[test_key]["token"][2]), end=tuple(TEST_STRINGS[test_key]["token"][3]), line=TEST_STRINGS[test_key]["token"][4], ) next_token = tokenize.TokenInfo( type=TEST_STRINGS[test_key]["next_token"][0], string=TEST_STRINGS[test_key]["next_token"][1], start=tuple(TEST_STRINGS[test_key]["next_token"][2]), end=tuple(TEST_STRINGS[test_key]["next_token"][3]), line=TEST_STRINGS[test_key]["next_token"][4], ) expected = [ tokenize.TokenInfo( type=3, string='"""This is a docstring.\\n."""', start=(3, 4), end=(6, 7), line=' """This is a docstring.\\n."""\n', ), tokenize.TokenInfo( type=4, string="\n", start=(6, 7), end=(6, 8), line=' """This is a docstring.\\n."""\n', ), tokenize.TokenInfo(type=4, string="\n", start=(7, 0), end=(7, 1), line="\n"), ] uut._do_add_formatted_docstring(token, next_token, "function", 1) assert ( uut.new_tokens == expected ), f"\nFailed {test_key}\nExpected {expected}\nGot {uut.new_tokens}" @pytest.mark.integration @pytest.mark.order(3) @pytest.mark.parametrize( "test_key, args", [ ("do_format_oneline_docstring", [""]), ("do_format_oneline_docstring_that_ends_in_quote", [""]), ("do_format_oneline_docstring_with_wrap", ["--wrap-summaries", "72", ""]), ( "do_format_oneline_docstring_with_quotes_newline", ["--close-quotes-on-newline", ""], ), ( "do_format_oneline_docstring_make_multiline", ["--make-summary-multi-line", ""], ), ], ) def test_format_one_line_docstring(test_key, test_args, args): uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) source = TEST_STRINGS[test_key]["source"] expected = TEST_STRINGS[test_key]["expected"] result = uut._do_format_oneline_docstring( " ", source, '"""', ) assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" @pytest.mark.integration @pytest.mark.parametrize( "test_key, args", [ ("do_format_multiline_docstring", [""]), ( "do_format_multiline_docstring_pre_summary_newline", ["--pre-summary-newline", ""], ), ( "do_format_multiline_docstring_post_description_blank", ["--blank", ""], ), ], ) def test_format_multiline_docstring(test_key, test_args, args): uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) source = TEST_STRINGS[test_key]["source"] expected = TEST_STRINGS[test_key]["expected"] result = uut._do_format_multiline_docstring( " ", source[0], source[1], '"""', ) assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" @pytest.mark.integration @pytest.mark.order(6) @pytest.mark.parametrize( "test_key, args", [ ("do_rewrite_docstring_blocks", [""]), ], ) def test_do_rewrite_docstring_blocks(test_key, test_args, args): uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) tokens = [] for token in TEST_STRINGS[test_key]["tokens"]: tokens.append( tokenize.TokenInfo( type=token[0], string=token[1], start=tuple(token[2]), end=tuple(token[3]), line=token[4], ) ) expected = [] for token in TEST_STRINGS[test_key]["expected"]: expected.append( tokenize.TokenInfo( type=token[0], string=token[1], start=tuple(token[2]), end=tuple(token[3]), line=token[4], ) ) uut._do_rewrite_docstring_blocks(tokens) assert ( uut.new_tokens == expected ), f"\nFailed {test_key}\nExpected {expected}\nGot {uut.new_tokens}" docformatter-1.7.8/tests/patterns/000077500000000000000000000000001517155121300172235ustar00rootroot00000000000000docformatter-1.7.8/tests/patterns/__init__.py000066400000000000000000000000001517155121300213220ustar00rootroot00000000000000docformatter-1.7.8/tests/patterns/test_field_patterns.py000066400000000000000000000046461517155121300236510ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.patterns.test_field_patterns.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Module for testing the field list pattern detection functions.""" # Standard Library Imports import contextlib import sys with contextlib.suppress(ImportError): if sys.version_info >= (3, 11): # Standard Library Imports import tomllib else: # Third Party Imports import tomli as tomllib # Third Party Imports import pytest # docformatter Package Imports from docformatter.patterns import is_field_list with open("tests/_data/string_files/field_patterns.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) @pytest.mark.integration @pytest.mark.order(3) @pytest.mark.parametrize( "test_key", [ "is_epytext_field_list", "is_epytext_field_list_sphinx_style", "is_sphinx_field_list", "is_sphinx_field_list_epytext_style", "is_numpy_field_list", ], ) def test_is_field_list(test_key): """Test the is_field_list function.""" text = TEST_STRINGS[test_key]["instring"] style = TEST_STRINGS[test_key]["style"] expected = TEST_STRINGS[test_key]["expected"] result = is_field_list(text, style) assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" docformatter-1.7.8/tests/patterns/test_header_patterns.py000066400000000000000000000110151517155121300240020ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.patterns.test_header_patterns.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Module for testing the field list pattern detection functions.""" # Standard Library Imports import contextlib import sys with contextlib.suppress(ImportError): if sys.version_info >= (3, 11): # Standard Library Imports import tomllib else: # Third Party Imports import tomli as tomllib # Third Party Imports import pytest # docformatter Package Imports from docformatter.patterns import ( is_alembic_header, is_numpy_section_header, is_rest_section_header, ) with open("tests/_data/string_files/header_patterns.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) @pytest.mark.unit @pytest.mark.parametrize( "test_key, patternizer", [ ("is_alembic_header", is_alembic_header), ("is_not_alembic_header_epytext", is_alembic_header), ("is_not_alembic_header_numpy", is_alembic_header), ("is_not_alembic_header_google", is_alembic_header), ("is_numpy_section_header_parameters", is_numpy_section_header), ("is_numpy_section_header_returns", is_numpy_section_header), ("is_numpy_section_header_yields", is_numpy_section_header), ("is_numpy_section_header_raises", is_numpy_section_header), ("is_numpy_section_header_receives", is_numpy_section_header), ("is_numpy_section_header_other_parameters", is_numpy_section_header), ("is_numpy_section_header_warns", is_numpy_section_header), ("is_numpy_section_header_warnings", is_numpy_section_header), ("is_numpy_section_header_see_also", is_numpy_section_header), ("is_numpy_section_header_examples", is_numpy_section_header), ("is_numpy_section_header_notes", is_numpy_section_header), ("is_not_numpy_section_header", is_numpy_section_header), ("is_not_numpy_section_header_wrong_dashes", is_numpy_section_header), ("is_rest_section_header_pound", is_rest_section_header), ("is_rest_section_header_star", is_rest_section_header), ("is_rest_section_header_equal", is_rest_section_header), ("is_rest_section_header_dash", is_rest_section_header), ("is_rest_section_header_circumflex", is_rest_section_header), ("is_rest_section_header_single_quote", is_rest_section_header), ("is_rest_section_header_double_quote", is_rest_section_header), ("is_rest_section_header_plus", is_rest_section_header), ("is_rest_section_header_underscore", is_rest_section_header), ("is_rest_section_header_tilde", is_rest_section_header), ("is_rest_section_header_backtick", is_rest_section_header), ("is_rest_section_header_period", is_rest_section_header), ("is_rest_section_header_colon", is_rest_section_header), ("is_not_rest_section_header_unknown_adornments", is_rest_section_header), ], ) def test_is_header(test_key, patternizer): source = TEST_STRINGS[test_key]["instring"] expected = TEST_STRINGS[test_key]["expected"] result = patternizer(source) if result: assert ( result.group(0) == expected ), f"\nFailed {test_key}\nExpected {expected}\nGot {result.group(0)}" else: result = "None" if result is None else result assert ( result == expected ), f"\nFailed {test_key}\nExpected {expected}\nGot {result}" docformatter-1.7.8/tests/patterns/test_list_patterns.py000066400000000000000000000057171517155121300235410ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.patterns.test_list_patterns.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Module for testing the list pattern detection functions.""" # Standard Library Imports import contextlib import sys with contextlib.suppress(ImportError): if sys.version_info >= (3, 11): # Standard Library Imports import tomllib else: # Third Party Imports import tomli as tomllib # Third Party Imports import pytest # docformatter Package Imports from docformatter.patterns import is_type_of_list with open("tests/_data/string_files/list_patterns.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) @pytest.mark.integration @pytest.mark.order(3) @pytest.mark.parametrize( "test_key", [ "is_bullet_list", "is_enum_list", "is_option_list", "is_option_list_indented", "is_list_with_single_hyphen", "is_list_with_double_hyphen", "is_list_with_at_sign", "is_heuristic_list", "is_not_list_sphinx_style", "is_sphinx_list_numpy_style", "is_numpy_list_sphinx_style", "is_google_list_numpy_style", "is_type_of_list_strict_wrap", "is_type_of_list_non_strict_wrap", "is_literal_block", "is_reST_header", "is_type_of_list_alembic_header", "is_epytext_field_list", "is_sphinx_field_list", "is_numpy_section_in_docstring_issue_338", "is_rest_section_in_docstring", ], ) def test_is_type_of_list(test_key): text = TEST_STRINGS[test_key]["instring"] strict = TEST_STRINGS[test_key]["strict"] style = TEST_STRINGS[test_key]["style"] expected = TEST_STRINGS[test_key]["expected"] result = is_type_of_list(text, strict, style) assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" docformatter-1.7.8/tests/patterns/test_misc_patterns.py000066400000000000000000000061671517155121300235210ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.patterns.test_misc_patterns.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Module for testing the miscellaneous pattern detection functions.""" # Standard Library Imports import contextlib import sys with contextlib.suppress(ImportError): if sys.version_info >= (3, 11): # Standard Library Imports import tomllib else: # Third Party Imports import tomli as tomllib # Third Party Imports import pytest # docformatter Package Imports from docformatter.patterns import ( is_probably_beginning_of_sentence, is_some_sort_of_code, ) with open("tests/_data/string_files/misc_patterns.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) @pytest.mark.unit @pytest.mark.parametrize( "test_key, patternizer", [ ("is_some_sort_of_code", is_some_sort_of_code), pytest.param( "is_some_sort_of_code_python", is_some_sort_of_code, marks=pytest.mark.skip( reason="The is_some_sort_of_code function is simply looking for long " "words. This function needs to be re-written to look for actual code " "patterns." ), ), ("is_probably_beginning_of_sentence", is_probably_beginning_of_sentence), ("is_not_probably_beginning_of_sentence", is_probably_beginning_of_sentence), ( "is_probably_beginning_of_sentence_pydoc_ref", is_probably_beginning_of_sentence, ), ], ) def test_miscellaneous_patterns(test_key, patternizer): source = TEST_STRINGS[test_key]["instring"] expected = TEST_STRINGS[test_key]["expected"] result = patternizer(source) if result: assert ( result == expected ), f"\nFailed {test_key}\nExpected {expected}\nGot {result}" else: result = "None" if result is None else result assert ( result == expected ), f"\nFailed {test_key}\nExpected {expected}\nGot {result}" docformatter-1.7.8/tests/patterns/test_rest_patterns.py000066400000000000000000000057711517155121300235430ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.patterns.test_rest_patterns.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Module for testing the reST directive pattern detection functions.""" # Standard Library Imports import contextlib import sys with contextlib.suppress(ImportError): if sys.version_info >= (3, 11): # Standard Library Imports import tomllib else: # Third Party Imports import tomli as tomllib # Third Party Imports import pytest # docformatter Package Imports from docformatter.patterns import do_find_rest_directives, do_find_inline_rest_markup with open("tests/_data/string_files/rest_patterns.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) @pytest.mark.unit @pytest.mark.parametrize( "test_key", [ "is_double_dot_directive", "is_double_dot_directive_indented", ], ) def test_do_find_rest_directives(test_key): source = TEST_STRINGS[test_key]["instring"] expected = TEST_STRINGS[test_key]["expected"] result = do_find_rest_directives(source) assert ( result[0][0] == expected[0] ), f"\nFailed {test_key}\nExpected {expected[0]}\nGot {result[0][0]}" assert ( result[0][1] == expected[1] ), f"\nFailed {test_key}\nExpected {expected[0]}\nGot {result[0][1]}" @pytest.mark.unit @pytest.mark.parametrize( "test_key", [ "is_inline_directive", "is_double_backtick_directive", ], ) def test_do_find_inline_rest_markup(test_key): source = TEST_STRINGS[test_key]["instring"] expected = TEST_STRINGS[test_key]["expected"] result = do_find_inline_rest_markup(source) print(result) assert ( result[0][0] == expected[0] ), f"\nFailed {test_key}\nExpected {expected[0]}\nGot {result[0][0]}" assert ( result[0][1] == expected[1] ), f"\nFailed {test_key}\nExpected {expected[0]}\nGot {result[0][1]}" docformatter-1.7.8/tests/patterns/test_url_patterns.py000066400000000000000000000077731517155121300233740ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.patterns.test_url_patterns.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Module for testing the URL pattern detection functions.""" # Standard Library Imports import contextlib import sys with contextlib.suppress(ImportError): if sys.version_info >= (3, 11): # Standard Library Imports import tomllib else: # Third Party Imports import tomli as tomllib # Third Party Imports import pytest # docformatter Package Imports from docformatter.patterns import do_find_links, do_skip_link with open("tests/_data/string_files/url_patterns.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) @pytest.mark.unit @pytest.mark.parametrize( "test_key", [ "apple_filing_protocol", "network_filing_system", "samba_filing_system", "apt", "bitcoin", "chrome", "java_compressed_archive", "concurrent_version_system", "git", "subversion", "domain_name_system", "file_transfer_protocol", "secure_file_transfer_protocol", "ssh_file_transfer_protocol", "finger", "rsync", "telnet", "virtual_network_computing", "extensible_resource_identifier", "fish", "ssh", "webdav_transfer_protocol", "hypertext_transfer_protocol", "secure_hypertext_transfer_protocol", "obsolete_secure_hypertext_transfer_protocol", "imap", "smtp", "pop", "internet_printing_protocol", "secure_internet_printing_protocol", "internet_relay_chat", "internet_relay_chat_v6", "secure_internet_relay_chat", "short_message_service", "extensible_messaging_and_presence_protocol", "ldap", "secure_ldap", "amazon_s3", "usenet", "nntp", "session_initiation_protocol", "secure_session_initiation_protocol", "simple_network_management_protocol", ], ) def test_do_find_links(test_key): source = TEST_STRINGS[test_key]["instring"] expected = TEST_STRINGS[test_key]["expected"] result = do_find_links(source) assert ( result[0][0] == expected[0] ), f"\nFailed {test_key}\nExpected {expected[0]}\nGot {result[0][0]}" assert ( result[0][1] == expected[1] ), f"\nFailed {test_key}\nExpected {expected[0]}\nGot {result[0][1]}" @pytest.mark.unit @pytest.mark.parametrize( "test_key, index", [ ("only_link_patterns", (70, 76)), ("only_link_patterns", (137, 145)), ("already_wrapped_url", (70, 117)), ], ) def test_do_skip_link(test_key, index): source = TEST_STRINGS[test_key]["instring"] expected = TEST_STRINGS[test_key]["expected"] result = do_skip_link(source, index) assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" docformatter-1.7.8/tests/test_classify_functions.py000066400000000000000000000146071517155121300227110ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.test_classify_functions.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Module for testing the classification functions.""" # Standard Library Imports import contextlib import sys import tokenize from io import BytesIO with contextlib.suppress(ImportError): if sys.version_info >= (3, 11): # Standard Library Imports import tomllib else: # Third Party Imports import tomli as tomllib # Third Party Imports import pytest # docformatter Package Imports from docformatter.classify import ( do_find_docstring_blocks, is_attribute_docstring, is_class_docstring, is_closing_quotes, is_code_line, is_definition_line, is_f_string, is_function_or_method_docstring, is_inline_comment, is_line_following_indent, is_module_docstring, is_nested_definition_line, is_newline_continuation, is_string_variable, ) with open("tests/_data/string_files/classify_functions.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) def get_tokens(source: str) -> list[tokenize.TokenInfo]: return list(tokenize.tokenize(BytesIO(source.encode()).readline)) def get_string_index(tokens: list[tokenize.TokenInfo]) -> int: for i, tok in enumerate(tokens): if tok.type == tokenize.STRING: return i raise ValueError("No string token found.") def build_token(prefix: str, test_key: str) -> tokenize.TokenInfo: """Build a tokenize.TokenInfo from test data using a prefix ('' or 'prev_').""" data = TEST_STRINGS[test_key] return tokenize.TokenInfo( type=data[f"{prefix}type"], string=data[f"{prefix}string"], start=tuple(data[f"{prefix}start"]), end=tuple(data[f"{prefix}end"]), line=data[f"{prefix}line"], ) @pytest.mark.unit @pytest.mark.parametrize( "test_key, classifier", [ ("is_module_docstring", is_module_docstring), ("is_class_docstring", is_class_docstring), ("is_method_docstring", is_function_or_method_docstring), ("is_function_docstring", is_function_or_method_docstring), ("is_attribute_docstring", is_attribute_docstring), ("is_not_attribute_docstring", is_attribute_docstring), ], ) def test_docstring_classifiers(test_key, classifier): source = TEST_STRINGS[test_key]["instring"] expected = TEST_STRINGS[test_key]["expected"] tokens = get_tokens(source) index = get_string_index(tokens) result = classifier(tokens, index) assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" @pytest.mark.unit @pytest.mark.parametrize( "test_key,classifier", [ ("is_code_line", is_code_line), ("is_closing_quotes", is_closing_quotes), ("is_definition_line_class", is_definition_line), ("is_definition_line_function", is_definition_line), ("is_definition_line_async_function", is_definition_line), ("is_not_definition_line_function", is_definition_line), ("is_inline_comment", is_inline_comment), ("is_line_following_indent", is_line_following_indent), ("is_nested_definition_line_class", is_nested_definition_line), ("is_nested_definition_line_function", is_nested_definition_line), ("is_nested_definition_line_async_function", is_nested_definition_line), ("is_not_nested_definition_line_function", is_nested_definition_line), ("is_newline_continuation", is_newline_continuation), ("is_string_variable", is_string_variable), ], ) def test_misc_classifiers(test_key, classifier): token = build_token("", test_key) try: prev_token = build_token("prev_", test_key) result = classifier(token, prev_token) except KeyError: result = classifier(token) expected = TEST_STRINGS[test_key]["expected"] assert result == expected, f"Failed {test_key}\nExpected {expected}\nGot {result}" @pytest.mark.unit @pytest.mark.skipif(sys.version_info < (3, 12), reason="requires Python 3.12 or higher") def test_is_f_string(): """Test is_f_string classifier (requires Python 3.12+).""" token = build_token("", "is_f_string") prev_token = build_token("prev_", "is_f_string") if sys.version_info >= (3, 13): expected = TEST_STRINGS["is_f_string"]["expected313"] else: expected = TEST_STRINGS["is_f_string"]["expected"] result = is_f_string(token, prev_token) assert result == expected, f"Failed is_f_string\nExpected {expected}\nGot {result}" @pytest.mark.integration @pytest.mark.order(5) @pytest.mark.parametrize( "test_key, expected", [ ("find_module_docstring", [(0, 1, "module")]), ("find_class_docstring", [(1, 6, "class")]), ("find_function_docstring", [(1, 8, "function")]), ("find_function_docstring_with_decorator", [(4, 11, "function")]), ("find_attribute_docstring", [(1, 5, "attribute")]), ( "find_multiple_docstrings", [(0, 1, "module"), (4, 9, "class"), (12, 20, "function")], ), ], ) def test_find_docstring_blocks(test_key, expected): source = TEST_STRINGS[test_key]["instring"] tokens = get_tokens(source) result = do_find_docstring_blocks(tokens) assert result == expected, f"Failed {test_key}\nExpected {expected}\nGot {result}" docformatter-1.7.8/tests/test_configuration_functions.py000066400000000000000000000436141517155121300237430ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.test_configuration_functions.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Module for testing docformatter's Configurater class.""" # Third Party Imports import pytest # docformatter Package Imports from docformatter.configuration import Configurater WRAP_72 = 72 WRAP_80 = 80 WRAP_88 = 88 @pytest.mark.unit def test_do_read_toml_configuration(): uut = Configurater([""]) uut.config_file = "./tests/_data/pyproject.toml" expected = { "wrap-summaries": "79", "wrap-descriptions": "72", "blank": "False", } uut._do_read_toml_configuration() assert uut.flargs == expected, ( f"\nFailed _do_read_toml_configuration:\n" f"Expected {expected}\n" f"Got {uut.flargs}" ) @pytest.mark.unit @pytest.mark.parametrize( "config_file", [ "./tests/_data/setup.cfg", "./tests/_data/tox.ini", ], ) def test_do_read_parser_configuration(config_file): uut = Configurater([""]) uut.config_file = config_file expected = { "blank": "False", "wrap-summaries": "79", "wrap-descriptions": "72", } uut._do_read_parser_configuration() assert uut.flargs == expected, ( f"\nFailed _do_read_parser_configuration:\n" f"Expected {expected}\n" f"Got {uut.flargs}" ) @pytest.mark.integration @pytest.mark.order(1) @pytest.mark.parametrize( "config_file", [ "./tests/_data/pyproject.toml", "./tests/_data/setup.cfg", "./tests/_data/tox.ini", ], ) def test_do_read_configuration_file(config_file): uut = Configurater([""]) uut.config_file = config_file if config_file == "./tests/_data/pyproject.toml": expected = { "wrap-summaries": "79", "wrap-descriptions": "72", "blank": "false", } else: expected = { "blank": "False", "wrap-summaries": "79", "wrap-descriptions": "72", } uut._do_read_parser_configuration() assert uut.flargs == expected, ( f"\nFailed _do_read_configuration_file:\n" f"Expected {expected}\n" f"Got {uut.flargs}" ) class TestConfigurater: """Class for testing configuration functions.""" @pytest.mark.integration @pytest.mark.order(0) def test_initialize_configurator_with_default(self): """Return a Configurater() instance using default pyproject.toml.""" argb = [ "/path/to/docformatter", "", ] uut = Configurater(argb) uut.do_parse_arguments() assert uut.args_lst == argb assert uut.config_file == "./pyproject.toml" @pytest.mark.integration @pytest.mark.order(0) def test_initialize_configurator_with_pyproject_toml(self): """Return a Configurater() instance loaded from a pyproject.toml.""" argb = [ "/path/to/docformatter", "-c", "--config", "./tests/_data/pyproject.toml", "", ] uut = Configurater(argb) uut.do_parse_arguments() assert uut.args.check assert not uut.args.in_place assert uut.args_lst == argb assert uut.config_file == "./tests/_data/pyproject.toml" assert uut.configuration_file_lst == [ "pyproject.toml", "setup.cfg", "tox.ini", ] assert uut.flargs == { "wrap-summaries": "79", "wrap-descriptions": "72", "blank": "False", } @pytest.mark.integration @pytest.mark.order(0) def test_initialize_configurator_with_setup_cfg(self): """Return docformatter configuration loaded from a setup.cfg file.""" argb = [ "/path/to/docformatter", "-c", "--config", "./tests/_data/setup.cfg", "", ] uut = Configurater(argb) uut.do_parse_arguments() assert uut.config_file == "./tests/_data/setup.cfg" assert uut.flargs == { "blank": "False", "wrap-summaries": "79", "wrap-descriptions": "72", } @pytest.mark.integration @pytest.mark.order(0) def test_initialize_configurator_with_tox_ini(self): """Return docformatter configuration loaded from a tox.ini file.""" argb = [ "/path/to/docformatter", "-c", "--config", "./tests/_data/tox.ini", "", ] uut = Configurater(argb) uut.do_parse_arguments() assert uut.config_file == "./tests/_data/tox.ini" assert uut.flargs == { "blank": "False", "wrap-summaries": "79", "wrap-descriptions": "72", } @pytest.mark.integration @pytest.mark.order(0) def test_unsupported_config_file(self): """Return empty configuration dict when file is unsupported.""" argb = [ "/path/to/docformatter", "-c", "--config", "./tests/conf.py", "", ] uut = Configurater(argb) uut.do_parse_arguments() assert uut.config_file == "./tests/conf.py" assert uut.flargs == {} @pytest.mark.integration @pytest.mark.order(0) def test_cli_override_config_file(self): """Command line arguments override configuration file options.""" argb = [ "/path/to/docformatter", "-c", "--config", "./tests/_data/tox.ini", "--make-summary-multi-line", "--blank", "--wrap-summaries", "88", "", ] uut = Configurater(argb) uut.do_parse_arguments() assert uut.config_file == "./tests/_data/tox.ini" assert uut.flargs == { "blank": "False", "wrap-summaries": "79", "wrap-descriptions": "72", } assert not uut.args.in_place assert uut.args.check assert not uut.args.recursive assert uut.args.exclude is None assert uut.args.wrap_summaries == WRAP_88 assert uut.args.wrap_descriptions == WRAP_72 assert not uut.args.black assert uut.args.post_description_blank assert not uut.args.pre_summary_newline assert not uut.args.pre_summary_space assert uut.args.make_summary_multi_line assert not uut.args.force_wrap assert uut.args.line_range is None assert uut.args.length_range is None assert not uut.args.non_strict @pytest.mark.integration @pytest.mark.order(1) def test_only_format_in_line_range(self, capsys): """Only format docstrings in line range.""" argb = [ "/path/to/docformatter", "-c", "--range", "1", "3", "", ] uut = Configurater(argb) uut.do_parse_arguments() assert uut.args.line_range == [1, 3] @pytest.mark.integration @pytest.mark.order(1) def test_low_line_range_is_zero(self, capsys): """Raise parser error if the first value for the range is zero.""" argb = [ "/path/to/docformatter", "-c", "--range", "0", "10", "", ] uut = Configurater(argb) with pytest.raises(SystemExit): uut.do_parse_arguments() out, err = capsys.readouterr() assert out == "" assert "--range must be positive numbers" in err @pytest.mark.integration @pytest.mark.order(1) def test_low_line_range_greater_than_high_line_range(self, capsys): """Raise parser error if first value for range > than second.""" argb = [ "/path/to/docformatter", "-c", "--range", "10", "1", "", ] uut = Configurater(argb) with pytest.raises(SystemExit): uut.do_parse_arguments() out, err = capsys.readouterr() assert out == "" assert ( "First value of --range should be less than or equal to the second" in err ) @pytest.mark.integration @pytest.mark.order(1) def test_only_format_in_length_range(self, capsys): """Only format docstrings in length range.""" argb = [ "/path/to/docformatter", "-c", "--docstring-length", "25", "55", "", ] uut = Configurater(argb) uut.do_parse_arguments() assert uut.args.length_range == [25, 55] @pytest.mark.integration @pytest.mark.order(1) def test_low_length_range_is_zero(self, capsys): """Raise parser error if the first value for the length range = 0.""" argb = [ "/path/to/docformatter", "-c", "--docstring-length", "0", "10", "", ] uut = Configurater(argb) with pytest.raises(SystemExit): uut.do_parse_arguments() out, err = capsys.readouterr() assert out == "" assert "--docstring-length must be positive numbers" in err @pytest.mark.integration @pytest.mark.order(1) def test_low_length_range_greater_than_high_length_range(self, capsys): """Raise parser error if first value for range > second value.""" argb = [ "/path/to/docformatter", "-c", "--docstring-length", "55", "25", "", ] uut = Configurater(argb) with pytest.raises(SystemExit): uut.do_parse_arguments() out, err = capsys.readouterr() assert out == "" assert ( "First value of --docstring-length should be less than or equal " "to the second" in err ) @pytest.mark.integration @pytest.mark.order(1) @pytest.mark.parametrize( "config", [ """\ [tool.docformatter] """ ], ) def test_black_defaults( self, temporary_pyproject_toml, config, ): """Black line length defaults to 88 and pre-summary-space to True.""" argb = [ "/path/to/docformatter", "-c", "--black", "--config", "/tmp/pyproject.toml", "", ] uut = Configurater(argb) uut.do_parse_arguments() assert uut.args.black assert uut.args.pre_summary_space assert uut.args.wrap_summaries == WRAP_88 assert uut.args.wrap_descriptions == WRAP_88 @pytest.mark.integration @pytest.mark.order(2) @pytest.mark.parametrize( "config", [ """\ [tool.docformatter] black = true wrap-summaries = 80 """ ], ) def test_black_from_pyproject( self, temporary_pyproject_toml, config, ): """Test black setting via pyproject.toml.""" argb = [ "/path/to/docformatter", "-c", "--config", "/tmp/pyproject.toml", "", ] uut = Configurater(argb) uut.do_parse_arguments() assert uut.args.black assert uut.args.pre_summary_space assert uut.args.wrap_summaries == WRAP_80 assert uut.args.wrap_descriptions == WRAP_88 assert uut.flargs == { "black": "True", "wrap-summaries": "80", } @pytest.mark.integration @pytest.mark.order(2) @pytest.mark.parametrize( "config", [ """\ [docformatter] black = True wrap-descriptions = 80 """ ], ) def test_black_from_setup_cfg( self, temporary_setup_cfg, config, ): """Read black config from setup.cfg.""" argb = [ "/path/to/docformatter", "-c", "--config", "/tmp/setup.cfg", "", ] uut = Configurater(argb) uut.do_parse_arguments() assert uut.args.black assert uut.args.pre_summary_space assert uut.args.wrap_summaries == WRAP_88 assert uut.args.wrap_descriptions == WRAP_80 assert uut.flargs == { "black": "True", "wrap-descriptions": "80", } @pytest.mark.integration @pytest.mark.order(2) @pytest.mark.parametrize( "config", [ """\ [tool.docformatter] check = true diff = true recursive = true exclude = ["src/", "tests/"] """ ], ) def test_exclude_from_pyproject_toml( self, temporary_pyproject_toml, config, ): """Read exclude list from pyproject.toml. See issue #120. """ argb = [ "/path/to/docformatter", "-c", "--config", "/tmp/pyproject.toml", "", ] uut = Configurater(argb) uut.do_parse_arguments() assert uut.args.check assert not uut.args.in_place assert uut.args_lst == argb assert uut.config_file == "/tmp/pyproject.toml" assert uut.flargs == { "recursive": "True", "check": "True", "diff": "True", "exclude": ["src/", "tests/"], } @pytest.mark.integration @pytest.mark.order(2) @pytest.mark.parametrize( "config", [ """\ [docformatter] check = true diff = true recursive = true exclude = ["src/", "tests/"] """ ], ) def test_exclude_from_setup_cfg( self, temporary_setup_cfg, config, ): """Read exclude list from setup.cfg. See issue #120. """ argb = [ "/path/to/docformatter", "-c", "--config", "/tmp/setup.cfg", "", ] uut = Configurater(argb) uut.do_parse_arguments() assert uut.args.check assert not uut.args.in_place assert uut.args_lst == argb assert uut.config_file == "/tmp/setup.cfg" assert uut.flargs == { "recursive": "true", "check": "true", "diff": "true", "exclude": '["src/", "tests/"]', } @pytest.mark.integration @pytest.mark.order(0) def test_non_capitalize_words(self, capsys): """Read list of words not to capitalize. See issue #193. """ argb = [ "/path/to/docformatter", "-n", "qBittorrent", "eBay", "iPad", "-c", "", ] uut = Configurater(argb) uut.do_parse_arguments() assert uut.args.non_cap == ["qBittorrent", "eBay", "iPad"] @pytest.mark.integration @pytest.mark.order(0) @pytest.mark.parametrize( "config", [ """\ [tool.docformatter] check = true diff = true recursive = true non-cap = ["qBittorrent", "iPad", "iOS", "eBay"] """ ], ) def test_non_cap_from_pyproject_toml( self, temporary_pyproject_toml, config, ): """Read list of words not to capitalize from pyproject.toml. See issue #193. """ argb = [ "/path/to/docformatter", "-c", "--config", "/tmp/pyproject.toml", "", ] uut = Configurater(argb) uut.do_parse_arguments() assert uut.args.check assert not uut.args.in_place assert uut.args_lst == argb assert uut.config_file == "/tmp/pyproject.toml" assert uut.flargs == { "recursive": "True", "check": "True", "diff": "True", "non-cap": ["qBittorrent", "iPad", "iOS", "eBay"], } @pytest.mark.integration @pytest.mark.order(0) @pytest.mark.parametrize( "config", [ """\ [docformatter] check = true diff = true recursive = true non-cap = ["qBittorrent", "iPad", "iOS", "eBay"] """ ], ) def test_non_cap_from_setup_cfg( self, temporary_setup_cfg, config, ): """Read list of words not to capitalize from setup.cfg. See issue #193. """ argb = [ "/path/to/docformatter", "-c", "--config", "/tmp/setup.cfg", "", ] uut = Configurater(argb) uut.do_parse_arguments() assert uut.args.check assert not uut.args.in_place assert uut.args_lst == argb assert uut.config_file == "/tmp/setup.cfg" assert uut.flargs == { "recursive": "true", "check": "true", "diff": "true", "non-cap": '["qBittorrent", "iPad", "iOS", "eBay"]', } docformatter-1.7.8/tests/test_docformatter.py000066400000000000000000001053661517155121300215000ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.test_docformatter.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Module for testing docformatter end-to-end.""" # Standard Library Imports import io import os # Third Party Imports import pytest # docformatter Package Imports from docformatter import __main__ as main class TestMain: """Class for testing the _main() function.""" @pytest.mark.system @pytest.mark.parametrize( "contents", [ '''\ def foo(): """ Hello world """ ''' ], ) def test_diff(self, temporary_file, contents): """Should produce diff showing changes.""" output_file = io.StringIO() main._main( argv=["my_fake_program", temporary_file], standard_out=output_file, standard_error=None, standard_in=None, ) assert '''\ @@ -1,4 +1,2 @@ def foo(): - """ - Hello world - """ + """Hello world.""" ''' == "\n".join( output_file.getvalue().split("\n")[2:] ) @pytest.mark.system def test_diff_with_nonexistent_file(self): """Should return error message when file doesn't exist.""" output_file = io.StringIO() main._main( argv=["my_fake_program", "nonexistent_file"], standard_out=output_file, standard_error=output_file, standard_in=None, ) @pytest.mark.system @pytest.mark.parametrize( "contents", [ '''\ def foo(): """ Hello world """ ''' ], ) @pytest.mark.parametrize("diff", [True, False], ids=["show-diff", "no-diff"]) def test_in_place(self, temporary_file, contents, diff): """Should make changes and save back to file.""" output_file = io.StringIO() args = ["my_fake_program", "--in-place", temporary_file] if diff: args.append("--diff") main._main( argv=args, standard_out=output_file, standard_error=None, standard_in=None, ) with open(temporary_file) as f: assert ( '''\ def foo(): """Hello world.""" ''' == f.read() ) if diff: assert "def foo" in output_file.getvalue() else: assert "def foo" not in output_file @pytest.mark.system @pytest.mark.parametrize( "contents", [ '''\ def foo(): """ Hello world """ ''' ], ) @pytest.mark.parametrize("file_directory", ["temporary_directory"]) @pytest.mark.parametrize("directory", ["."]) @pytest.mark.parametrize("prefix", ["."]) def test_ignore_hidden_directories( self, temporary_file, temporary_directory, contents, file_directory, directory, prefix, ): """Ignore 'hidden' directories when recursing.""" output_file = io.StringIO() main._main( argv=["my_fake_program", "--recursive", temporary_directory], standard_out=output_file, standard_error=None, standard_in=None, ) assert "" == output_file.getvalue().strip() @pytest.mark.system def test_io_error_exit_code(self): """Return error code 1 when file does not exist.""" stderr = io.StringIO() ret_code = main._main( argv=["my_fake_program", "this_file_should_not_exist_please"], standard_out=None, standard_error=stderr, standard_in=None, ) assert ret_code == 1 @pytest.mark.system @pytest.mark.parametrize( "contents", ["""Totally fine docstring, do not report anything."""], ) @pytest.mark.parametrize("diff", [True, False], ids=["show-diff", "no-diff"]) def test_check_mode_correct_docstring(self, temporary_file, contents, diff): """""" stdout = io.StringIO() stderr = io.StringIO() args = ["my_fake_program", "--check", temporary_file] if diff: args.append("--diff") ret_code = main._main( argv=args, standard_out=stdout, standard_error=stderr, standard_in=None, ) assert ret_code == 0 # FormatResult.ok assert stdout.getvalue() == "" assert stderr.getvalue() == "" @pytest.mark.system @pytest.mark.parametrize( "contents", [ ''' def my_function(): """ Print my path and return error code """ ''' ], ) @pytest.mark.parametrize("diff", [True, False], ids=["show-diff", "no-diff"]) def test_check_mode_incorrect_docstring(self, temporary_file, contents, diff): """""" stdout = io.StringIO() stderr = io.StringIO() args = ["my_fake_program", "--check", temporary_file] if diff: args.append("--diff") ret_code = main._main( argv=args, standard_out=stdout, standard_error=stderr, standard_in=None, ) assert ret_code == 3 # FormatResult.format_required if not diff: assert stdout.getvalue() == "" else: assert "Print my path" in stdout.getvalue() assert stderr.getvalue().strip() == temporary_file def test_help_output(self): """Ensure help message is printed when passed --help.""" stdout = io.StringIO() stderr = io.StringIO() args = ["--help"] ret_code = main._main( argv=args, standard_out=stdout, standard_error=stderr, standard_in=None, ) assert ret_code == 0 class TestEndToEnd: """Class to test docformatter by executing it from the command line.""" @pytest.mark.system @pytest.mark.parametrize( "contents", [ '''\ def foo(): """ Hello world """ ''' ], ) @pytest.mark.parametrize("arguments", [[]]) def test_end_to_end( self, temporary_file, contents, run_docformatter, arguments, ): """""" assert '''\ @@ -1,4 +1,2 @@ def foo(): - """ - Hello world - """ + """Hello world.""" ''' == "\n".join( run_docformatter.communicate()[0].decode().replace("\r", "").split("\n")[2:] ) @pytest.mark.system @pytest.mark.parametrize( "contents", [ '''\ def foo(): """ Hello world this is a summary that will get wrapped """ ''' ], ) @pytest.mark.parametrize("arguments", [["--wrap-summaries=40"]]) def test_end_to_end_with_wrapping( self, run_docformatter, temporary_file, contents, arguments, ): """Wrap summary at --wrap-summaries number of columns.""" assert '''\ @@ -1,4 +1,3 @@ def foo(): - """ - Hello world this is a summary that will get wrapped - """ + """Hello world this is a summary + that will get wrapped.""" ''' == "\n".join( run_docformatter.communicate()[0].decode().replace("\r", "").split("\n")[2:] ) @pytest.mark.system @pytest.mark.parametrize( "contents", [ '''\ def foo(): """Hello world is a long sentence that will not be wrapped because I turned wrapping off. Hello world is a long sentence that will not be wrapped because I turned wrapping off. """ ''' ], ) @pytest.mark.parametrize( "arguments", [["--wrap-summaries=0", "--wrap-description=0"]] ) def test_end_to_end_with_no_wrapping_long_sentences( self, run_docformatter, temporary_file, contents, arguments, ): """Long sentences remain long with wrapping turned off.""" assert "" == "\n".join( run_docformatter.communicate()[0].decode().replace("\r", "").split("\n")[2:] ) @pytest.mark.system @pytest.mark.parametrize( "contents", [ '''\ def foo(): """Hello world is a long sentence that will not be wrapped because I turned wrapping off. Hello world is a long sentence that will not be wrapped because I turned wrapping off. """ ''' ], ) @pytest.mark.parametrize( "arguments", [["--wrap-summaries=0", "--wrap-description=0"]] ) def test_end_to_end_with_no_wrapping_short_sentences( self, run_docformatter, temporary_file, arguments, contents, ): """Short sentences remain short with wrapping turned off.""" assert "" == "\n".join( run_docformatter.communicate()[0].decode().replace("\r", "").split("\n")[2:] ) @pytest.mark.system @pytest.mark.parametrize( "contents", [ '''\ def foo(): """Wrapping is off, but it will still add the trailing period """ ''' ], ) @pytest.mark.parametrize("arguments", [["--wrap-summaries=0"]]) def test_end_to_end_no_wrapping_period( self, run_docformatter, temporary_file, arguments, contents, ): """Add period to end of summary even with wrapping off.""" assert '''\ @@ -1,3 +1,3 @@ def foo(): """Wrapping is off, but it will still add - the trailing period """ + the trailing period.""" ''' == "\n".join( run_docformatter.communicate()[0].decode().replace("\r", "").split("\n")[2:] ) @pytest.mark.system @pytest.mark.parametrize( "contents", [ '''\ def foo(): """Description from issue #145 that was being improperly wrapped. .. _linspace API: https://numpy.org/doc/stable/reference/generated/numpy.linspace.html .. _arange API: https://numpy.org/doc/stable/reference/generated/numpy.arange.html .. _logspace API: https://numpy.org/doc/stable/reference/generated/numpy.logspace.html """ ''' ], ) @pytest.mark.parametrize( "arguments", [ [ "--wrap-summaries=72", "--wrap-descriptions=78", ] ], ) def test_end_to_end_keep_rest_link_one_line( self, run_docformatter, temporary_file, arguments, contents, ): """Keep reST in-line URL link on one line. See issue #145. See requirement docformatter_10.1.3.1. """ assert "" == "\n".join( run_docformatter.communicate()[0].decode().replace("\r", "").split("\n")[2:] ) @pytest.mark.system @pytest.mark.parametrize( "contents", [ '''\ def foo(): """Description from issue #150 that was being improperly wrapped. The text file can be retrieved via the Chrome plugin `Get Cookies.txt ` while browsing.""" ''' ], ) @pytest.mark.parametrize( "arguments", [ [ "--wrap-summaries=72", "--wrap-descriptions=78", ] ], ) def test_ignore_already_wrapped_link( self, run_docformatter, temporary_file, arguments, contents, ): """Ignore a URL link that was wrapped by the user. See issue #150. """ assert '''\ @@ -1,6 +1,7 @@ def foo(): """Description from issue #150 that was being improperly wrapped. - The text file can be retrieved via the Chrome plugin `Get - Cookies.txt ` while browsing.""" + The text file can be retrieved via the Chrome plugin + `Get Cookies.txt ` while browsing. + """ ''' == "\n".join( run_docformatter.communicate()[0].decode().replace("\r", "").split("\n")[2:] ) @pytest.mark.system @pytest.mark.parametrize( "contents", [ '''\ def create_wix_wrapper(tools, wix_home, bin_install): """Create a wrapper around a WiX install. :param tools: ToolCache of available tools. :param wix_home: The path of the WiX installation. :param bin_install: Is the install a binaries-only install? A full MSI install of WiX has a `/bin` folder in the paths; a binaries-only install does not. :returns: A valid WiX SDK wrapper. If WiX is not available, and was not installed, raises MissingToolError. """\ ''' ], ) @pytest.mark.parametrize( "arguments", [["--black"]], ) def test_end_to_end_no_excessive_whitespace( self, run_docformatter, temporary_file, arguments, contents, ): """Remove all excess whitespace in the middle of wrappped lines. See issue #222. """ assert '''\ @@ -1,11 +1,10 @@ def create_wix_wrapper(tools, wix_home, bin_install): """Create a wrapper around a WiX install. - :param tools: ToolCache of available tools. - :param wix_home: The path of the WiX installation. - :param bin_install: Is the install a binaries-only install? A full - MSI install of WiX has a `/bin` folder in the paths; a - binaries-only install does not. - :returns: A valid WiX SDK wrapper. If WiX is not available, and was - not installed, raises MissingToolError. + :param tools: ToolCache of available tools. + :param wix_home: The path of the WiX installation. + :param bin_install: Is the install a binaries-only install? A full MSI install of + WiX has a `/bin` folder in the paths; a binaries-only install does not. + :returns: A valid WiX SDK wrapper. If WiX is not available, and was not installed, + raises MissingToolError. """ ''' == "\n".join( run_docformatter.communicate()[0].decode().replace("\r", "").split("\n")[2:] ) @pytest.mark.system @pytest.mark.parametrize( "contents", [ '''\ def foo(): """Hello world is a long sentence that will be wrapped at 40 characters because I'm using that option - My list item - My list item """ ''' ], ) @pytest.mark.parametrize( "arguments", [ [ "--wrap-summaries=40", "--pre-summary-newline", "--blank", ] ], ) def test_end_to_end_all_options( self, run_docformatter, temporary_file, arguments, contents, ): """""" assert '''\ @@ -1,7 +1,10 @@ def foo(): - """Hello world is a long sentence that will be wrapped at 40 characters because I\'m using that option + """ + Hello world is a long sentence that + will be wrapped at 40 characters + because I\'m using that option. + - My list item - My list item - """ ''' == "\n".join( run_docformatter.communicate()[0].decode().replace("\r", "").split("\n")[2:] ) @pytest.mark.system @pytest.mark.parametrize("contents", [""]) @pytest.mark.parametrize( "arguments", [ ["--range", "0", "1", os.devnull], ["--range", "3", "1", os.devnull], ], ) def test_invalid_range( self, run_docformatter, arguments, contents, ): """""" if arguments[1] == "0": assert "must be positive" in run_docformatter.communicate()[1].decode() if arguments[1] == "3": assert "should be less than" in run_docformatter.communicate()[1].decode() @pytest.mark.system @pytest.mark.parametrize("arguments", [[]]) @pytest.mark.parametrize("contents", [""]) def test_no_arguments( self, run_docformatter, arguments, contents, ): """""" assert "" == run_docformatter.communicate()[1].decode() @pytest.mark.system @pytest.mark.parametrize("arguments", [["-"]]) @pytest.mark.parametrize("contents", [""]) def test_standard_in( self, run_docformatter, arguments, contents, ): result = ( run_docformatter.communicate( '''\ def foo(): """ Hello world""" '''.encode() )[0] .decode() .replace("\r", "") ) assert 0 == run_docformatter.returncode assert ( '''\ def foo(): """Hello world.""" ''' == result ) @pytest.mark.system @pytest.mark.parametrize( "arguments", [ ["foo.py", "-"], ["--in-place", "-"], ["--recursive", "-"], ], ) @pytest.mark.parametrize("contents", [""]) def test_standard_in_with_invalid_options( self, run_docformatter, arguments, contents, ): """""" if arguments[0] == "foo.py": assert "cannot mix" in run_docformatter.communicate()[1].decode() if arguments[0] == "--in-place": assert "cannot be used" in run_docformatter.communicate()[1].decode() if arguments[0] == "--recursive": assert "cannot be used" in run_docformatter.communicate()[1].decode() class TestEndToEndPyproject: """Class to test docformatter using pyproject.toml for options.""" @pytest.mark.system @pytest.mark.parametrize( "contents", [ '''\ class TestFoo(): """ Docstring that should not have a pre-summary space.""" ''' ], ) @pytest.mark.parametrize( "config", [ """\ [tool.docformatter] pre-summary-space = false """ ], ) @pytest.mark.parametrize( "arguments", [ [ "--config", "/tmp/pyproject.toml", ] ], ) def test_no_pre_summary_space_using_pyproject( self, run_docformatter, temporary_pyproject_toml, temporary_file, arguments, contents, config, ): """No pre-summary space using configuration from pyproject.toml. See issue #119. """ assert '''\ @@ -1,2 +1,2 @@ class TestFoo(): - """ Docstring that should not have a pre-summary space.""" + """Docstring that should not have a pre-summary space.""" ''' == "\n".join( run_docformatter.communicate()[0].decode().replace("\r", "").split("\n")[2:] ) @pytest.mark.system @pytest.mark.parametrize( "contents", [ '''\ class TestFoo(): """Docstring that should have a pre-summary space.""" ''' ], ) @pytest.mark.parametrize( "config", [ """\ [tool.docformatter] pre-summary-space = true """ ], ) @pytest.mark.parametrize( "arguments", [ [ "--config", "/tmp/pyproject.toml", ] ], ) def test_pre_summary_space_using_pyproject( self, run_docformatter, temporary_pyproject_toml, temporary_file, arguments, contents, config, ): """Pre-summary space using configuration from pyproject.toml. See issue #119. """ assert '''\ @@ -1,2 +1,2 @@ class TestFoo(): - """Docstring that should have a pre-summary space.""" + """ Docstring that should have a pre-summary space.""" ''' == "\n".join( run_docformatter.communicate()[0].decode().replace("\r", "").split("\n")[2:] ) @pytest.mark.system @pytest.mark.parametrize( "contents", [ '''\ class TestFoo(): """Docstring that should not have a pre-summary newline. This is a multi-line docstring that should not have a newline placed before the summary.""" ''' ], ) @pytest.mark.parametrize( "config", [ """\ [tool.docformatter] pre-summary-newline = false """ ], ) @pytest.mark.parametrize( "arguments", [ [ "--config", "/tmp/pyproject.toml", ] ], ) def test_no_pre_summary_newline_using_pyproject( self, run_docformatter, temporary_pyproject_toml, temporary_file, arguments, contents, config, ): """No pre-summary newline using configuration from pyproject.toml. See issue #119. """ assert '''\ @@ -1,5 +1,6 @@ class TestFoo(): """Docstring that should not have a pre-summary newline. - This is a multi-line docstring that should not have a - newline placed before the summary.""" + This is a multi-line docstring that should not have a newline placed + before the summary. + """ ''' == "\n".join( run_docformatter.communicate()[0].decode().replace("\r", "").split("\n")[2:] ) @pytest.mark.system @pytest.mark.parametrize( "contents", [ '''\ class TestFoo(): """Docstring that should have a pre-summary newline. This is a multi-line docstring that should have a newline placed before the summary.""" ''' ], ) @pytest.mark.parametrize( "config", [ """\ [tool.docformatter] pre-summary-newline = true """ ], ) @pytest.mark.parametrize( "arguments", [ [ "--config", "/tmp/pyproject.toml", ] ], ) def test_pre_summary_newline_using_pyproject( self, run_docformatter, temporary_pyproject_toml, temporary_file, arguments, contents, config, ): """Pre-summary newline using configuration from pyproject.toml. See issue #119. """ assert '''\ @@ -1,5 +1,7 @@ class TestFoo(): - """Docstring that should have a pre-summary newline. + """ + Docstring that should have a pre-summary newline. - This is a multi-line docstring that should have a newline - placed before the summary.""" + This is a multi-line docstring that should have a newline placed + before the summary. + """ ''' == "\n".join( run_docformatter.communicate()[0].decode().replace("\r", "").split("\n")[2:] ) @pytest.mark.system @pytest.mark.parametrize( "contents", [ '''\ class TestFoo(): """Really long summary docstring that should not be split into a multiline summary.""" ''' ], ) @pytest.mark.parametrize( "config", [ """\ [tool.docformatter] pre-summary-multi-line = false """ ], ) @pytest.mark.parametrize( "arguments", [ [ "--config", "/tmp/pyproject.toml", ] ], ) def test_no_pre_summary_multiline_using_pyproject( self, run_docformatter, temporary_pyproject_toml, temporary_file, arguments, contents, config, ): """No pre-summary multi-line using configuration from pyproject.toml. See issue #119. """ assert '''\ @@ -1,3 +1,3 @@ class TestFoo(): - """Really long summary docstring that should not be - split into a multiline summary.""" + """Really long summary docstring that should not be split into a + multiline summary.""" ''' == "\n".join( run_docformatter.communicate()[0].decode().replace("\r", "").split("\n")[2:] ) @pytest.mark.system @pytest.mark.parametrize( "contents", [ '''\ class TestFoo(): """Really long summary docstring that should be split into a multiline summary.""" ''' ], ) @pytest.mark.parametrize( "config", [ """\ [tool.docformatter] pre-summary-multi-line = true """ ], ) @pytest.mark.parametrize( "arguments", [ [ "--config", "/tmp/pyproject.toml", ] ], ) def test_pre_summary_multiline_using_pyproject( self, run_docformatter, temporary_pyproject_toml, temporary_file, arguments, contents, config, ): """Pre-summary multi-line using configuration from pyproject.toml. See issue #119. """ assert '''\ @@ -1,3 +1,3 @@ class TestFoo(): - """Really long summary docstring that should be - split into a multiline summary.""" + """Really long summary docstring that should be split into a multiline + summary.""" ''' == "\n".join( run_docformatter.communicate()[0].decode().replace("\r", "").split("\n")[2:] ) @pytest.mark.system @pytest.mark.parametrize( "contents", [ '''\ class TestFoo(): """Summary docstring that is followed by a description. This is the description and it shouldn't have a blank line inserted after it. """ ''' ], ) @pytest.mark.parametrize( "config", [ """\ [tool.docformatter] blank = false """ ], ) @pytest.mark.parametrize( "arguments", [ [ "--config", "/tmp/pyproject.toml", ] ], ) def test_no_blank_using_pyproject( self, run_docformatter, temporary_pyproject_toml, temporary_file, arguments, contents, config, ): """No blank after description using configuration from pyproject.toml. See issue #119. """ assert '''\ @@ -1,6 +1,6 @@ class TestFoo(): """Summary docstring that is followed by a description. - This is the description and it shouldn\'t have a blank line - inserted after it. + This is the description and it shouldn\'t have a blank line inserted + after it. """ ''' == "\n".join( run_docformatter.communicate()[0].decode().replace("\r", "").split("\n")[2:] ) @pytest.mark.system @pytest.mark.parametrize( "contents", [ '''\ class TestFoo(): """Summary docstring that is followed by a description. This is the description and it should have a blank line inserted after it. """ ''' ], ) @pytest.mark.parametrize( "config", [ """\ [tool.docformatter] blank = true """ ], ) @pytest.mark.parametrize( "arguments", [ [ "--config", "/tmp/pyproject.toml", ] ], ) def test_blank_using_pyproject( self, run_docformatter, temporary_pyproject_toml, temporary_file, arguments, contents, config, ): """Blank after description using configuration from pyproject.toml. See issue #119. """ assert '''\ @@ -1,6 +1,7 @@ class TestFoo(): """Summary docstring that is followed by a description. - This is the description and it should have a blank line - inserted after it. + This is the description and it should have a blank line inserted + after it. + """ ''' == "\n".join( run_docformatter.communicate()[0].decode().replace("\r", "").split("\n")[2:] ) @pytest.mark.system @pytest.mark.parametrize( "contents", [ '''\ class foo(): """Hello world is a long sentence that will be wrapped at 12 characters because I\'m using that option in pyproject.toml.""" ''' ], ) @pytest.mark.parametrize( "config", [ """\ [tool.docformatter] wrap-summaries = 12 """ ], ) @pytest.mark.parametrize( "arguments", [ [ "--config", "/tmp/pyproject.toml", ] ], ) def test_format_wrap_using_pyproject( self, run_docformatter, temporary_pyproject_toml, temporary_file, arguments, contents, config, ): """Wrap docstring using configuration from pyproject.toml. See issue #119. """ assert '''\ @@ -1,3 +1,18 @@ class foo(): - """Hello world is a long sentence that will be wrapped at 12 - characters because I\'m using that option in pyproject.toml.""" + """Hello + world is + a long + sentence + that + will be + wrapped + at 12 ch + aracters + because + I\'m + using + that + option + in pypro + ject.tom + l.""" ''' == "\n".join( run_docformatter.communicate()[0].decode().replace("\r", "").split("\n")[2:] ) class TestEndToEndSetupcfg: """Class to test docformatter using setup.cfg for options.""" @pytest.mark.system @pytest.mark.parametrize( "contents", [ '''\ class TestFoo(): """ Docstring that should not have a pre-summary space.""" ''' ], ) @pytest.mark.parametrize( "config", [ """\ [docformatter] pre-summary-space = false """ ], ) @pytest.mark.parametrize( "arguments", [ [ "--config", "/tmp/setup.cfg", ] ], ) def test_no_pre_summary_space_using_setup_cfg( self, run_docformatter, temporary_setup_cfg, temporary_file, arguments, contents, config, ): """No pre-summary space using configuration from setup.cfg. See issue #119. """ assert '''\ @@ -1,2 +1,2 @@ class TestFoo(): - """ Docstring that should not have a pre-summary space.""" + """Docstring that should not have a pre-summary space.""" ''' == "\n".join( run_docformatter.communicate()[0].decode().replace("\r", "").split("\n")[2:] ) @pytest.mark.system @pytest.mark.parametrize( "contents", [ '''\ class TestFoo(): """ Docstring that should not have a pre-summary space.""" ''' ], ) @pytest.mark.parametrize( "config", [ """\ [docformatter] in-place = true check = false diff = false """ ], ) @pytest.mark.parametrize( "arguments", [ [ "--config", "/tmp/setup.cfg", ] ], ) def test_in_place_using_setup_cfg( self, run_docformatter, temporary_setup_cfg, temporary_file, arguments, contents, config, ): """Make changes in-place if set in setup.cfg. See issue #122. """ assert "" == "\n".join( run_docformatter.communicate()[0].decode().replace("\r", "").split("\n")[2:] ) with open(temporary_file, "r") as f: assert ( f.read() == '''\ class TestFoo(): """Docstring that should not have a pre-summary space.""" ''' ) @pytest.mark.system @pytest.mark.parametrize( "contents", [ '''\ class TestFoo(): """ Docstring that should not have a pre-summary space.""" ''' ], ) @pytest.mark.parametrize( "config", [ """\ [docformatter] in-place = true check = true diff = false """ ], ) @pytest.mark.parametrize( "arguments", [ [ "--config", "/tmp/setup.cfg", ] ], ) def test_check_using_setup_cfg( self, run_docformatter, temporary_setup_cfg, temporary_file, arguments, contents, config, ): """Just check for changes if set in setup.cfg. See issue #122. """ _results = run_docformatter.communicate() assert "" == "\n".join(_results[0].decode().replace("\r", "").split("\n")[2:]) assert temporary_file == _results[1].decode().rstrip("\n") @pytest.mark.system @pytest.mark.parametrize( "contents", [ '''\ class TestFoo(): """ Docstring that should not have a pre-summary space.""" ''' ], ) @pytest.mark.parametrize( "config", [ """\ [docformatter] check = true diff = true """ ], ) @pytest.mark.parametrize( "arguments", [ [ "--config", "/tmp/setup.cfg", ] ], ) def test_check_with_diff_using_setup_cfg( self, run_docformatter, temporary_setup_cfg, temporary_file, arguments, contents, config, ): """Check for changes and print diff if set in setup.cfg. See issue #122. """ assert '''\ @@ -1,2 +1,2 @@ class TestFoo(): - """ Docstring that should not have a pre-summary space.""" + """Docstring that should not have a pre-summary space.""" ''' == "\n".join( run_docformatter.communicate()[0].decode().replace("\r", "").split("\n")[2:] ) docformatter-1.7.8/tests/test_encoding_functions.py000066400000000000000000000121731517155121300226560ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.test.encoding_functions.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Module for testing functions used to determine file encodings.""" # Standard Library Imports import contextlib import io import sys with contextlib.suppress(ImportError): if sys.version_info >= (3, 11): # Standard Library Imports import tomllib else: # Third Party Imports import tomli as tomllib # Third Party Imports import pytest # docformatter Package Imports from docformatter import Encoder with open("tests/_data/string_files/encoding_functions.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) @pytest.mark.unit @pytest.mark.parametrize( "test_key", [ "find_newline_only_cr", "find_newline_only_lf", "find_newline_only_crlf", "find_newline_cr1_and_lf2", "find_newline_cr1_and_crlf2", "find_newline_should_default_to_lf_empty", "find_newline_should_default_to_lf_blank", "find_dominant_newline", ], ) def test_do_find_newline(test_key): uut = Encoder() source = TEST_STRINGS[test_key]["instring"] expected = TEST_STRINGS[test_key]["expected"] result = uut.do_find_newline(source) assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" @pytest.mark.usefixtures("temporary_file") class TestDoOpenWithEncoding: """Class for testing the do_open_with_encoding function.""" @pytest.mark.unit @pytest.mark.parametrize("contents", ["# -*- coding: utf-8 -*-\n"]) def test_do_open_with_utf_8_encoding(self, temporary_file, contents): """Return TextIOWrapper object when opening file with encoding.""" uut = Encoder() assert isinstance( uut.do_open_with_encoding(temporary_file), io.TextIOWrapper, ) @pytest.mark.unit @pytest.mark.parametrize("contents", ["# -*- coding: utf-8 -*-\n"]) def test_do_open_with_wrong_encoding(self, temporary_file, contents): """Raise LookupError when passed unknown encoding.""" uut = Encoder() uut.encoding = "cr1252" with pytest.raises(LookupError): uut.do_open_with_encoding(temporary_file) @pytest.mark.usefixtures("temporary_file") class TestDoDetectEncoding: """Class for testing the detect_encoding() function.""" @pytest.mark.integration @pytest.mark.parametrize("contents", ["# -*- coding: utf-8 -*-\n"]) def test_do_detect_encoding_with_explicit_utf_8(self, temporary_file, contents): """Return utf-8 when explicitly set in the file.""" uut = Encoder() uut.do_detect_encoding(temporary_file) assert "utf_8" == uut.encoding @pytest.mark.integration @pytest.mark.parametrize("contents", ["# Wow! docformatter is super-cool.\n"]) def test_do_detect_encoding_with_non_explicit_setting( self, temporary_file, contents ): """Return default system encoding when encoding not explicitly set.""" uut = Encoder() uut.do_detect_encoding(temporary_file) assert "ascii" == uut.encoding @pytest.mark.integration @pytest.mark.parametrize("contents", ["# -*- coding: blah -*-"]) def test_do_detect_encoding_with_bad_encoding(self, temporary_file, contents): """Default to latin-1 when unknown encoding detected.""" uut = Encoder() uut.do_detect_encoding(temporary_file) assert "ascii" == uut.encoding @pytest.mark.integration @pytest.mark.parametrize("contents", [""]) def test_do_detect_encoding_with_undetectable_encoding(self, temporary_file): """Default to latin-1 when encoding detection fails.""" uut = Encoder() # Simulate a file with undetectable encoding with open(temporary_file, "wb") as file: # Binary content unlikely to have a detectable encoding file.write(b"\xff\xfe\xfd\xfc\x00\x00\x00\x00") uut.do_detect_encoding(temporary_file) assert uut.encoding == uut.DEFAULT_ENCODING docformatter-1.7.8/tests/test_string_functions.py000066400000000000000000000260671517155121300224050ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.test_string_functions.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Module for testing functions that manipulate text.""" # Standard Library Imports import contextlib import sys with contextlib.suppress(ImportError): if sys.version_info >= (3, 11): # Standard Library Imports import tomllib else: # Third Party Imports import tomli as tomllib # Third Party Imports import pytest # docformatter Package Imports from docformatter.strings import ( description_to_list, do_clean_excess_whitespace, do_find_shortest_indentation, do_normalize_line, do_normalize_line_endings, do_normalize_summary, do_reindent, do_split_description, do_split_first_sentence, do_split_summary, do_split_summary_and_description, do_strip_docstring, do_strip_leading_blank_lines, ) with open("tests/_data/string_files/string_functions.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) @pytest.mark.unit @pytest.mark.parametrize( "test_key, indentation", [ ("do_reindent", " "), ("do_reindent_should_expand_tabs_to_indentation", " "), ("do_reindent_with_no_indentation_expand_tabs", ""), ("do_reindent_should_maintain_indentation", " "), ("do_reindent_tab_indentation", "\t"), ], ) def test_do_reindent(test_key, indentation): """Test the do_reindent function.""" source = TEST_STRINGS[test_key]["instring"] expected = TEST_STRINGS[test_key]["expected"] result = do_reindent(source, indentation) assert result == expected, ( f"\nFailed {test_key}:\nExpected {expected}" f"\nGot {result}" ) @pytest.mark.unit def test_do_find_shortest_indentation(): """Test the do_find_shorted_indentation function.""" assert " " == do_find_shortest_indentation( [" ", " b", " a"], ) @pytest.mark.unit @pytest.mark.parametrize( "test_key", [ "do_normalize_summary", "do_normalize_summary_multiline", "do_normalize_summary_question_mark", "do_normalize_summary_exclamation_point", "do_normalize_summary_with_title", "do_normalize_summary_capitalize_first_letter", "do_normalize_summary_with_proprer_noun", "do_normalize_summary_capitalize_first_letter_with_period", "do_normalize_summary_dont_capitalize_first_letter_if_variable", ], ) def test_do_normalize_summary(test_key): """Test the do_normalize_summary function.""" source = TEST_STRINGS[test_key]["instring"] expected = TEST_STRINGS[test_key]["expected"] result = do_normalize_summary(source) assert result == expected, ( f"\nFailed {test_key}:\nExpected {expected}" f"\nGot {result}" ) @pytest.mark.unit @pytest.mark.parametrize("test_key", ["do_normalize_line"]) def test_do_normalize_line(test_key): """Test the do_normalize_line function.""" source = TEST_STRINGS[test_key]["instring"] expected = TEST_STRINGS[test_key]["expected"] result = do_normalize_line(source, "\n") assert result == expected, ( f"\nFailed {test_key}:\nExpected {expected}" f"\nGot {result}" ) @pytest.mark.unit @pytest.mark.parametrize( "test_key, indentation, wrap_length", [("description_to_list", " ", 72)] ) def test_description_to_list(test_key, indentation, wrap_length): """Test the description_to_list function.""" source = TEST_STRINGS[test_key]["instring"] expected = TEST_STRINGS[test_key]["expected"] result = description_to_list(source, indentation, wrap_length) assert result == expected, ( f"\nFailed {test_key}:\nExpected {expected}" f"\nGot {result}" ) @pytest.mark.unit @pytest.mark.parametrize( "test_key", [ "do_split_first_sentence", "do_split_first_sentence_2", "do_split_first_sentence_3", ], ) def test_do_split_first_sentence(test_key): """Test the do_split_first_sentence function.""" source = TEST_STRINGS[test_key]["instring"] expected = TEST_STRINGS[test_key]["expected"] # We convert the tuple return to a list since we can't store a tuple in a TOML file. result = list(do_split_first_sentence(source)) assert result == expected, ( f"\nFailed {test_key}:\nExpected {expected}" f"\nGot {result}" ) @pytest.mark.unit @pytest.mark.parametrize( "test_key", [ "do_split_summary", "do_split_summary_2", "do_split_multi_sentence_summary", "do_split_multi_sentence_summary_2", ], ) def test_do_split_summary(test_key): """Test the do_split_summary function.""" source = TEST_STRINGS[test_key]["instring"] expected = TEST_STRINGS[test_key]["expected"] # We convert the tuple returned to a list since we can't store a tuple in a TOML # file. result = do_split_summary(source) assert result == expected, ( f"\nFailed {test_key}:\nExpected {expected}" f"\nGot {result}" ) @pytest.mark.unit @pytest.mark.parametrize( "test_key", [ "do_strip_docstring", "do_strip_docstring_triple_single_quotes", "do_strip_docstring_empty_string", "do_strip_docstring_raw_string", "do_strip_docstring_raw_string_2", "do_strip_docstring_unicode_string", "do_strip_docstring_unicode_string_2", "do_strip_docstring_with_unknown", "do_strip_docstring_with_single_quotes", "do_strip_docstring_with_double_quotes", ], ) def test_do_strip_docstring(test_key): """Test the do_strip_docstring function.""" source = TEST_STRINGS[test_key]["instring"] expected = TEST_STRINGS[test_key]["expected"] raises = TEST_STRINGS[test_key].get("raises") if raises: with pytest.raises(eval(raises)): do_strip_docstring(source) else: # We convert the tuple returned to a list since we can't store a tuple in a TOML # file. result = list(do_strip_docstring(source)) assert result == expected, ( f"\nFailed {test_key}:\nExpected {expected}" f"\nGot {result}" ) @pytest.mark.unit @pytest.mark.parametrize( "test_key", [ "do_strip_leading_blank_lines", ], ) def test_do_strip_leading_blank_lines(test_key): """Test the do_strup_leading_blank_lines function.""" source = TEST_STRINGS[test_key]["instring"] expected = TEST_STRINGS[test_key]["expected"] result = do_strip_leading_blank_lines(source) assert result == expected, ( f"\nFailed {test_key}:\nExpected {expected}" f"\nGot {result}" ) @pytest.mark.unit @pytest.mark.parametrize( "test_key, indentation", [ ("do_clean_excess_whitespace", " "), ], ) def test_do_clean_excess_whitespace(test_key, indentation): """Test the do_clean_excess_whitespace function.""" source = TEST_STRINGS[test_key]["instring"] expected = TEST_STRINGS[test_key]["expected"] result = do_clean_excess_whitespace(source, indentation) assert result == expected, ( f"\nFailed {test_key}:\nExpected {expected}" f"\nGot {result}" ) @pytest.mark.integration @pytest.mark.order(5) @pytest.mark.parametrize("test_key", ["do_normalize_line_endings"]) def test_do_normalize_line_endings(test_key): """Test the do_normalize_line_endings function.""" source = TEST_STRINGS[test_key]["instring"] expected = TEST_STRINGS[test_key]["expected"] result = do_normalize_line_endings(source, "\n") assert result == expected, ( f"\nFailed {test_key}:\nExpected {expected}" f"\nGot {result}" ) @pytest.mark.integration @pytest.mark.order(1) @pytest.mark.parametrize( "test_key, indentation, wrap_length, style", [ ("do_split_description_url_outside_param", " ", 72, "sphinx"), ("do_split_description_single_url_in_param", " ", 72, "sphinx"), ("do_split_description_single_url_in_multiple_params", " ", 72, "sphinx"), ("do_split_description_multiple_urls_in_param", " ", 72, "sphinx"), ], ) def test_do_split_description(test_key, indentation, wrap_length, style): """Test the do_split_description function.""" source = TEST_STRINGS[test_key]["instring"] expected = TEST_STRINGS[test_key]["expected"] result = do_split_description(source, indentation, wrap_length, style) assert result == expected, ( f"\nFailed {test_key}:\nExpected {expected}" f"\nGot {result}" ) @pytest.mark.integration @pytest.mark.order(3) @pytest.mark.parametrize( "test_key", [ "do_split_summary_and_description", "do_split_summary_and_description_complex", "do_split_summary_and_description_more_complex", "do_split_summary_and_description_with_list", "do_split_summary_and_description_with_list_of_parameters", "do_split_summary_and_description_with_capital", "do_split_summary_and_description_with_list_on_other_line", "do_split_summary_and_description_with_other_symbol", "do_split_summary_and_description_with_colon", "do_split_summary_and_description_with_exclamation", "do_split_summary_and_description_with_question_mark", "do_split_summary_and_description_with_double_quote", "do_split_summary_and_description_with_single_quote", "do_split_summary_and_description_with_double_backtick", "do_split_summary_and_description_with_punctuation", "do_split_summary_and_description_without_punctuation", "do_split_summary_and_description_with_abbreviation", "do_split_summary_and_description_with_url", ], ) def test_do_split_summary_and_description(test_key): """Test the do_split_summary_and_description function.""" source = TEST_STRINGS[test_key]["instring"] expected = TEST_STRINGS[test_key]["expected"] # We convert the tuple returned to a list since we can't store a tuple in a TOML # file. result = list(do_split_summary_and_description(source)) assert result == expected, ( f"\nFailed {test_key}:\nExpected {expected}" f"\nGot {result}" ) docformatter-1.7.8/tests/test_utility_functions.py000066400000000000000000000103651517155121300225740ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.test_utility_functions.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Module for testing utility functions used when processing docstrings.""" # Standard Library Imports import contextlib import sys with contextlib.suppress(ImportError): if sys.version_info >= (3, 11): # Standard Library Imports import tomllib else: # Third Party Imports import tomli as tomllib # Third Party Imports import pytest # docformatter Package Imports from docformatter.util import find_py_files, has_correct_length, is_in_range with open("tests/_data/string_files/utility_functions.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) @pytest.mark.unit @pytest.mark.parametrize( "test_key", [ "has_correct_length_none", "has_correct_length_start_in_range", "has_correct_length_end_in_range", "has_correct_length_both_in_range", "has_correct_length_start_out_of_range", "has_correct_length_end_out_of_range", "has_correct_length_both_out_of_range", ], ) def test_has_correct_length(test_key): """Test has_correct_length() function.""" length_range = TEST_STRINGS[test_key]["length_range"] start = TEST_STRINGS[test_key]["start"] end = TEST_STRINGS[test_key]["end"] expected = TEST_STRINGS[test_key]["expected"] if length_range == "None": length_range = None result = has_correct_length(length_range, start, end) assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" @pytest.mark.unit @pytest.mark.parametrize( "test_key", [ "is_in_range_none", "is_in_range_start_in_range", "is_in_range_end_in_range", "is_in_range_both_in_range", "is_in_range_out_of_range", ], ) def test_is_in_range(test_key): """Test is_in_range() function.""" line_range = TEST_STRINGS[test_key]["line_range"] start = TEST_STRINGS[test_key]["start"] end = TEST_STRINGS[test_key]["end"] expected = TEST_STRINGS[test_key]["expected"] if line_range == "None": line_range = None result = is_in_range(line_range, start, end) assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" @pytest.mark.unit @pytest.mark.parametrize( "test_key, recursive", [ ("find_py_file", False), ("find_py_file_recursive", True), ("skip_hidden_py_file", False), ("skip_hidden_py_file_recursive", True), ("ignore_non_py_file", False), ("ignore_non_py_file_recursive", True), ("exclude_py_file", False), ("exclude_py_file_recursive", True), ("exclude_multiple_files", False), ("exclude_multiple_files_recursive", True), ], ) def test_find_py_files(test_key, recursive): """Test find_py_files() function.""" sources = TEST_STRINGS[test_key]["sources"] exclude = TEST_STRINGS[test_key]["exclude"] expected = TEST_STRINGS[test_key]["expected"] result = list(find_py_files(sources, recursive, exclude)) assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" docformatter-1.7.8/tests/wrappers/000077500000000000000000000000001517155121300172265ustar00rootroot00000000000000docformatter-1.7.8/tests/wrappers/test_description_wrapper.py000066400000000000000000000057651517155121300247370ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.wrappers.test_description_wrapper.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Module for testing the description wrapper functions.""" # Standard Library Imports import contextlib import sys with contextlib.suppress(ImportError): if sys.version_info >= (3, 11): # Standard Library Imports import tomllib else: # Third Party Imports import tomli as tomllib # Third Party Imports import pytest # docformatter Package Imports from docformatter.wrappers import do_close_description, do_wrap_description with open("tests/_data/string_files/description_wrappers.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) @pytest.mark.unit @pytest.mark.parametrize( "test_key, text_index", [ ("do_close_description", 24), ], ) def test_do_close_description(test_key, text_index): source = TEST_STRINGS[test_key]["instring"] expected = TEST_STRINGS[test_key]["expected"] result = do_close_description(source, text_index, " ") assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" @pytest.mark.integration @pytest.mark.order(2) @pytest.mark.parametrize( "test_key, force_wrap", [ ("do_wrap_description", False), ("do_wrap_description_with_doctest", False), ("do_wrap_description_with_list", False), ("do_wrap_description_with_heuristic_list", False), ("do_wrap_description_with_heuristic_list_force_wrap", True), ("do_wrap_description_with_directive", False), ], ) def test_do_wrap_description(test_key, force_wrap): source = TEST_STRINGS[test_key]["instring"] expected = TEST_STRINGS[test_key]["expected"] result = do_wrap_description(source, " ", 72, force_wrap, False, "", "sphinx") assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" docformatter-1.7.8/tests/wrappers/test_field_wrapper.py000066400000000000000000000074621517155121300234730ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.wrappers.test_field_wrapper.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Module for testing the description wrapper functions.""" # Standard Library Imports import contextlib import sys with contextlib.suppress(ImportError): if sys.version_info >= (3, 11): # Standard Library Imports import tomllib else: # Third Party Imports import tomli as tomllib # Third Party Imports import pytest # docformatter Package Imports from docformatter.wrappers import fields with open("tests/_data/string_files/field_wrappers.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) @pytest.mark.unit @pytest.mark.parametrize( "test_key, field_idx, idx", [ ( "do_join_field_body", [(146, 161), (185, 208), (319, 342), (372, 395), (425, 433), (598, 605)], 0, ), ( "do_join_field_body_2", [(146, 161), (185, 208), (319, 342), (372, 395), (425, 433), (598, 605)], 1, ), ( "do_join_field_body_3", [(146, 161), (185, 208), (319, 342), (372, 395), (425, 433), (598, 605)], 2, ), ], ) def test_do_join_field_body(test_key, field_idx, idx): source = TEST_STRINGS[test_key]["instring"] expected = TEST_STRINGS[test_key]["expected"] result = fields._do_join_field_body(source, field_idx, idx) assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" @pytest.mark.unit @pytest.mark.parametrize( "test_key", [ "do_wrap_field", "do_wrap_long_field", ], ) def test_do_wrap_field(test_key): source = TEST_STRINGS[test_key]["instring"] expected = TEST_STRINGS[test_key]["expected"] result = fields._do_wrap_field(source[0], source[1], " ", 72) assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" @pytest.mark.integration @pytest.mark.order(0) @pytest.mark.parametrize( "test_key, field_idx, text_idx", [ ( "do_wrap_field_list", [(146, 162), (186, 209), (320, 343), (373, 396), (426, 434), (599, 606)], 140, ), ], ) def test_do_wrap_field_lists(test_key, field_idx, text_idx): source = TEST_STRINGS[test_key]["instring"] lines = TEST_STRINGS[test_key]["lines"] expected = TEST_STRINGS[test_key]["expected"] # We convert the returned tuple to a list because we can't store tuple in a TOML # file. result = list( fields.do_wrap_field_lists(source, field_idx, lines, text_idx, " ", 72) ) assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" docformatter-1.7.8/tests/wrappers/test_summary_wrapper.py000066400000000000000000000066321517155121300241030ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.wrappers.test_summary_wrapper.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Module for testing functions that wrap summary text.""" # Standard Library Imports import contextlib import sys with contextlib.suppress(ImportError): if sys.version_info >= (3, 11): # Standard Library Imports import tomllib else: # Third Party Imports import tomli as tomllib # Third Party Imports import pytest # docformatter Package Imports from docformatter.wrappers import do_unwrap_summary, do_wrap_summary with open("tests/_data/string_files/summary_wrappers.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) @pytest.mark.unit @pytest.mark.parametrize( "test_key", [ "do_unwrap_summary", "do_unwrap_summary_empty", "do_unwrap_summary_only_newlines", "do_unwrap_summary_double_newlines", "do_unwrap_summary_leading_trailing", ], ) def test_do_unwrap_summary(test_key): source = TEST_STRINGS[test_key]["instring"] expected = TEST_STRINGS[test_key]["expected"] result = do_unwrap_summary(source) assert ( result == expected ), f"Failed {test_key}:\nExpected:\n{expected!r}\nGot:\n{result!r}" @pytest.mark.integration @pytest.mark.order(2) @pytest.mark.parametrize( "test_key, initial_indent, subsequent_indent, wrap_length", [ ("do_wrap_summary_no_wrap", " ", " ", 88), ("do_wrap_summary_disabled", " ", " ", 0), ("do_wrap_summary_with_wrap", " ", " ", 50), ("do_wrap_summary_empty", "", "", 50), ("do_wrap_summary_long_word", "", "", 50), ("do_wrap_summary_exact_length", "", "", 50), ("do_wrap_summary_tabs_spaces", " ", " ", 40), ("do_wrap_summary_wrap_length_1", ">", "-", 1), ], ) def test_do_wrap_summary( test_key, initial_indent, subsequent_indent, wrap_length, ): source = TEST_STRINGS[test_key]["instring"] expected = TEST_STRINGS[test_key]["expected"] result = do_wrap_summary( source, initial_indent=initial_indent, subsequent_indent=subsequent_indent, wrap_length=wrap_length, ) assert ( result == expected ), f"Failed {test_key}:\nExpected:\n{expected!r}\nGot:\n{result!r}" docformatter-1.7.8/tests/wrappers/test_url_wrapper.py000066400000000000000000000056701517155121300232110ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.wrappers.test_url_wrapper.py is part of the docformatter project # # Copyright (C) 2012-2023 Steven Myint # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Module for testing functions that wrap URL text.""" # Standard Library Imports import contextlib import sys with contextlib.suppress(ImportError): if sys.version_info >= (3, 11): # Standard Library Imports import tomllib else: # Third Party Imports import tomli as tomllib # Third Party Imports import pytest # docformatter Package Imports from docformatter.wrappers import do_wrap_urls with open("tests/_data/string_files/url_wrappers.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) @pytest.mark.unit @pytest.mark.parametrize( "test_key, url_idx, text_idx", [ ("elaborate_inline_url", [(134, 226)], 0), ("short_inline_url", [(8, 42)], 0), ("long_inline_url", [(44, 168)], 0), ("simple_url", [(4, 100)], 0), ("short_url", [(8, 32)], 0), ("inline_url_retain_space", [(47, 171)], 0), ("keep_inline_url_together", [(20, 133)], 0), ("inline_url_two_paragraphs", [(26, 153)], 0), ("url_no_delete_words", [(36, 92)], 0), ("no_newline_after_url", [(113, 167), (229, 280)], 0), ("only_url_in_description", [(4, 99)], 0), ("no_indent_string_on_newline", [(43, 91)], 0), ("short_anonymous_url", [(137, 190)], 0), ("quoted_url", [(59, 80)], 0), ], ) def test_do_wrap_urls(test_key, url_idx, text_idx): source = TEST_STRINGS[test_key]["instring"] expected = TEST_STRINGS[test_key]["expected"] # We convert the returned tuple to a list because we can't store a tuple in a # TOML file. result = list(do_wrap_urls(source, url_idx, text_idx, " ", 72)) assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" docformatter-1.7.8/tox.ini000066400000000000000000000040261517155121300155360ustar00rootroot00000000000000[tox] env_list = py310 py311 py312 py313 py314 pypy3 coverage pre-commit isolated_build = true skip_missing_interpreters = true skipsdist = true [gh-actions] python = 3.10: py310 3.11: py311 3.12: py312 3.13: py313 3.14: py314 pypy-3.11: pypy3 [testenv] description = Run the test suite using pytest under {basepython} deps = charset_normalizer coverage[toml] mock pytest pytest-cov pytest-order tomli setenv = COVERAGE_FILE = {toxworkdir}/.coverage.{envname} commands = pip install -U pip pip install --prefix={toxworkdir}/{envname} -e .[tomli] pytest -s -x -c {toxinidir}/pyproject.toml \ -m unit \ --cache-clear \ --cov=docformatter \ --cov-config={toxinidir}/pyproject.toml \ --cov-branch \ {toxinidir}/tests/ pytest -s -x -c {toxinidir}/pyproject.toml \ -m integration \ --cache-clear \ --cov=docformatter \ --cov-config={toxinidir}/pyproject.toml \ --cov-branch \ {toxinidir}/tests/ pytest -s -x -c {toxinidir}/pyproject.toml \ -m system \ --cache-clear \ --cov=docformatter \ --cov-config={toxinidir}/pyproject.toml \ --cov-branch \ --cov-append \ {toxinidir}/tests/ [testenv:coverage] description = combine coverage data and create report setenv = COVERAGE_FILE = {toxworkdir}/.coverage skip_install = true deps = coverage[toml] parallel_show_output = true commands = coverage combine coverage report -m coverage xml -o {toxworkdir}/coverage.xml depends = py310, py311, py312, py313, py314, pypy3 [testenv:pre-commit] description = Run autoformatters and quality assurance tools under {basepython}. deps = pre-commit commands = {envpython} -m pre_commit run \ --color=always \ --show-diff-on-failure \ {posargs:--all-files} [testenv:docs] description = build docformatter documentation allowlist_externals = make changedir = docs commands = make html