pax_global_header00006660000000000000000000000064144534020570014516gustar00rootroot0000000000000052 comment=dfefe062799848234b4cd60b04aa633c0608025e docformatter-1.7.5/000077500000000000000000000000001445340205700142215ustar00rootroot00000000000000docformatter-1.7.5/.github/000077500000000000000000000000001445340205700155615ustar00rootroot00000000000000docformatter-1.7.5/.github/release-drafter.yml000066400000000000000000000014771445340205700213620ustar00rootroot00000000000000name-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.5/.github/workflows/000077500000000000000000000000001445340205700176165ustar00rootroot00000000000000docformatter-1.7.5/.github/workflows/ci.yml000066400000000000000000000020161445340205700207330ustar00rootroot00000000000000name: Execute Test Suite on: push: branches: - "*" pull_request: branches: - master jobs: test: strategy: fail-fast: false matrix: python-version: - "pypy-3.8-v7.3.10" - "3.11" - "3.10" - "3.9" - "3.8" - "3.7" 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.11" - 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.5/.github/workflows/do-lint.yml000066400000000000000000000010041445340205700217020ustar00rootroot00000000000000name: 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.11" - 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 style docformatter-1.7.5/.github/workflows/do-prioritize-issues.yml000066400000000000000000000036171445340205700244610ustar00rootroot00000000000000# 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.5/.github/workflows/do-release.yml000066400000000000000000000131741445340205700223670ustar00rootroot00000000000000# 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.5/.github/workflows/do-update-authors.yml000066400000000000000000000047121445340205700237120ustar00rootroot00000000000000name: 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: truedocformatter-1.7.5/.github/workflows/on-issue-open.yml000066400000000000000000000005061445340205700230430ustar00rootroot00000000000000# 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.5/.github/workflows/on-push-tag.yml000066400000000000000000000022451445340205700225060ustar00rootroot00000000000000# 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.5/.gitignore000066400000000000000000000002551445340205700162130ustar00rootroot00000000000000*~ *.egg *.egg-info/ *.eggs/ *.pyc .*.swp .travis-solo/ MANIFEST README.html __pycache__/ build/ dist/ htmlcov/ *coverage* .python-version poetry.lock .idea/ .vscode/ .tox/ docformatter-1.7.5/.pre-commit-config.yaml000066400000000000000000000033051445340205700205030ustar00rootroot00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.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/psf/black rev: '23.3.0' hooks: - id: black types_or: [python, pyi] language_version: python3 - repo: https://github.com/PyCQA/isort rev: 5.12.0 hooks: - id: isort args: [--settings-file, ./pyproject.toml] - repo: https://github.com/PyCQA/docformatter rev: v1.7.1 hooks: - id: docformatter additional_dependencies: [tomli] args: [--in-place, --config, ./pyproject.toml] - repo: https://github.com/charliermarsh/ruff-pre-commit rev: 'v0.0.269' hooks: - id: ruff args: [ --select, "PL", --select, "F" ] - 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.3.0 hooks: - id: mypy additional_dependencies: [types-python-dateutil] args: [--config-file, ./pyproject.toml] - repo: https://github.com/myint/eradicate rev: '2.2.0' hooks: - id: eradicate args: [] - repo: https://github.com/rstcheck/rstcheck rev: 'v6.1.2' hooks: - id: rstcheck additional_dependencies: [tomli] args: [-r, --config, ./pyproject.toml] docformatter-1.7.5/.pre-commit-hooks.yaml000066400000000000000000000006171445340205700203640ustar00rootroot00000000000000- id: docformatter name: docformatter description: 'Formats docstrings to follow PEP 257.' entry: docformatter args: [-i] language: python types: [python] - id: docformatter-venv name: docformatter-venv description: 'Formats docstrings to follow PEP 257. Uses python3 -m venv.' entry: docformatter args: [-i] language: python_venv types: [python] docformatter-1.7.5/.sourcery.yaml000066400000000000000000000027261445340205700170450ustar00rootroot00000000000000# 🪄 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.5/AUTHORS.rst000066400000000000000000000027661445340205700161130ustar00rootroot00000000000000.. 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) -------------------------------------------- - Alec Merdler - Alexander Biggs - Alexander Kapshuna - Andrew Howe - Andy Hayden - Anthony Sottile - Antoine Dechaume - Asher Foa - Benjamin Schubert - Doyle Rowland - Eric Hutton - Filip Kucharczyk - 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 - Sho Iwamoto - Swen Kooij - happlebao - icp - serhiy-yevtushenko docformatter-1.7.5/CHANGELOG.md000066400000000000000000000164571445340205700160470ustar00rootroot00000000000000# Changelog ## [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) Bug Fixes - fix: IndexError when only URL in long description [\#190](https://github.com/PyCQA/docformatter/pull/190) ([weibullguy](https://github.com/weibullguy)) - fix: removing newline after shebang [\#188](https://github.com/PyCQA/docformatter/pull/188) ([weibullguy](https://github.com/weibullguy)) - fix: not capitalizing first word when summary ends in period [\#185](https://github.com/PyCQA/docformatter/pull/185) ([weibullguy](https://github.com/weibullguy)) ## [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) Bug Fixes - fix: adding newlines around wrapped URL [\#182](https://github.com/PyCQA/docformatter/pull/182) ([weibullguy](https://github.com/weibullguy)) - fix: adding blank line in summary with symbol [\#179](https://github.com/PyCQA/docformatter/pull/179) ([weibullguy](https://github.com/weibullguy)) **Merged pull requests:** - ci: split test suite run into unit and system [\#181](https://github.com/PyCQA/docformatter/pull/181) ([weibullguy](https://github.com/weibullguy)) ## [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) Bug Fixes - fix: remove blank after comment [\#177](https://github.com/PyCQA/docformatter/pull/177) ([weibullguy](https://github.com/weibullguy)) ## [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) Bug Fixes - fix: remove blank lines after line beginning with 'def' [\#171](https://github.com/PyCQA/docformatter/pull/171) ([weibullguy](https://github.com/weibullguy)) ## [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) Features - \(🎁\) Support python 3.11 [\#164](https://github.com/PyCQA/docformatter/pull/164) ([KotlinIsland](https://github.com/KotlinIsland)) Bug Fixes - fix: update URL handling functions [\#152](https://github.com/PyCQA/docformatter/pull/152) ([weibullguy](https://github.com/weibullguy)) **Merged pull requests:** - docs: clarify future arguments [\#168](https://github.com/PyCQA/docformatter/pull/168) ([weibullguy](https://github.com/weibullguy)) - chore: update GitHub action workflows [\#153](https://github.com/PyCQA/docformatter/pull/153) ([weibullguy](https://github.com/weibullguy)) ## [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.5/LICENSE000066400000000000000000000020451445340205700152270ustar00rootroot00000000000000Copyright (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.5/README.rst000066400000000000000000000153201445340205700157110ustar00rootroot00000000000000============ 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 launch_rocket(): """Launch the rocket. Go colonize space.""" 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): launch_rocket() gets formatted into this .. code-block:: python """Here are some examples. This module docstring should be dedented. """ def launch_rocket(): """Launch the rocket. Go colonize space. """ 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): launch_rocket() 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 Issues ====== Bugs and patches can be reported on the `GitHub page`_. .. _`GitHub page`: https://github.com/PyCQA/docformatter/issues docformatter-1.7.5/docs/000077500000000000000000000000001445340205700151515ustar00rootroot00000000000000docformatter-1.7.5/docs/Makefile000066400000000000000000000011761445340205700166160ustar00rootroot00000000000000# 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.5/docs/images/000077500000000000000000000000001445340205700164165ustar00rootroot00000000000000docformatter-1.7.5/docs/images/pycharm-file-watcher-configurations.png000066400000000000000000006540111445340205700261760ustar00rootroot00000000000000PNG  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.5/docs/make.bat000066400000000000000000000014011445340205700165520ustar00rootroot00000000000000@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.5/docs/source/000077500000000000000000000000001445340205700164515ustar00rootroot00000000000000docformatter-1.7.5/docs/source/authors.rst000066400000000000000000000000571445340205700206720ustar00rootroot00000000000000Authors ======= .. include:: ../../AUTHORS.rstdocformatter-1.7.5/docs/source/conf.py000066400000000000000000000017661445340205700177620ustar00rootroot00000000000000# 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.5" # -- 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.5/docs/source/configuration.rst000066400000000000000000000106541445340205700220600ustar00rootroot00000000000000How 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.5/docs/source/faq.rst000066400000000000000000000013551445340205700177560ustar00rootroot00000000000000 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.5/docs/source/index.rst000066400000000000000000000010651445340205700203140ustar00rootroot00000000000000.. 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.5/docs/source/installation.rst000066400000000000000000000016611445340205700217100ustar00rootroot00000000000000How 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.5/docs/source/license.rst000066400000000000000000000000621445340205700206230ustar00rootroot00000000000000License ======= .. literalinclude:: ../../LICENSEdocformatter-1.7.5/docs/source/requirements.rst000066400000000000000000000533361445340205700217400ustar00rootroot00000000000000========================= 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.5/docs/source/usage.rst000066400000000000000000000152141445340205700203120ustar00rootroot00000000000000How 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.6.1 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 docformatter-1.7.5/pyproject.toml000066400000000000000000000143201445340205700171350ustar00rootroot00000000000000[tool.poetry] name = "docformatter" version = "1.7.5" 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.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: Implementation', 'Programming Language :: Python :: Implementation :: PyPy', 'Programming Language :: Python :: Implementation :: CPython', 'License :: OSI Approved :: MIT License', ] packages = [{include = "docformatter", from = "src"}] include = ["LICENSE"] [tool.poetry.dependencies] python = "^3.7" charset_normalizer = "^3.0.0" tomli = {version = "^2.0.0", python = "<3.11", optional = true} untokenize = "^0.1.1" [tool.poetry.dev-dependencies] autopep8 = "^2.0.0" black = "^22.0.0" coverage = {extras = ["toml"], version = "^6.4.0"} isort = "^5.10.0" mock = "^4.0.0" mypy = "0.991" pycodestyle = "^2.8.0" pydocstyle = "^6.1.1" pylint = [ {version = "^2.12.0", python = "<3.7.2"}, {version = "^2.14.0", python = ">=3.7.2"}, ] pytest = "^7.1.0" pytest-cov = "^4.0.0" ruff = "^0.0.267" rstcheck = "^6.1.0" tox = "<4.0.0" Sphinx = [ {version = "5.3.0", python = "<3.8"}, {version = "^6.0.0", python = ">=3.8"}, ] twine = "^4.0.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.", "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 = ['py37', 'py38', 'py39', 'py310', 'py311'] include = '\.pyi?$' 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", "tabularcolumns", "toctree", ] ignore_roles = [ "numref", ] [tool.tox] legacy_tox_ini = """ [tox] envlist = py37 py38 py39 py310 py311 pypy3 coverage style isolated_build = true skip_missing_interpreters = true skipsdist = true [gh-actions] python = 3.7: py37 3.8: py38 3.9: py39 3.10: py310 3.11: py311 pypy-3.8: pypy3 [testenv] description = run the test suite using pytest under {basepython} deps = charset_normalizer coverage[toml] mock pytest pytest-cov tomli untokenize 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 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 = py37, py38, py39, py310, py311, pypy3 [testenv:style] description = run autoformatters and style checkers deps = charset_normalizer pycodestyle pydocstyle ruff rstcheck toml untokenize commands = pip install -U pip pip install . docformatter --black --recursive {toxinidir}/src/docformatter pycodestyle --exclude=.git,.tox,*.pyc,*.pyo,build,dist,*.egg-info,config,docs,locale,tests,tools --ignore=C326,C330,E121,E123,E126,E133,E203,E242,E265,E402,W503,W504 --format=pylint --max-line-length=88 {toxinidir}/src/docformatter pydocstyle {toxinidir}/src/docformatter ruff check --select "PL" --select "F" {toxinidir}/src/docformatter rstcheck --report-level=1 {toxinidir}/README.rst """ docformatter-1.7.5/src/000077500000000000000000000000001445340205700150105ustar00rootroot00000000000000docformatter-1.7.5/src/docformatter/000077500000000000000000000000001445340205700175015ustar00rootroot00000000000000docformatter-1.7.5/src/docformatter/__init__.py000066400000000000000000000031161445340205700216130ustar00rootroot00000000000000#!/usr/bin/env python # # 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. """This is the docformatter package.""" __all__ = ["__version__"] # docformatter Local Imports from .__pkginfo__ import __version__ from .strings import * # noqa F403 from .syntax import * # noqa F403 from .util 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 from .format import Formatter, FormatResult # isort: skip # noqa F401 docformatter-1.7.5/src/docformatter/__main__.py000077500000000000000000000144211445340205700216000ustar00rootroot00000000000000#!/usr/bin/env python # # 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. """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: _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 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.5/src/docformatter/__pkginfo__.py000066400000000000000000000022471445340205700223110ustar00rootroot00000000000000#!/usr/bin/env python # # 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. """Package information for docformatter.""" __version__ = "1.7.5" docformatter-1.7.5/src/docformatter/configuration.py000066400000000000000000000324301445340205700227240ustar00rootroot00000000000000#!/usr/bin/env python # # 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. """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, List, Union TOMLLIB_INSTALLED = False TOMLI_INSTALLED = False with contextlib.suppress(ImportError): if sys.version_info >= (3, 11): # Standard Library Imports import tomllib TOMLLIB_INSTALLED = True else: # Third Party Imports import tomli TOMLI_INSTALLED = True # 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_dct: 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: List[Union[bool, int, str]]) -> None: """Initialize a Configurater class instance. Parameters ---------- args : list Any command line arguments passed during invocation. """ self.args_lst = args self.config_file = "" self.parser = argparse.ArgumentParser( description=__doc__, prog="docformatter", ) try: self.config_file = 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=self.flargs_dct.get("in-place", "false").lower() == "true", help="make changes to files instead of printing diffs", ) changes.add_argument( "-c", "--check", action="store_true", default=self.flargs_dct.get("check", "false").lower() == "true", help="only check and report incorrectly formatted files", ) self.parser.add_argument( "-d", "--diff", action="store_true", default=self.flargs_dct.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=self.flargs_dct.get("recursive", "false").lower() == "true", help="drill down directories recursively", ) self.parser.add_argument( "-e", "--exclude", nargs="*", default=self.flargs_dct.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_dct.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=self.flargs_dct.get("black", "false").lower() == "true", help="make formatting compatible with standard black options " "(default: False)", ) 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_dct.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_dct.get( "rest_section_adorns", r"[!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~]{4,}" ), help="regex for identifying reST section header adornments", ) self.parser.add_argument( "--wrap-summaries", default=int(self.flargs_dct.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_dct.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=self.flargs_dct.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_dct.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=self.flargs_dct.get("blank", "false").lower() == "true", help="add blank line after description (default: False)", ) self.parser.add_argument( "--pre-summary-newline", action="store_true", default=self.flargs_dct.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=self.flargs_dct.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=self.flargs_dct.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=self.flargs_dct.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_dct.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_dct.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=self.flargs_dct.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", ) 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 (TOMLI_INSTALLED or TOMLLIB_INSTALLED) 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: if TOMLI_INSTALLED: config = tomli.load(f) elif TOMLLIB_INSTALLED: config = tomllib.load(f) result = config.get("tool", {}).get("docformatter", None) if result is not None: self.flargs_dct = { k: v if isinstance(v, list) else str(v) for k, v in result.items() } 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_dct = { k: v if isinstance(v, list) else str(v) for k, v in config[_section].items() } docformatter-1.7.5/src/docformatter/encode.py000066400000000000000000000072051445340205700213140ustar00rootroot00000000000000#!/usr/bin/env python # # 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. """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" def __init__(self): """Initialize an Encoder instance.""" self.encoding = "latin-1" 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: self.encoding = from_path(filename).best().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 = "latin-1" 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.5/src/docformatter/format.py000066400000000000000000000510161445340205700213460ustar00rootroot00000000000000#!/usr/bin/env python # # 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. """This module provides docformatter's Formattor class.""" # Standard Library Imports import argparse import collections import contextlib import io import re import tokenize from typing import TextIO, Tuple # Third Party Imports import untokenize # type: ignore # docformatter Package Imports import docformatter.encode as _encode import docformatter.strings as _strings import docformatter.syntax as _syntax import docformatter.util as _util unicode = str def _do_remove_blank_lines_after_definitions( modified_tokens, ): """Remove blank lines between definitions and docstrings. Blank lines between class, method, function, and variable definitions and the docstring will be removed. Parameters ---------- modified_tokens: list The list of tokens created from the docstring. Returns ------- modified_tokens: list The list of tokens with any blank lines following a variable definition removed. """ for _idx, _token in enumerate(modified_tokens): if _token[0] == 3: # noqa PLR2004 j = 1 # Remove newline between variable definition and docstring # unless it's separating a docstring from: # * A previous docstring. # * The file's shebang. # * The import section. while ( modified_tokens[_idx - j][4] == "\n" and not (modified_tokens[_idx - j - 1][4].strip().endswith('"""')) and not modified_tokens[_idx - j - 1][4].startswith("#!/") and "import" not in modified_tokens[_idx - j - 1][4] ): modified_tokens.pop(_idx - j) j += 1 # Remove newline between class, method, and function # definitions and docstring. j = 2 while modified_tokens[_idx - j][4] == "\n" and modified_tokens[ _idx - j - 2 ][4].strip().startswith(("def", "class")): modified_tokens.pop(_idx - j) j += 1 return modified_tokens def _do_remove_blank_lines_after_docstring(modified_tokens): """Remove blank lines between docstring and first Python statement. Parameters ---------- modified_tokens: list The list of tokens created from the docstring. Returns ------- modified_tokens: list The list of tokens with any blank lines following a docstring removed. """ # Remove all newlines between docstring and first Python # statement as long as it's not a stub function. for _idx, _token in enumerate(modified_tokens): j = 1 _num_blank_lines = 0 while modified_tokens[_idx - j][4] == "\n": j += 1 _num_blank_lines += 1 with contextlib.suppress(IndexError): _is_definition = _token[4].lstrip().startswith(("class ", "def ", "@")) _is_docstring = modified_tokens[_idx - 2][4].strip().endswith('"""') _after_definition = ( modified_tokens[_idx - _num_blank_lines - 4][4] .lstrip() .startswith(("class", "def", "@")) ) _after_docstring = modified_tokens[_idx - 5][4].strip().endswith( '"""' ) or modified_tokens[_idx - 5][4].strip().startswith('"""') _comment_follows = re.search(r"\"\"\" *#", modified_tokens[_idx - 4][4]) if ( _token[0] == 1 and not _is_definition and not _is_docstring and not _comment_follows and _after_definition and _after_docstring ): for j in range(_num_blank_lines): modified_tokens.pop(_idx - j - 1) return modified_tokens class FormatResult: """Possible exit codes.""" ok = 0 error = 1 interrupted = 2 check_failed = 3 class Formatter: """Format docstrings.""" 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 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() def do_format_standard_in(self, parser: argparse.ArgumentParser): """Print formatted text 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): """Format multiple files. Return ------ code: int One of the FormatResult codes. """ outcomes = collections.Counter() for filename in _util.find_py_files( set(self.args.files), self.args.recursive, self.args.exclude ): try: result = self._do_format_file(filename) outcomes[result] += 1 except OSError as exception: outcomes[FormatResult.error] += 1 print(unicode(exception), file=self.stderror) return_codes = [ # in order of preference FormatResult.error, FormatResult.check_failed, FormatResult.ok, ] for code in return_codes: if outcomes[code]: return code def _do_format_file(self, filename): """Run format_code() on a file. Parameters ---------- filename: str The path to the file to be formatted. Return ------ result_code: 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.check_failed if self.args.check: 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: # Standard Library Imports import difflib 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): """Return source code with docstrings formatted. Parameters ---------- source: str The text from the source file. """ try: _original_newline = self.encodor.do_find_newline(source.splitlines(True)) _code = self._format_code(source) return _strings.normalize_line_endings( _code.splitlines(True), _original_newline ) except (tokenize.TokenError, IndentationError): return source def _format_code( self, source, ): """Return source code with docstrings formatted. Parameters ---------- source: str The source code string. Returns ------- formatted_source: str The source code with formatted docstrings. """ 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 modified_tokens = [] sio = io.StringIO(source) previous_token_type = None only_comments_so_far = True try: for ( token_type, token_string, start, end, line, ) in tokenize.generate_tokens(sio.readline): _token_string = token_string if ( token_type == tokenize.STRING and token_string.startswith(self.QUOTE_TYPES) and ( previous_token_type == tokenize.INDENT or previous_token_type == tokenize.NEWLINE or only_comments_so_far ) and _util.is_in_range(self.args.line_range, start[0], end[0]) and _util.has_correct_length( self.args.length_range, start[0], end[0] ) ): indentation = " " * (len(line) - len(line.lstrip())) _token_string = self._do_format_docstring( indentation, token_string, ) if token_type not in [ tokenize.COMMENT, tokenize.NEWLINE, tokenize.NL, ]: only_comments_so_far = False previous_token_type = token_type modified_tokens.append((token_type, _token_string, start, end, line)) modified_tokens = _do_remove_blank_lines_after_definitions(modified_tokens) modified_tokens = _do_remove_blank_lines_after_docstring(modified_tokens) return untokenize.untokenize(modified_tokens) except tokenize.TokenError: 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 ------- docstring_formatted: str The docstring formatted according the various options. """ contents, open_quote = self._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(self.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 = _syntax.do_find_links(contents) with contextlib.suppress(IndexError): if _links[0][0] == 0 and _links[0][1] == len(contents): return docstring summary, description = _strings.split_summary_and_description(contents) # Leave docstrings with only field lists alone. if _syntax.is_some_sort_of_field_list( summary, self.args.style, ): return docstring # Leave docstrings with underlined descriptions alone. # TODO: Deprecate the remove_section_header method now that section headers # are being handled. if _syntax.remove_section_header(description).strip() != description.strip(): return docstring if not self.args.force_wrap and ( _syntax.is_some_sort_of_list( summary, self.args.non_strict, self.args.rest_section_adorns, self.args.style, ) or _syntax.do_find_links(summary) ): # Something is probably not 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 ------- formatted_docstring : str The formatted docstring. """ if self.args.make_summary_multi_line: beginning = f"{open_quote}\n{indentation}" ending = f'\n{indentation}"""' summary_wrapped = _syntax.wrap_summary( _strings.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 = _syntax.wrap_summary( open_quote + _strings.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]}" f"\n{indentation}" f"{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 ------- formatted_docstring : 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 = _syntax.wrap_summary( _strings.normalize_summary(summary, self.args.non_cap), wrap_length=self.args.wrap_summaries, initial_indent=initial_indent, subsequent_indent=indentation, ).lstrip() description = _syntax.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_strip_docstring(self, 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. Determines 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 self.QUOTE_TYPES: if quote in self.RAW_QUOTE_TYPES + self.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" ) docformatter-1.7.5/src/docformatter/strings.py000066400000000000000000000155261445340205700215550ustar00rootroot00000000000000#!/usr/bin/env python # # 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. """This module provides docformatter string functions.""" # Standard Library Imports import contextlib import re from typing import List, Match, Optional, Union def find_shortest_indentation(lines: List[str]) -> 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 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 ------- is_beginning : bool True if this token is the beginning of a sentence. """ # 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 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 normalize_line_endings(lines, newline): """Return fixed line endings. All lines will be modified to use the most common line ending. """ return "".join([normalize_line(line, newline) for line in lines]) def 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 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(("e.g.", "i.e.", "Dr.", "Mr.", "Mrs.", "Ms.")): # 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 afterwards. break previous_delimiter = delimiter delimiter = "" return sentence, delimiter + rest def split_summary_and_description(contents): """Split docstring into summary and description. Return tuple (summary, description). """ split_lines = contents.rstrip().splitlines() 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 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 = split_first_sentence(contents) if split[0].strip() and split[1].strip(): return ( split[0].strip(), find_shortest_indentation(split[1].splitlines()[1:]) + split[1].strip(), ) return contents, "" docformatter-1.7.5/src/docformatter/syntax.py000066400000000000000000000674511445340205700214160ustar00rootroot00000000000000#!/usr/bin/env python # # 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. """This module provides docformatter's syntax functions.""" # Standard Library Imports import contextlib import re import textwrap from typing import Iterable, List, Tuple, Union DEFAULT_INDENT = 4 ALEMBIC_REGEX = r"^ *[a-zA-Z0-9_\- ]*: " """Regular expression to use for finding alembic headers.""" BULLET_REGEX = r"\s*[*\-+] [\S ]+" """Regular expression to use for finding bullet lists.""" 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.""" OPTION_REGEX = r"^-{1,2}[\S ]+ {2}\S+" """Regular expression to use for finding option lists.""" REST_REGEX = r"((\.{2}|`{2}) ?[\w.~-]+(:{2}|`{2})?[\w ]*?|`[\w.~]+`)" """Regular expression to use for finding reST directives.""" SPHINX_REGEX = r":(param|raises|return|rtype|type|yield)[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. """ HEURISTIC_MIN_LIST_ASPECT_RATIO = 0.4 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_url(url: str, indentation: str) -> str: r"""Strip newlines and multiple whitespace from URL string. This function deals with situations such as: `Get\n Cookies.txt bool: """Determine if docstring contains any reST directives. .. todo:: Currently this function only returns True/False to indicate whether a reST directive was found. Should return a list of tuples containing the start and end position of each reST directive found similar to the function do_find_links(). Parameters ---------- text : str The docstring text to test. Returns ------- is_directive : bool Whether the docstring is a reST directive. """ _rest_iter = re.finditer(REST_REGEX, text) return bool([(_rest.start(0), _rest.end(0)) for _rest in _rest_iter]) def do_find_field_lists( text: str, style: str, ): 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 ------- _field_idx, _wrap_parameters : tuple A list of tuples with each tuple containing the starting and ending position of each field list found in the passed description. A boolean indicating 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 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 ------- url_index : list A list of tuples with each tuple containing the starting and ending position of each URL found in the passed 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 something other than a complete link. 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. Arguments --------- 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 simply treat it as a standard text word. """ _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 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 = do_find_links(text) # Check if the description contains any field lists. _field_idx, _wrap_fields = do_find_field_lists( text, style, ) # Field list wrapping takes precedence over URL wrapping. _url_idx = _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 = do_wrap_urls( text, _url_idx, 0, indentation, wrap_length, ) if _field_idx: _lines, _text_idx = 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 += _do_close_description(text, _text_idx, indentation) return _lines 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( description_to_list( text[text_idx : field_idx[0][0]], indentation, wrap_length, ) ) for _idx, __ in enumerate(field_idx): _field_name = text[field_idx[_idx][0] : field_idx[_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[_idx][1] return lines, text_idx 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 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( 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"{do_clean_url(_text, indentation)}") text_idx = _url[1] return _lines, text_idx def is_some_sort_of_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( ( # "@param x:" <-- Epytext style # "@type x:" <-- Epytext style re.match(EPYTEXT_REGEX, line) ) for line in split_lines ) elif style == "sphinx": return any( ( # ":parameter: description" <-- Sphinx style re.match(SPHINX_REGEX, line) ) for line in split_lines ) return False # pylint: disable=line-too-long def is_some_sort_of_list( text: str, strict: bool, rest_sections: str, style: str, ) -> bool: """Determine if docstring is a reST list. Notes ----- There are five types of lists in reST/docutils that need to be handled. * `Bullet lists `_ * `Enumerated lists `_ * `Definition lists `_ * `Field lists `_ * `Option lists `_ """ split_lines = text.rstrip().splitlines() # TODO: Find a better way of doing this. # 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 if is_some_sort_of_field_list(text, style): return False return any( ( # "* parameter" <-- Bullet list # "- parameter" <-- Bullet list # "+ parameter" <-- Bullet list re.match(BULLET_REGEX, line) or # "1. item" <-- Enumerated list re.match(ENUM_REGEX, line) or # "====\ndescription\n====" <-- reST section # "----\ndescription\n----" <-- reST section # "description\n----" <-- reST section re.match(rest_sections, line) or # "-a description" <-- Option list # "--long description" <-- Option list re.match(OPTION_REGEX, line) or # "@param x:" <-- Epytext style # "@type x:" <-- Epytext style re.match(EPYTEXT_REGEX, line) or # ":parameter: description" <-- Sphinx style re.match(SPHINX_REGEX, line) or # "parameter : description" <-- Numpy style re.match(NUMPY_REGEX, line) or # "word\n----" <-- Numpy headings re.match(r"^\s*-+", line) or # "Args:" <-- Google style # "parameter:" <-- Google style re.match(GOOGLE_REGEX, line) or # "parameter - description" re.match(r"[\S ]+ - \S+", line) or # "parameter -- description" re.match(r"\s*\S+\s+--\s+", line) or # Literal block re.match(LITERAL_REGEX, line) or # "@parameter" re.match(r"^ *@[a-zA-Z0-9_\- ]*(?:(?!:).)*$", line) or # " c :math:`[0, `]`. re.match(r" *\w *:[a-zA-Z0-9_\- ]*:", line) or # "Revision ID: >" # "Revises: " # "Create Date: 2023-01-06 10:13:28.156709" re.match(ALEMBIC_REGEX, line) ) for line in split_lines ) def is_some_sort_of_code(text: str) -> bool: """Return True if text looks like code.""" return any( len(word) > 50 and not re.match(URL_REGEX, word) # noqa: PLR2004 for word in text.split() ) def 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 remove_section_header(text): r"""Return text with section header removed. >>> remove_section_header('----\nfoo\nbar\n') 'foo\nbar\n' >>> remove_section_header('===\nfoo\nbar\n') 'foo\nbar\n' """ stripped = text.lstrip() if not stripped: return text first = stripped[0] return ( text if ( first.isalnum() or first.isspace() or stripped.splitlines()[0].strip(first).strip() ) else stripped.lstrip(first).lstrip() ) def 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:]) def unwrap_summary(summary): """Return summary with newlines removed in preparation for wrapping.""" return re.sub(r"\s*\n\s*", " ", summary) def wrap_summary(summary, initial_indent, subsequent_indent, wrap_length): """Return line-wrapped summary text.""" if wrap_length > 0: return textwrap.fill( unwrap_summary(summary), width=wrap_length, initial_indent=initial_indent, subsequent_indent=subsequent_indent, ).strip() else: return summary def 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 ------- description : str The description wrapped at wrap_length characters. """ text = strip_leading_blank_lines(text) # Do not modify doctests at all. if ">>>" in text: return text text = reindent(text, indentation).rstrip() # Ignore possibly complicated cases. if wrap_length <= 0 or ( not force_wrap and ( is_some_sort_of_code(text) or do_find_directives(text) or is_some_sort_of_list(text, strict, rest_sections, style) ) ): return text lines = 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 : str 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 def _do_join_field_body(text, field_idx, idx): """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. 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 def _field_over_url( field_idx: List[Tuple[int, int]], url_idx: List[Tuple[int, int]], ): """Remove URL indices that overlap with filed 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. """ for _fieldl, _fieldu in field_idx: for _key, _value in enumerate(url_idx): if ( _value[0] == _fieldl or _value[0] == _fieldu or _value[1] == _fieldl or _value[1] == _fieldu ): url_idx.pop(_key) return url_idx docformatter-1.7.5/src/docformatter/util.py000066400000000000000000000107351445340205700210360ustar00rootroot00000000000000#!/usr/bin/env python # # 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. """This module provides docformatter utility functions.""" # Standard Library Imports import os import re import sysconfig unicode = str _PYTHON_LIBS = set(sysconfig.get_paths().values()) def find_py_files(sources, recursive, exclude=None): """Find Python source files. Parameters - sources: iterable with paths as strings. - recursive: drill down directories if True. - exclude: string based on which directories and files are excluded. Return: yields paths to found files. """ def not_hidden(name): """Return True if file 'name' isn't .hidden.""" return not name.startswith(".") def is_excluded(name, exclude): """Return True if file 'name' is excluded.""" return ( any( re.search(re.escape(str(e)), name, re.IGNORECASE) for e in exclude ) if exclude else False ) for name in sorted(sources): if recursive and os.path.isdir(name): for root, dirs, children in os.walk(unicode(name)): dirs[:] = [ d for d in dirs if not_hidden(d) and not is_excluded(d, _PYTHON_LIBS) ] dirs[:] = sorted( [d for d in dirs if not is_excluded(d, exclude)] ) files = sorted( [ f for f in children if not_hidden(f) and not is_excluded(f, exclude) ] ) for filename in files: if filename.endswith(".py") and not is_excluded( root, exclude ): yield os.path.join(root, filename) else: yield name def has_correct_length(length_range, start, end): """Determine if the line under test is within 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 is correct length or length range is None, else 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) ) docformatter-1.7.5/tests/000077500000000000000000000000001445340205700153635ustar00rootroot00000000000000docformatter-1.7.5/tests/__init__.py000066400000000000000000000035621445340205700175020ustar00rootroot00000000000000# 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.5/tests/_data/000077500000000000000000000000001445340205700164335ustar00rootroot00000000000000docformatter-1.7.5/tests/_data/pyproject.toml000066400000000000000000000000711445340205700213450ustar00rootroot00000000000000[tool.docformatter] recursive = true wrap-summaries = 82 docformatter-1.7.5/tests/_data/setup.cfg000066400000000000000000000001101445340205700202440ustar00rootroot00000000000000[docformatter] blank = False wrap-summaries = 79 wrap-descriptions = 72 docformatter-1.7.5/tests/_data/string_files/000077500000000000000000000000001445340205700211235ustar00rootroot00000000000000docformatter-1.7.5/tests/_data/string_files/do_format_code.toml000066400000000000000000000131301445340205700247620ustar00rootroot00000000000000[one_line] instring='''def foo(): """ Hello foo. """''' outstring='''def foo(): """Hello foo."""''' [module_docstring] instring='''#!/usr/env/bin python """This is a module docstring. 1. One 2. Two """ """But this is not."""''' outstring='''#!/usr/env/bin python """This is a module docstring. 1. One 2. Two """ """But this is not."""''' [newline_module_variable] instring=''' CONST = 123 """docstring for CONST.""" ''' outstring=''' CONST = 123 """docstring for CONST.""" ''' [class_docstring] instring=''' class TestClass: """This is a class docstring. :cvar test_int: a class attribute. ..py.method: big_method() """ ''' outstring=''' class TestClass: """This is a class docstring. :cvar test_int: a class attribute. ..py.method: big_method() """ ''' [newline_class_variable] instring=''' 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.""" ''' outstring=''' 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.""" ''' [newline_outside_docstring] instring=''' def new_function(): """Description of function.""" found = next( (index for index, line in enumerate(split) if line.strip()), 0 ) return "\n".join(split[found:]) ''' outstring=''' 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] instring=''' def foo():\r """\r Hello\r foo. This is a docstring.\r """\r ''' outstring='''def foo():\r """\r Hello\r foo. This is a docstring.\r """\r ''' [issue_51] instring='''def my_func(): """Summary of my function.""" pass''' outstring='''def my_func(): """Summary of my function.""" pass''' [issue_51_2] instring=''' def crash_rocket(location): # pragma: no cover """This is a docstring following an in-line comment.""" return location''' outstring=''' def crash_rocket(location): # pragma: no cover """This is a docstring following an in-line comment.""" return location''' [issue_97] instring='''def pytest_addoption(parser: pytest.Parser) -> " None: register_toggle.pytest_addoption(parser) ''' outstring='''def pytest_addoption(parser: pytest.Parser) -> " None: register_toggle.pytest_addoption(parser) ''' [issue_97_2] instring='''def pytest_addoption(parser: pytest.Parser) -> None: # pragma: no cover register_toggle.pytest_addoption(parser) ''' outstring='''def pytest_addoption(parser: pytest.Parser) -> None: # pragma: no cover register_toggle.pytest_addoption(parser) ''' [issue_130] instring=''' 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 ''' outstring=''' 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] instring=''' class TestClass: """This is a class docstring. :cvar test_int: a class attribute. ..py.method: big_method() """ ''' outstring=''' class TestClass: """This is a class docstring. :cvar test_int: a class attribute. ..py.method: big_method() """ ''' [issue_139_2] instring=""" class TestClass: variable = 1 """ outstring=""" class TestClass: variable = 1 """ [issue_156] instring=''' 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() ''' outstring=''' 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_173] instring=''' class Foo: @abstractmethod def bar(self): """This is a description.""" @abstractmethod def baz(self): """This is a second description.""" ''' outstring=''' class Foo: @abstractmethod def bar(self): """This is a description.""" @abstractmethod def baz(self): """This is a second description.""" ''' [issue_187] instring=''' #!/usr/bin/env python """a.py""" ''' outstring=''' #!/usr/bin/env python """a.py.""" ''' [issue_203] instring=''' #!/usr/bin/env python import os from typing import Iterator """Don't remove this comment, it's cool.""" IMPORTANT_CONSTANT = "potato" ''' outstring=''' #!/usr/bin/env python import os from typing import Iterator """Don't remove this comment, it's cool.""" IMPORTANT_CONSTANT = "potato" ''' [issue_243] instring='''def foo(bar): """Return `foo` using `bar`. Description.""" ''' outstring='''def foo(bar): """Return `foo` using `bar`. Description. """ ''' docformatter-1.7.5/tests/_data/string_files/do_format_docstrings.toml000066400000000000000000000101051445340205700262260ustar00rootroot00000000000000[one_line] instring='''""" Hello. """''' outstring='''"""Hello."""''' [summary_end_quote] instring='''""" "Hello" """''' outstring='''""""Hello"."""''' [bad_indentation] instring='''"""Hello. This should be indented but it is not. The next line should be indented too. And this too. """''' outstring='''"""Hello. This should be indented but it is not. The next line should be indented too. And this too. """''' [too_much_indentation] instring='''"""Hello. This should be dedented. 1. This too. 2. And this. 3. And this. """''' outstring='''"""Hello. This should be dedented. 1. This too. 2. And this. 3. And this. """''' [trailing_whitespace] instring='''"""Hello. This should be not have trailing whitespace. The next line should not have trailing whitespace either. """''' outstring='''"""Hello. This should be not have trailing whitespace. The next line should not have trailing whitespace either. """''' [empty_docstring] instring='''""""""''' outstring='''""""""''' [no_summary_period] instring='''""" Hello """''' outstring='''"""Hello."""''' [single_quotes] instring="""''' Hello. '''""" outstring='''"""Hello."""''' [single_quotes_multiline] instring="""''' Return x factorial. This uses math.factorial. '''""" outstring='''"""Return x factorial. This uses math.factorial. """''' [skip_underlined_summary] instring='''""" Foo bar ------- This is more. """''' outstring='''""" Foo bar ------- This is more. """''' [issue_156] instring='''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 (?)''' outstring='''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_157] instring='''""".. 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 """''' outstring='''""".. 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_176] instring='''def Class1: """Class.""" #noqa attribute """Attr.""" def Class2: """Class.""" attribute """Attr.""" def Class3: """Class docstring. With long description. """ #noqa attribute """Attr."""''' outstring='''def Class1: """Class.""" #noqa attribute """Attr.""" def Class2: """Class.""" attribute """Attr.""" def Class3: """Class docstring. With long description. """ #noqa attribute """Attr."""''' [issue_193] instring='''""" eBay kinda suss """''' outstring='''"""eBay kinda suss."""''' docformatter-1.7.5/tests/_data/string_files/format_black.toml000066400000000000000000000036211445340205700244460ustar00rootroot00000000000000[quote_no_space] instring='''""" This one-line docstring will not have a leading space."""''' outstring='''"""This one-line docstring will not have a leading space."""''' [quote_space] instring='''""""This" quote starting one-line docstring will have a leading space."""''' outstring='''""" "This" quote starting one-line docstring will have a leading space."""''' [quote_space_2] instring='''""""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. """''' outstring='''""" "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. """''' [strip_blank_lines] instring=''' 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''' outstring=''' 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''' [issue_176] instring='''class C: """Class.""" #noqa attr: int """Attr."""''' outstring='''class C: """Class.""" #noqa attr: int """Attr."""''' docformatter-1.7.5/tests/_data/string_files/format_code.toml000066400000000000000000000122141445340205700243020ustar00rootroot00000000000000[non_docstring] instring='''x = """This is not a docstring."""''' outstring='''x = """This is not a docstring."""''' [tabbed_indentation] instring='''def foo(): """ Hello foo. """ if True: x = 1''' outstring='''def foo(): """Hello foo.""" if True: x = 1''' [mixed_indentation] instring='''def foo(): """ Hello foo. """ if True: x = 1''' outstring='''def foo(): """Hello foo.""" if True: x = 1''' [escaped_newlines] instring='''def foo(): """ Hello foo. """ x = \ 1''' outstring='''def foo(): """Hello foo.""" x = \ 1''' [code_comments] instring='''def foo(): """ Hello foo. """ # My comment # My comment with escape \ 123''' outstring='''def foo(): """Hello foo.""" # My comment # My comment with escape \ 123''' [inline_comment] instring='''def foo(): """ Hello foo. """ def test_method_no_chr_92(): the501(92) # \''' outstring='''def foo(): """Hello foo.""" def test_method_no_chr_92(): the501(92) # \''' [raw_lowercase] instring='''def foo(): r""" Hello raw foo. """''' outstring='''def foo(): r"""Hello raw foo."""''' [raw_uppercase] instring='''def foo(): R""" Hello Raw foo. """''' outstring='''def foo(): R"""Hello Raw foo."""''' [raw_lowercase_single] instring="""def foo(): r''' Hello raw foo. '''""" outstring='''def foo(): r"""Hello raw foo."""''' [raw_uppercase_single] instring="""def foo(): R''' Hello Raw foo. '''""" outstring='''def foo(): R"""Hello Raw foo."""''' [unicode_lowercase] instring='''def foo(): u""" Hello unicode foo. """''' outstring='''def foo(): u"""Hello unicode foo."""''' [unicode_uppercase] instring='''def foo(): U""" Hello Unicode foo. """''' outstring='''def foo(): U"""Hello Unicode foo."""''' [unicode_lowercase_single] instring="""def foo(): u''' Hello unicode foo. '''""" outstring='''def foo(): u"""Hello unicode foo."""''' [unicode_uppercase_single] instring="""def foo(): U''' Hello Unicode foo. '''""" outstring='''def foo(): U"""Hello Unicode foo."""''' [nested_triple] instring="""def foo(): '''Hello foo. \"\"\"abc\"\"\" '''""" outstring="""def foo(): '''Hello foo. \"\"\"abc\"\"\" '''""" [multiple_sentences] instring='''def foo(): """ Hello foo. This is a docstring. """''' outstring='''def foo(): """Hello foo. This is a docstring. """''' [multiple_sentences_same_line] instring='''def foo(): """ Hello foo. This is a docstring. """''' outstring='''def foo(): """Hello foo. This is a docstring. """''' [multiline_summary] instring='''def foo(): """ Hello foo. This is a docstring. """''' outstring='''def foo(): """Hello foo. This is a docstring. """''' [empty_lines] instring='''def foo(): """ Hello foo and this is a docstring. More stuff. """''' outstring='''def foo(): """Hello foo and this is a docstring. More stuff. """''' [class_empty_lines] instring='''class Foo: """ Hello foo and this is a docstring. More stuff. """''' outstring='''class Foo: """Hello foo and this is a docstring. More stuff. """''' instring_2='''def foo(): class Foo: """Summary.""" pass''' outstring_2='''def foo(): class Foo: """Summary.""" pass''' [method_empty_lines] instring='''class Foo: def foo(self): """Summary.""" pass''' outstring='''class Foo: def foo(self): """Summary.""" pass''' [trailing_whitespace] instring='''def foo(): """ Hello foo and this is a docstring. More stuff. """''' outstring='''def foo(): """Hello foo and this is a docstring. More stuff. """''' [parameter_list] instring='''def foo(): """Test one - first two - second """''' outstring='''def foo(): """Test. one - first two - second """''' [single_quote] instring="""def foo(): 'Just a regular string' """ outstring="""def foo(): 'Just a regular string' """ [double_quote] instring="""def foo(): "Just a regular string" """ outstring="""def foo(): "Just a regular string" """ [nested_triple_quote] instring='''def foo(): 'Just a """foo""" string' ''' outstring='''def foo(): 'Just a """foo""" string' ''' [first_line_assignment] instring='''def foo(): x = """Just a regular string. Alpha.""" ''' outstring='''def foo(): x = """Just a regular string. Alpha.""" ''' [regular_strings] instring='''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 """''' outstring='''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] instring='''""" ''' outstring='''""" ''' [slash_r] instring='''"""\r''' outstring='''"""\r''' [slash_r_slash_n] instring='''"""\r\n''' outstring='''"""\r\n''' docformatter-1.7.5/tests/_data/string_files/format_code_ranges.toml000066400000000000000000000020151445340205700256370ustar00rootroot00000000000000[range_miss] instring=''' def f(x): """ This is a docstring. That should be on more lines""" pass def g(x): """ Badly indented docstring""" pass''' outstring=''' def f(x): """ This is a docstring. That should be on more lines""" pass def g(x): """ Badly indented docstring""" pass''' [range_hit] instring=''' def f(x): """ This is a docstring. That should be on more lines""" pass def g(x): """ Badly indented docstring""" pass''' outstring=''' def f(x): """This is a docstring. That should be on more lines """ pass def g(x): """ Badly indented docstring""" pass''' [length_ignore] instring=''' def f(x): """This is a docstring. That should be on less lines """ pass def g(x): """ Badly indented docstring""" pass''' outstring=''' def f(x): """This is a docstring. That should be on less lines """ pass def g(x): """Badly indented docstring.""" pass''' docformatter-1.7.5/tests/_data/string_files/format_epytext.toml000066400000000000000000000050611445340205700250740ustar00rootroot00000000000000[epytext] instring='''"""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. """''' outstring='''"""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] instring='''"""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. """''' outstring='''"""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. """''' docformatter-1.7.5/tests/_data/string_files/format_lists.toml000066400000000000000000000027671445340205700245420ustar00rootroot00000000000000[numbered] instring='''"""Hello. 1. This should be indented but it is not. The next line should be indented too. But this is okay. """''' outstring='''"""Hello. 1. This should be indented but it is not. The next line should be indented too. But this is okay. """''' [parameter.dash] instring='''"""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. """''' outstring='''"""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] instring='''"""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. """''' outstring='''"""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] instring='''""" one two three four five six seven eight nine ten eleven """''' outstring='''""" one two three four five six seven eight nine ten eleven """''' [issue_239] instring='''"""CC. C. C c :math:`[0, 1]`. """''' outstring='''"""CC. C. C c :math:`[0, 1]`. """''' docformatter-1.7.5/tests/_data/string_files/format_sphinx.toml000066400000000000000000000160631445340205700247070ustar00rootroot00000000000000[sphinx] instring='''"""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 """''' outstring='''"""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] instring='''"""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 """''' outstring='''"""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 """''' [issue_215] instring='''"""Create or return existing HTTP session. :return: Requests :class:`~requests.Session` object """''' outstring='''"""Create or return existing HTTP session. :return: Requests :class:`~requests.Session` object """''' [issue_217_222] instring='''"""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. """''' outstring='''"""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_224] instring='''""" Add trackers to a torrent. :raises NotFound404Error: :param torrent_hash: hash for torrent :param urls: tracker URLs to add to torrent :return: None """''' outstring='''"""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] instring='''"""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. """''' outstring='''"""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] instring='''"""CC. :meth:`!X` """''' outstring='''"""CC. :meth:`!X` """''' [issue_229_2] instring='''"""CC. :math: `-` """''' outstring='''"""CC. :math: `-` """''' [issue_230] instring='''"""CC. :math:`-` :param d: blabla :param list(str) l: more blabla. """''' outstring= '''"""CC. :math:`-` :param d: blabla :param list(str) l: more blabla. """''' [issue_232] instring='''def function: """ :param x: X :param y: Y """''' outstring='''def function: """ :param x: X :param y: Y """''' [issue_234] instring=''' """CC. :math:`f(0) = 1`. XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXX """''' outstring='''"""CC. :math:`f(0) = 1`. XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXX """''' [issue_235] instring='''"""CC. C. C, :math:`[0, 1]`. """''' outstring='''"""CC. C. C, :math:`[0, 1]`. """''' [issue_239] instring='''""" Summary. :raises InvalidRequest400Error: :raises NotFound404Error: :raises Conflict409Error: :param param: asdf """''' outstring='''"""Summary. :raises InvalidRequest400Error: :raises NotFound404Error: :raises Conflict409Error: :param param: asdf """''' [issue_245] instring='''"""Some f. :param a: Some param. :raises my.package.MyReallySrsError: Bad things happened. """''' outstring='''"""Some f. :param a: Some param. :raises my.package.MyReallySrsError: Bad things happened. """''' [issue_250] instring=''' """CC. c. c c :math:`[0, 1]`. """''' outstring='''"""CC. c. c c :math:`[0, 1]`. """''' [issue_253] instring='''""" My test fixture. :param caplog: Pytest caplog fixture. :yield: Until test complete, then run cleanup. """''' outstring='''""" My test fixture. :param caplog: Pytest caplog fixture. :yield: Until test complete, then run cleanup. """''' docformatter-1.7.5/tests/_data/string_files/format_style_options.toml000066400000000000000000000011031445340205700262760ustar00rootroot00000000000000[no_blank] instring='''""" Hello. Description. """''' outstring='''"""Hello. Description. """''' [presummary_newline] instring='''""" Hello. Description. """''' outstring='''""" Hello. Description. """''' [summary_multiline] instring='''"""This one-line docstring will be multi-line"""''' outstring='''""" This one-line docstring will be multi-line. """''' [presummary_space] instring='''"""This one-line docstring will have a leading space."""''' outstring='''""" This one-line docstring will have a leading space."""''' docformatter-1.7.5/tests/_data/string_files/format_urls.toml000066400000000000000000000554071445340205700243700ustar00rootroot00000000000000[inline] instring='''"""This is a docstring with a link. Here is an elaborate description containing a link. `Area Under the Receiver Operating Characteristic Curve (ROC AUC) `_. """''' outstring='''"""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] instring='''"""This is yanf with a short link. See `the link `_ for more details. """''' outstring='''"""This is yanf with a short link. See `the link `_ for more details. """''' [inline.long] instring='''"""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. """''' outstring='''"""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] instring='''""" `Source of this snippet `_. """''' outstring='''""" `Source of this snippet `_. """''' [issue_75] instring='''"""This is another docstring with `a link`_. .. a link: http://www.reliqual.com/wiki/how_to_use_ramstk/verification_and_validation_module/index.html. """''' outstring='''"""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] instring='''"""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. """''' outstring='''"""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] instring='''"""This is yanf with a short link. See http://www.reliaqual.com for examples. """''' outstring='''"""This is yanf with a short link. See http://www.reliaqual.com for examples. """''' [issue_140] instring='''"""This is a docstring with a link that causes a wrap. See `the link `_ for more details. """''' outstring='''"""This is a docstring with a link that causes a wrap. See `the link `_ for more details. """''' [issue_140_2] instring='''"""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. """''' outstring='''"""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] instring='''"""Do something. See https://www.postgresql.org/docs/current/static/role-removal.html """''' outstring='''"""Do something. See https://www.postgresql.org/docs/current/static/role-removal.html """''' [issue_145] instring='''""" .. _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 """''' outstring='''""" .. _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] instring='''""" Translates incoming json to a processable Entity. Stackoverflow reference: """''' outstring='''"""Translates incoming json to a processable Entity. Stackoverflow reference: """''' [issue_157] instring='''"""Get the Python type of a Click parameter. See the list of `custom types provided by Click `_. """''' outstring='''"""Get the Python type of a Click parameter. See the list of `custom types provided by Click `_. """''' [issue_157_2] instring='''"""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. """''' outstring='''"""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] instring='''"""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. """''' outstring='''"""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] instring='''"""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 `. """''' outstring='''"""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] instring='''"""Locate and call the ``mpm`` CLI. The output must supports both `Xbar dialect `_ and `SwiftBar dialect `_. """''' outstring='''"""Locate and call the ``mpm`` CLI. The output must supports both `Xbar dialect `_ and `SwiftBar dialect `_. """''' [issue_157_6] instring='''"""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. """''' outstring='''"""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_7] instring='''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 `_. """''' outstring='''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] instring='''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. """''' outstring='''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] instring='''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 `_. """''' outstring='''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] instring='''"""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. """''' outstring='''"""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_157_11] instring='''"""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. """''' outstring='''"""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] instring='''"""Blah blah. This will normally be used with https://aaaaaaaa.bbb.ccccccccc.com/xxxxx/xxx_xxxxxxxxxxx to generate the xxx """''' outstring='''"""Blah blah. This will normally be used with https://aaaaaaaa.bbb.ccccccccc.com/xxxxx/xxx_xxxxxxxxxxx to generate the xxx """''' [issue_180] instring='''"""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/ """''' outstring='''"""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] instring='''"""This method doesn't do anything. https://example.com/this-is-just-a-long-url/designed-to-trigger/the-wrapping-of-the-description """''' outstring='''"""This method doesn't do anything. https://example.com/this-is-just-a-long-url/designed-to-trigger/the-wrapping-of-the-description """''' [issue_199] instring='''""" 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. """''' outstring='''"""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] instring='''"""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/ """''' outstring='''"""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_218] instring='''"""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 """''' outstring='''"""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 """''' docformatter-1.7.5/tests/_data/string_files/format_wrap.toml000066400000000000000000000074121445340205700243450ustar00rootroot00000000000000[unwrap] instring='''"This is a sentence."''' outstring='''"This is a sentence."''' [weird_punctuation] instring='''"""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(). """''' outstring='''"""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] instring='''"""Hello. This should be indented but it is not. The next line should be indented too. But this is okay. """''' outstring='''"""Hello. This should be indented but it is not. The next line should be indented too. But this is okay. """''' [ignore_doctest] instring='''"""Hello. >>> 4 4 """''' outstring='''"""Hello. >>> 4 4 """''' [ignore_summary_doctest] instring='''""" >>> 4 4 """''' outstring='''""" >>> 4 4 """''' [same_indentation_doctest] instring='''"""Foo bar bing bang. >>> tests = DocTestFinder().find(_TestClass) >>> runner = DocTestRunner(verbose=False) >>> tests.sort(key = lambda test: test.name) """''' outstring='''"""Foo bar bing bang. >>> tests = DocTestFinder().find(_TestClass) >>> runner = DocTestRunner(verbose=False) >>> tests.sort(key = lambda test: test.name) """''' [force_wrap] instring='''""" num_iterations is the number of updates - instead of a better definition of convergence. """''' outstring='''"""num_iterations is the number of updates - instead of a better definition of convergence."""''' [summary_wrap_tab] instring=''' """Some summary x x x x."""''' outstring='''"""Some summary x x x x."""''' [one_line_wrap_newline] instring='''"""This one-line docstring will be multi-line because it's quite long."""''' outstring='''"""This one-line docstring will be multi-line because it's quite long. """''' [one_line_no_wrap] instring='''"""This one-line docstring will not be wrapped and quotes will be in-line."""''' outstring='''"""This one-line docstring will not be wrapped and quotes will be in-line."""''' [class_attribute_wrap] instring='''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."""''' outstring='''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."""''' [issue_79] instring='''def function2(): """Hello yeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeet -v."""''' outstring='''def function2(): """Hello yeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeet -v."""''' [issue_127] instring='''"""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. """''' outstring='''"""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. """''' docformatter-1.7.5/tests/_data/tox.ini000066400000000000000000000001101445340205700177360ustar00rootroot00000000000000[docformatter] wrap-descriptions = 72 wrap-summaries = 79 blank = False docformatter-1.7.5/tests/conftest.py000066400000000000000000000156361445340205700175750ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.conftest.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. """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.5/tests/formatter/000077500000000000000000000000001445340205700173665ustar00rootroot00000000000000docformatter-1.7.5/tests/formatter/__init__.py000066400000000000000000000000001445340205700214650ustar00rootroot00000000000000docformatter-1.7.5/tests/formatter/test_do_format_code.py000066400000000000000000000307011445340205700237440ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.test_do_format_code.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. """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 class TestDoFormatCode: """Class for testing _do_format_code() with no arguments.""" with open("tests/_data/string_files/do_format_code.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_do_format_code(self, test_args, args): """Should place one-liner on single line.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["one_line"]["instring"] outstring = self.TEST_STRINGS["one_line"]["outstring"] assert outstring == uut._do_format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_do_format_code_with_module_docstring(self, test_args, args): """Should format module docstrings.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["module_docstring"]["instring"] outstring = self.TEST_STRINGS["module_docstring"]["outstring"] assert outstring == uut._do_format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_strip_blank_line_after_module_variable( self, test_args, args, ): """Strip newlines between module variable definition and docstring.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["newline_module_variable"]["instring"] outstring = self.TEST_STRINGS["newline_module_variable"]["outstring"] assert outstring == uut._do_format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_class_docstring(self, test_args, args): """Format class docstring.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["class_docstring"]["instring"] outstring = self.TEST_STRINGS["class_docstring"]["outstring"] assert outstring == uut._do_format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_strip_blank_line_after_class_variable( self, test_args, args, ): """Strip any newlines between a class variable definition and docstring. See requirement . """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["newline_class_variable"]["instring"] outstring = self.TEST_STRINGS["newline_class_variable"]["outstring"] assert outstring == uut._do_format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_do_format_code_keep_newlines_outside_docstring(self, test_args, args): """Should keep newlines in code following docstring.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["newline_outside_docstring"]["instring"] outstring = self.TEST_STRINGS["newline_outside_docstring"]["outstring"] assert outstring == uut._do_format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_dominant_line_ending_style_preserved(self, test_args, args): """Should retain carriage return line endings.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["preserve_line_ending"]["instring"] outstring = self.TEST_STRINGS["preserve_line_ending"]["outstring"] assert outstring == uut._do_format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_additional_empty_line_before_doc(self, test_args, args): """Should remove empty line between function def and docstring. See issue #51. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_51"]["instring"] outstring = self.TEST_STRINGS["issue_51"]["outstring"] assert outstring == uut._do_format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_extra_newline_following_comment(self, test_args, args): """Should remove extra newline following in-line comment. See issue #51. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_51_2"]["instring"] outstring = self.TEST_STRINGS["issue_51_2"]["outstring"] assert outstring == uut._do_format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_no_docstring(self, test_args, args): """Should leave code as is if there is no docstring. See issue #97. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_97"]["instring"] outstring = self.TEST_STRINGS["issue_97"]["outstring"] assert outstring == uut._do_format_code( instring, ) instring = self.TEST_STRINGS["issue_97_2"]["instring"] outstring = self.TEST_STRINGS["issue_97_2"]["outstring"] assert outstring == uut._do_format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_class_docstring_remove_blank_line(self, test_args, args): """Remove blank line before class docstring. See issue #139. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_139"]["instring"] outstring = self.TEST_STRINGS["issue_139"]["outstring"] assert outstring == uut._do_format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_class_docstring_keep_blank_line(self, test_args, args): """Keep blank line after class definition if there is no docstring. See issue #139. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_139_2"]["instring"] outstring = self.TEST_STRINGS["issue_139_2"]["outstring"] assert outstring == uut._do_format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_strip_blank_line_after_method_docstring( self, test_args, args, ): """Strip any newlines after a method docstring. See requirement PEP_257_4.4, issue #130. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_130"]["instring"] outstring = self.TEST_STRINGS["issue_130"]["outstring"] assert outstring == uut._do_format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_do_not_touch_function_no_docstring( self, test_args, args, ): """Do not remove newlines in functions with no docstring. See issue #156. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_156"]["instring"] outstring = self.TEST_STRINGS["issue_156"]["outstring"] assert outstring == uut._do_format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_do_format_code_keep_newline_for_stub_functions(self, test_args, args): """Should keep newline after docstring in stub functions. See issue #156 and issue #173. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_156_173"]["instring"] outstring = self.TEST_STRINGS["issue_156_173"]["outstring"] assert outstring == uut._do_format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_keep_newline_after_shebang( self, test_args, args, ): """Do not remove newlines following the shebang. See issue #187. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_187"]["instring"] outstring = self.TEST_STRINGS["issue_187"]["outstring"] assert outstring == uut._do_format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_keep_newline_after_import( self, test_args, args, ): """Do not remove newlines following the import section. See issue #203. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_203"]["instring"] outstring = self.TEST_STRINGS["issue_203"]["outstring"] assert outstring == uut._do_format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_with_backtick_in_summary( self, test_args, args, ): """Format docstring with summary containing backticks. See issue #243. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_243"]["instring"] outstring = self.TEST_STRINGS["issue_243"]["outstring"] assert outstring == uut._do_format_code( instring, ) docformatter-1.7.5/tests/formatter/test_do_format_docstring.py000066400000000000000000000241051445340205700250270ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.test_format_docstring.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. """Module for testing the Formatter class.""" # 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 INDENTATION = " " class TestFormatDocstring: """Class for testing _do_format_docstring() with no arguments.""" with open("tests/_data/string_files/do_format_docstrings.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_one_line_docstring(self, test_args, args): """Return one-line docstring.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["one_line"]["instring"] outstring = self.TEST_STRINGS["one_line"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_docstring_with_summary_that_ends_in_quote(self, test_args, args): """Return one-line docstring with period after quote.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["summary_end_quote"]["instring"] outstring = self.TEST_STRINGS["summary_end_quote"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [["--wrap-descriptions", "44", ""]]) def test_format_docstring_with_bad_indentation(self, test_args, args): """Add spaces to indentation when too few.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["bad_indentation"]["instring"] outstring = self.TEST_STRINGS["bad_indentation"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_docstring_with_too_much_indentation(self, test_args, args): """Remove spaces from indentation when too many.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["too_much_indentation"]["instring"] outstring = self.TEST_STRINGS["too_much_indentation"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [["--wrap-descriptions", "52", ""]]) def test_format_docstring_with_trailing_whitespace(self, test_args, args): """Remove trailing white space.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["trailing_whitespace"]["instring"] outstring = self.TEST_STRINGS["trailing_whitespace"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_docstring_with_empty_docstring(self, test_args, args): """Do nothing with empty docstring.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["empty_docstring"]["instring"] outstring = self.TEST_STRINGS["empty_docstring"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_docstring_with_no_period(self, test_args, args): """Add period to end of one-line and summary line.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["no_summary_period"]["instring"] outstring = self.TEST_STRINGS["no_summary_period"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_docstring_with_single_quotes(self, test_args, args): """Replace single triple quotes with triple double quotes.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["single_quotes"]["instring"] outstring = self.TEST_STRINGS["single_quotes"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_docstring_with_single_quotes_multi_line(self, test_args, args): """Replace single triple quotes with triple double quotes.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["single_quotes_multiline"]["instring"] outstring = self.TEST_STRINGS["single_quotes_multiline"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_docstring_leave_underlined_summaries_alone(self, test_args, args): """Leave underlined summary lines as is.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["skip_underlined_summary"]["instring"] outstring = self.TEST_STRINGS["skip_underlined_summary"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_docstring_leave_blank_line_after_variable_def( self, test_args, args, ): """Leave blank lines after any variable beginning with 'def'. See issue #156. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_156"]["instring"] outstring = self.TEST_STRINGS["issue_156"]["outstring"] assert outstring == uut._do_format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_docstring_leave_directive_alone(self, test_args, args): """Leave docstrings that have a reST directive in the summary alone. See issue #157. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_157"]["instring"] outstring = self.TEST_STRINGS["issue_157"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_docstring_leave_blank_line_after_comment( self, test_args, args, ): """Leave blank lines after docstring followed by a comment. See issue #176. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_176"]["instring"] outstring = self.TEST_STRINGS["issue_176"]["outstring"] assert outstring == uut._do_format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [["--non-cap", "eBay", "iPad", "-c", ""]]) def test_format_docstring_with_non_cap_words(self, test_args, args): """Capitalize words not found in the non_cap list. See issue #193. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_193"]["instring"] outstring = self.TEST_STRINGS["issue_193"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) docformatter-1.7.5/tests/formatter/test_format_black.py000066400000000000000000000110131445340205700234170ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.test_format_black.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. """Module for testing the Formatter class with the --black option.""" # 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 INDENTATION = " " class TestFormatWrapBlack: """Class for testing _do_format_docstring() with line wrapping and black option.""" with open("tests/_data/string_files/format_black.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) @pytest.mark.unit @pytest.mark.parametrize( "args", [ [ "--black", "", ] ], ) def test_format_docstring_black( self, test_args, args, ): """Format with black options when --black specified. Add a space between the opening quotes and the summary if content starts with a quote. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["quote_no_space"]["instring"] outstring = self.TEST_STRINGS["quote_no_space"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) instring = self.TEST_STRINGS["quote_space"]["instring"] outstring = self.TEST_STRINGS["quote_space"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) instring = self.TEST_STRINGS["quote_space_2"]["instring"] outstring = self.TEST_STRINGS["quote_space_2"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [ [ "--black", "", ] ], ) def test_format_code_strip_blank_lines( self, test_args, args, ): """Blank lines are stripped in black mode.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["strip_blank_lines"]["instring"] outstring = self.TEST_STRINGS["strip_blank_lines"]["outstring"] assert outstring == uut._do_format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [ [ "--black", "", ] ], ) def test_format_docstring_black_keep_newline_after_comment( self, test_args, args, ): """Retain the newline after a docstring with an inline comment. See issue #176. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_176"]["instring"] outstring = self.TEST_STRINGS["issue_176"]["outstring"] assert outstring == uut._do_format_code( instring, ) docformatter-1.7.5/tests/formatter/test_format_code.py000066400000000000000000000451251445340205700232700ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.test_format_code.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. """Module for testing the Formattor._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 class TestFormatCode: """Class for testing _format_code() with no arguments.""" with open("tests/_data/string_files/format_code.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_should_ignore_non_docstring(self, test_args, args): """Should ignore triple quoted strings that are assigned values.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["non_docstring"]["instring"] outstring = self.TEST_STRINGS["non_docstring"]["outstring"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_with_empty_string(self, test_args, args): """Should do nothing with an empty string.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) assert not uut._format_code("") assert not uut._format_code("") @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_with_tabs(self, test_args, args): """Should retain tabbed indentation.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["tabbed_indentation"]["instring"] outstring = self.TEST_STRINGS["tabbed_indentation"]["outstring"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_with_mixed_tabs(self, test_args, args): """Should retain mixed tabbed and spaced indentation.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["mixed_indentation"]["instring"] outstring = self.TEST_STRINGS["mixed_indentation"]["outstring"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_with_escaped_newlines(self, test_args, args): """Should leave escaped newlines in code untouched.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["escaped_newlines"]["instring"] outstring = self.TEST_STRINGS["escaped_newlines"]["outstring"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_with_comments(self, test_args, args): """Should leave comments as is.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["code_comments"]["instring"] outstring = self.TEST_STRINGS["code_comments"]["outstring"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_with_escaped_newline_in_inline_comment(self, test_args, args): """Should leave code with inline comment as is.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["inline_comment"]["instring"] outstring = self.TEST_STRINGS["inline_comment"]["outstring"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_raw_docstring_double_quotes(self, test_args, args): """Should format raw docstrings with triple double quotes. See requirement PEP_257_2. See issue #54 for request to handle raw docstrings. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["raw_lowercase"]["instring"] outstring = self.TEST_STRINGS["raw_lowercase"]["outstring"] assert outstring == uut._format_code( instring, ) instring = self.TEST_STRINGS["raw_uppercase"]["instring"] outstring = self.TEST_STRINGS["raw_uppercase"]["outstring"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_raw_docstring_single_quotes(self, test_args, args): """Should format raw docstrings with triple single quotes. See requirement PEP_257_2. See issue #54 for request to handle raw docstrings. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["raw_lowercase_single"]["instring"] outstring = self.TEST_STRINGS["raw_lowercase_single"]["outstring"] assert outstring == uut._format_code( instring, ) instring = self.TEST_STRINGS["raw_uppercase_single"]["instring"] outstring = self.TEST_STRINGS["raw_uppercase_single"]["outstring"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_unicode_docstring_double_quotes(self, test_args, args): """Should format unicode docstrings with triple double quotes. See requirement PEP_257_3. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["unicode_lowercase"]["instring"] outstring = self.TEST_STRINGS["unicode_lowercase"]["outstring"] assert outstring == uut._format_code( instring, ) instring = self.TEST_STRINGS["unicode_uppercase"]["instring"] outstring = self.TEST_STRINGS["unicode_uppercase"]["outstring"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_unicode_docstring_single_quotes(self, test_args, args): """Should format unicode docstrings with triple single quotes. See requirement PEP_257_3. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["unicode_lowercase_single"]["instring"] outstring = self.TEST_STRINGS["unicode_lowercase_single"]["outstring"] assert outstring == uut._format_code( instring, ) instring = self.TEST_STRINGS["unicode_uppercase_single"]["instring"] outstring = self.TEST_STRINGS["unicode_uppercase_single"]["outstring"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_skip_nested(self, test_args, args): """Should ignore nested triple quotes.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["nested_triple"]["instring"] outstring = self.TEST_STRINGS["nested_triple"]["outstring"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_with_multiple_sentences(self, test_args, args): """Should create multi-line docstring from multiple sentences.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["multiple_sentences"]["instring"] outstring = self.TEST_STRINGS["multiple_sentences"]["outstring"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_with_multiple_sentences_same_line(self, test_args, args): """Should create multi-line docstring from multiple sentences.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["multiple_sentences_same_line"]["instring"] outstring = self.TEST_STRINGS["multiple_sentences_same_line"]["outstring"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_with_multiple_sentences_multi_line_summary( self, test_args, args ): """Should put summary line on a single line.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["multiline_summary"]["instring"] outstring = self.TEST_STRINGS["multiline_summary"]["outstring"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_with_empty_lines(self, test_args, args): """Summary line on one line when wrapped, followed by empty line.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["empty_lines"]["instring"] outstring = self.TEST_STRINGS["empty_lines"]["outstring"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_with_empty_lines_class_docstring(self, test_args, args): """No blank lines before a class's docstring.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["class_empty_lines"]["instring"] outstring = self.TEST_STRINGS["class_empty_lines"]["outstring"] assert outstring == uut._format_code( instring, ) instring = self.TEST_STRINGS["class_empty_lines"]["instring_2"] outstring = self.TEST_STRINGS["class_empty_lines"]["outstring_2"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_with_empty_lines_method_docstring(self, test_args, args): """No blank lines before a method's docstring.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["method_empty_lines"]["instring"] outstring = self.TEST_STRINGS["method_empty_lines"]["outstring"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_with_trailing_whitespace(self, test_args, args): """Should strip trailing whitespace.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["trailing_whitespace"]["instring"] outstring = self.TEST_STRINGS["trailing_whitespace"]["outstring"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_with_parameters_list(self, test_args, args): """Should treat parameters list as elaborate description.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["parameter_list"]["instring"] outstring = self.TEST_STRINGS["parameter_list"]["outstring"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_ignore_code_with_single_quote(self, test_args, args): """Single single quote on first line of code should remain untouched. See requirement PEP_257_1. See issue #66 for example of docformatter breaking code when encountering single quote. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["single_quote"]["instring"] outstring = self.TEST_STRINGS["single_quote"]["outstring"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_ignore_code_with_double_quote(self, test_args, args): """Single double quotes on first line of code should remain untouched. See requirement PEP_257_1. See issue #66 for example of docformatter breaking code when encountering single quote. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["double_quote"]["instring"] outstring = self.TEST_STRINGS["double_quote"]["outstring"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_should_skip_nested_triple_quotes(self, test_args, args): """Should ignore triple quotes nested in a string.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["nested_triple_quote"]["instring"] outstring = self.TEST_STRINGS["nested_triple_quote"]["outstring"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_with_assignment_on_first_line(self, test_args, args): """Should ignore triple quotes in variable assignment.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["first_line_assignment"]["instring"] outstring = self.TEST_STRINGS["first_line_assignment"]["outstring"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_with_regular_strings_too(self, test_args, args): """Should ignore triple quoted strings after the docstring.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["regular_strings"]["instring"] outstring = self.TEST_STRINGS["regular_strings"]["outstring"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_with_syntax_error(self, test_args, args): """Should ignore single set of triple quotes followed by newline.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["syntax_error"]["instring"] outstring = self.TEST_STRINGS["syntax_error"]["outstring"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_with_syntax_error_case_slash_r(self, test_args, args): """Should ignore single set of triple quotes followed by return.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["slash_r"]["instring"] outstring = self.TEST_STRINGS["slash_r"]["outstring"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_code_with_syntax_error_case_slash_r_slash_n(self, test_args, args): """Should ignore single triple quote followed by return, newline.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["slash_r_slash_n"]["instring"] outstring = self.TEST_STRINGS["slash_r_slash_n"]["outstring"] assert outstring == uut._format_code( instring, ) docformatter-1.7.5/tests/formatter/test_format_code_ranges.py000066400000000000000000000067711445340205700246330ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.test_format_code_ranges.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. """Module for testing the Formattor._format_code() method with ranges and lengths.""" # 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 class TestFormatCodeRanges: """Class for testing _format_code() with the line_range or length_range arguments.""" with open("tests/_data/string_files/format_code_ranges.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) @pytest.mark.unit @pytest.mark.parametrize("args", [["--range", "1", "1", ""]]) def test_format_code_range_miss(self, test_args, args): """Should leave docstrings outside line range as is.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["range_miss"]["instring"] outstring = self.TEST_STRINGS["range_miss"]["outstring"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [["--range", "1", "2", ""]]) def test_format_code_range_hit(self, test_args, args): """Should format docstrings within line_range.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["range_hit"]["instring"] outstring = self.TEST_STRINGS["range_hit"]["outstring"] assert outstring == uut._format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [["--docstring-length", "1", "1", ""]]) def test_format_code_docstring_length(self, test_args, args): """Should leave docstrings outside length_range as is.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["length_ignore"]["instring"] outstring = self.TEST_STRINGS["length_ignore"]["outstring"] assert outstring == uut._format_code( instring, ) docformatter-1.7.5/tests/formatter/test_format_epytext.py000066400000000000000000000071761445340205700240640ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.test_format_epytext.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. """Module for testing the Formatter class.""" # 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 INDENTATION = " " class TestFormatWrapEpytext: """Class for testing _do_format_docstring() with line wrapping and Epytext lists.""" with open("tests/_data/string_files/format_epytext.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) @pytest.mark.unit @pytest.mark.parametrize( "args", [ [ "--wrap-descriptions", "88", "--wrap-summaries", "88", "--style", "epytext", "", ] ], ) def test_format_docstring_epytext_style( self, test_args, args, ): """Wrap epytext style parameter lists. See requirement docformatter_10.6.2 """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["epytext"]["instring"] outstring = self.TEST_STRINGS["epytext"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [ [ "--wrap-descriptions", "88", "--wrap-summaries", "88", "--style", "numpy", "", ] ], ) def test_format_docstring_non_epytext_style( self, test_args, args, ): """Ignore wrapping epytext style parameter lists when not using epytext style. See requirement docformatter_10.6.1 """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["epytext"]["numpy"]["instring"] outstring = self.TEST_STRINGS["epytext"]["numpy"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) docformatter-1.7.5/tests/formatter/test_format_lists.py000066400000000000000000000113141445340205700235050ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.test_format_lists.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. """Module for testing the Formatter class.""" # 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 INDENTATION = " " class TestFormatLists: """Class for testing format_docstring() with lists in the docstring.""" with open("tests/_data/string_files/format_lists.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) @pytest.mark.unit @pytest.mark.parametrize("args", [["--wrap-descriptions", "72", ""]]) def test_format_docstring_should_ignore_numbered_lists(self, test_args, args): """Ignore lists beginning with numbers.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["numbered"]["instring"] outstring = self.TEST_STRINGS["numbered"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [["--wrap-descriptions", "72", ""]]) def test_format_docstring_should_ignore_parameter_lists(self, test_args, args): """Ignore lists beginning with -.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["parameter"]["dash"]["instring"] outstring = self.TEST_STRINGS["parameter"]["dash"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [["--wrap-descriptions", "72", "--style", "numpy", ""]] ) def test_format_docstring_should_ignore_colon_parameter_lists( self, test_args, args ): """Ignore lists beginning with :""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["parameter"]["colon"]["instring"] outstring = self.TEST_STRINGS["parameter"]["colon"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_docstring_should_leave_list_alone(self, test_args, args): uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["many"]["short"]["columns"]["instring"] outstring = self.TEST_STRINGS["many"]["short"]["columns"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_docstring_should_leave_list_alone_with_rest(self, test_args, args): uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_239"]["instring"] outstring = self.TEST_STRINGS["issue_239"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) docformatter-1.7.5/tests/formatter/test_format_sphinx.py000066400000000000000000000274241445340205700236710ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.test_format_sphinx.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. """Module for testing the Formatter class.""" # 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 INDENTATION = " " class TestFormatWrapSphinx: """Class for testing _do_format_docstring() with line wrapping and Sphinx lists.""" with open("tests/_data/string_files/format_sphinx.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) @pytest.mark.unit @pytest.mark.parametrize( "args", [ [ "--wrap-descriptions", "88", "--wrap-summaries", "88", "--style", "sphinx", "", ] ], ) def test_format_docstring_sphinx_style( self, test_args, args, ): """Wrap sphinx style parameter lists. See requirement docformatter_10.4.2 and issue #230. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["sphinx"]["instring"] outstring = self.TEST_STRINGS["sphinx"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) # Issue #230 required adding parenthesis to the SPHINX_REGEX. instring = self.TEST_STRINGS["issue_230"]["instring"] outstring = self.TEST_STRINGS["issue_230"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [ [ "--wrap-descriptions", "88", "--wrap-summaries", "88", "--style", "numpy", "", ] ], ) def test_format_docstring_non_sphinx_style( self, test_args, args, ): """Ignore wrapping sphinx style parameter lists when not using sphinx style. See requirement docformatter_10.4.1 """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["sphinx"]["numpy"]["instring"] outstring = self.TEST_STRINGS["sphinx"]["numpy"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [ [ "--wrap-descriptions", "88", "--wrap-summaries", "88", "", ] ], ) def test_format_docstring_sphinx_style_remove_excess_whitespace( self, test_args, args, ): """Should remove unneeded whitespace. See issue #217 and #222 """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_217_222"]["instring"] outstring = self.TEST_STRINGS["issue_217_222"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [ [ "--wrap-descriptions", "88", "--wrap-summaries", "88", "", ] ], ) def test_format_docstring_sphinx_style_two_directives_in_row( self, test_args, args, ): """Should remove unneeded whitespace. See issue #215. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_215"]["instring"] outstring = self.TEST_STRINGS["issue_215"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [ [ "--wrap-descriptions", "88", "--wrap-summaries", "88", "", ] ], ) def test_format_docstring_sphinx_style_field_body_is_blank( self, test_args, args, ): """Retain newline after the field list when it's in the original docstring. Also do not return a field body that is just whitespace. See docformatter_10.4.3.2, issue #224, and issue #239. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_224"]["instring"] outstring = self.TEST_STRINGS["issue_224"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) instring = self.TEST_STRINGS["issue_239"]["instring"] outstring = self.TEST_STRINGS["issue_239"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [ [ "--wrap-descriptions", "88", "--wrap-summaries", "88", "", ] ], ) def test_format_docstring_sphinx_style_field_name_included_wrap_length( self, test_args, args, ): """Should consider field name, not just field body, when wrapping. See issue #228. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_228"]["instring"] outstring = self.TEST_STRINGS["issue_228"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [ [ "--wrap-descriptions", "88", "--wrap-summaries", "88", "", ] ], ) def test_format_docstring_sphinx_style_field_body_is_a_link( self, test_args, args, ): """Should not add a space after the field name when the body is a link. See docformatter_10.4.3.1, issue #229, issue #234, and issue #235. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_229"]["instring"] outstring = self.TEST_STRINGS["issue_229"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) instring = self.TEST_STRINGS["issue_229_2"]["instring"] outstring = self.TEST_STRINGS["issue_229_2"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) instring = self.TEST_STRINGS["issue_234"]["instring"] outstring = self.TEST_STRINGS["issue_234"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) instring = self.TEST_STRINGS["issue_235"]["instring"] outstring = self.TEST_STRINGS["issue_235"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [ [ "--wrap-descriptions", "88", "--wrap-summaries", "88", "", ] ], ) def test_format_docstring_sphinx_style_field_name_has_periods( self, test_args, args, ): """Should format sphinx field names containing a period. See issue #245. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_245"]["instring"] outstring = self.TEST_STRINGS["issue_245"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [ [ "--wrap-descriptions", "88", "--wrap-summaries", "88", "", ] ], ) def test_format_docstring_sphinx_style_ignore_directive( self, test_args, args, ): """Should not identify inline directives as sphinx field names. See issue #250. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_250"]["instring"] outstring = self.TEST_STRINGS["issue_250"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [ [ "--wrap-descriptions", "120", "--wrap-summaries", "120", "--pre-summary-newline", "--black", "", ] ], ) def test_format_docstring_sphinx_style_recognize_yield( self, test_args, args, ): """Should identify `yield` as sphinx field name. See issue #253. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_253"]["instring"] outstring = self.TEST_STRINGS["issue_253"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) docformatter-1.7.5/tests/formatter/test_format_styles.py000066400000000000000000000105521445340205700236750ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.test_format_styles.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. """Module for testing the Formatter class with various style options.""" # 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 INDENTATION = " " class TestFormatStyleOptions: """Class for testing format_docstring() when requesting style options.""" with open("tests/_data/string_files/format_style_options.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_docstring_with_no_post_description_blank( self, test_args, args, ): """Remove blank lines before closing triple quotes.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["no_blank"]["instring"] outstring = self.TEST_STRINGS["no_blank"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [["--pre-summary-newline", ""]]) def test_format_docstring_with_pre_summary_newline( self, test_args, args, ): """Remove blank line before summary.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["presummary_newline"]["instring"] outstring = self.TEST_STRINGS["presummary_newline"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [["--make-summary-multi-line", ""]]) def test_format_docstring_make_summary_multi_line( self, test_args, args, ): """Place the one-line docstring between triple quotes.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["summary_multiline"]["instring"] outstring = self.TEST_STRINGS["summary_multiline"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [["--pre-summary-space", ""]]) def test_format_docstring_pre_summary_space( self, test_args, args, ): """Place a space between the opening quotes and the summary.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["presummary_space"]["instring"] outstring = self.TEST_STRINGS["presummary_space"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) docformatter-1.7.5/tests/formatter/test_format_urls.py000066400000000000000000000415341445340205700233430ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.test_format_urls.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. """Module for testing the Formatter class.""" # 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 INDENTATION = " " class TestFormatWrapURL: """Class for testing _do_format_docstring() with line wrapping and URLs.""" with open("tests/_data/string_files/format_urls.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) @pytest.mark.unit @pytest.mark.parametrize( "args", [["--wrap-descriptions", "72", ""]], ) def test_format_docstring_with_inline_links( self, test_args, args, ): """Preserve links instead of wrapping them. See requirement docformatter_10.1.3. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["inline"]["instring"] outstring = self.TEST_STRINGS["inline"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [["--wrap-descriptions", "72", ""]], ) def test_format_docstring_with_short_inline_link( self, test_args, args, ): """Short in-line links will remain untouched. See requirement docformatter_10.1.3.1. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["inline"]["short"]["instring"] outstring = self.TEST_STRINGS["inline"]["short"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [["--wrap-descriptions", "72", ""]], ) def test_format_docstring_with_long_inline_link( self, test_args, args, ): """Should move long in-line links to line by themselves.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["inline"]["long"]["instring"] outstring = self.TEST_STRINGS["inline"]["long"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [["--wrap-descriptions", "72", ""]], ) def test_format_docstring_with_only_link( self, test_args, args, ): """Should format docstring containing only a link.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["only"]["link"]["instring"] outstring = self.TEST_STRINGS["only"]["link"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [["--wrap-descriptions", "72", ""]], ) def test_format_docstring_with_target_links( self, test_args, args, ): """Preserve links instead of wrapping them. See requirement docformatter_10.1.3, issue #75, issue #145. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_75"]["instring"] outstring = self.TEST_STRINGS["issue_75"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) instring = self.TEST_STRINGS["issue_145"]["instring"] outstring = self.TEST_STRINGS["issue_145"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [ [ "--wrap-descriptions", "72", "", ] ], ) def test_format_docstring_with_simple_link( self, test_args, args, ): """Preserve links instead of wrapping them. See requirement docformatter_10.1.3, issue #75. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_75_2"]["instring"] outstring = self.TEST_STRINGS["issue_75_2"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [ [ "--wrap-descriptions", "72", "", ] ], ) def test_format_docstring_with_short_link( self, test_args, args, ): """Short links will remain untouched. See requirement docformatter_10.1.3, issue #75. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_75_3"]["instring"] outstring = self.TEST_STRINGS["issue_75_3"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [["--wrap-descriptions", "72", ""]], ) def test_format_docstring_with_inline_link_retain_spaces( self, test_args, args, ): """In-line links shouldn't remove blank spaces between words. See issue #140. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_140"]["instring"] outstring = self.TEST_STRINGS["issue_140"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) instring = self.TEST_STRINGS["issue_140_2"]["instring"] outstring = self.TEST_STRINGS["issue_140_2"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) instring = self.TEST_STRINGS["issue_140_3"]["instring"] outstring = self.TEST_STRINGS["issue_140_3"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [["--wrap-descriptions", "72", ""]], ) def test_format_docstring_link_after_colon( self, test_args, args, ): """In-line links shouldn't be put on next line when following a colon. See issue #150. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_150"]["instring"] outstring = self.TEST_STRINGS["issue_150"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [ [ "--wrap-descriptions", "88", "", ] ], ) def test_format_docstring_keep_inline_link_together( self, test_args, args, ): """Keep in-line links together with the display text. See issue #157. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_157"]["instring"] outstring = self.TEST_STRINGS["issue_157"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [ [ "--wrap-descriptions", "88", "", ] ], ) def test_format_docstring_keep_inline_link_together_two_paragraphs( self, test_args, args, ): """Keep in-line links together with the display text. If there is another paragraph following the in-line link, don't strip the newline in between. See issue #157. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_157_2"]["instring"] outstring = self.TEST_STRINGS["issue_157_2"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) instring = self.TEST_STRINGS["issue_157_3"]["instring"] outstring = self.TEST_STRINGS["issue_157_3"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) instring = self.TEST_STRINGS["issue_157_4"]["instring"] outstring = self.TEST_STRINGS["issue_157_4"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) instring = self.TEST_STRINGS["issue_157_5"]["instring"] outstring = self.TEST_STRINGS["issue_157_5"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) instring = self.TEST_STRINGS["issue_157_6"]["instring"] outstring = self.TEST_STRINGS["issue_157_6"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) instring = self.TEST_STRINGS["issue_157_7"]["instring"] outstring = self.TEST_STRINGS["issue_157_7"]["outstring"] assert outstring == uut._do_format_code( instring, ) instring = self.TEST_STRINGS["issue_157_8"]["instring"] outstring = self.TEST_STRINGS["issue_157_8"]["outstring"] assert outstring == uut._do_format_code( instring, ) instring = self.TEST_STRINGS["issue_157_9"]["instring"] outstring = self.TEST_STRINGS["issue_157_9"]["outstring"] assert outstring == uut._do_format_code( instring, ) instring = self.TEST_STRINGS["issue_157_10"]["instring"] outstring = self.TEST_STRINGS["issue_157_10"]["outstring"] assert outstring == uut._do_format_code( instring, ) instring = self.TEST_STRINGS["issue_157_11"]["instring"] outstring = self.TEST_STRINGS["issue_157_11"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [ [ "--wrap-descriptions", "88", "--wrap-summaries", "88", "", ] ], ) def test_format_docstring_link_no_delete_words( self, test_args, args, ): """Should not delete words when wrapping a URL. See issue #159. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_159"]["instring"] outstring = self.TEST_STRINGS["issue_159"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [ [ "--wrap-descriptions", "88", "--wrap-summaries", "88", "", ] ], ) def test_format_docstring_link_no_newline_after_link( self, test_args, args, ): """Links should have no newline before or after them. See issue #180. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_180"]["instring"] outstring = self.TEST_STRINGS["issue_180"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [ [ "--wrap-descriptions", "72", "", ] ], ) def test_format_docstring_with_only_link_in_description( self, test_args, args, ): """No index error when only link in long description. See issue #189. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_189"]["instring"] outstring = self.TEST_STRINGS["issue_189"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_docstring_no_indent_string_on_newline( self, test_args, args, ): """Should not add the indentation string to a newline. See issue #199. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_199"]["instring"] outstring = self.TEST_STRINGS["issue_199"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_docstring_with_short_anonymous_link( self, test_args, args, ): """Anonymous link references should not be wrapped into the link. See issue #210. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_210"]["instring"] outstring = self.TEST_STRINGS["issue_210"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_docstring_with_quoted_link( self, test_args, args, ): """Anonymous link references should not be wrapped into the link. See issue #218. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_218"]["instring"] outstring = self.TEST_STRINGS["issue_218"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) docformatter-1.7.5/tests/formatter/test_format_wrap.py000066400000000000000000000264301445340205700233250ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.test_format_wrap.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. """Module for testing the Formatter class with the --wrap options.""" # 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 import docformatter from docformatter import Formatter # docformatter Local Imports from .. import generate_random_docstring INDENTATION = " " class TestFormatWrap: """Class for testing _do_format_docstring() with line wrapping.""" with open("tests/_data/string_files/format_wrap.toml", "rb") as f: TEST_STRINGS = tomllib.load(f) @pytest.mark.unit def test_unwrap_summary(self): """Remove newline and tab characters.""" instring = self.TEST_STRINGS["unwrap"]["instring"] outstring = self.TEST_STRINGS["unwrap"]["outstring"] assert outstring == docformatter.unwrap_summary( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_docstring_with_wrap( self, test_args, args, ): """Wrap the docstring.""" 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 @pytest.mark.unit @pytest.mark.parametrize("args", [["--wrap-summaries", "79", ""]]) def test_format_docstring_with_weird_indentation_and_punctuation( self, test_args, args, ): """Wrap and dedent docstring.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["weird_punctuation"]["instring"] outstring = self.TEST_STRINGS["weird_punctuation"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [["--wrap-descriptions", "72", ""]]) def test_format_docstring_with_description_wrapping( self, test_args, args, ): """Wrap description at 72 characters.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["description_wrap"]["instring"] outstring = self.TEST_STRINGS["description_wrap"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [["--wrap-descriptions", "72", ""]]) def test_format_docstring_should_ignore_doctests( self, test_args, args, ): """Leave doctests alone.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["ignore_doctest"]["instring"] outstring = self.TEST_STRINGS["ignore_doctest"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [["--wrap-descriptions", "72", ""]]) def test_format_docstring_should_ignore_doctests_in_summary( self, test_args, args, ): """Leave doctests alone if they're in the summary.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["ignore_summary_doctest"]["instring"] outstring = self.TEST_STRINGS["ignore_summary_doctest"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [["--wrap-descriptions", "72", ""]]) def test_format_docstring_should_maintain_indentation_of_doctest( self, test_args, args, ): """Don't change indentation of doctest lines.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["same_indentation_doctest"]["instring"] outstring = self.TEST_STRINGS["same_indentation_doctest"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [ [ "--wrap-descriptions", "72", "--wrap-summaries", "50", "--force-wrap", "", ] ], ) def test_force_wrap( self, test_args, args, ): """Force even lists to be wrapped.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["force_wrap"]["instring"] outstring = self.TEST_STRINGS["force_wrap"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [["--wrap-summaries", "30", "--tab-width", "4", ""]], ) def test_format_docstring_with_summary_only_and_wrap_and_tab_indentation( self, test_args, args, ): """Should account for length of tab when wrapping. See PR #69. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["summary_wrap_tab"]["instring"] outstring = self.TEST_STRINGS["summary_wrap_tab"]["outstring"] assert outstring == uut._do_format_docstring( "\t\t", instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [["--wrap-summaries", "69", "--close-quotes-on-newline", ""]], ) def test_format_docstring_for_multi_line_summary_alone( self, test_args, args, ): """Place closing quotes on newline when wrapping one-liner.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["one_line_wrap_newline"]["instring"] outstring = self.TEST_STRINGS["one_line_wrap_newline"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [["--wrap-summaries", "88", "--close-quotes-on-newline", ""]], ) def test_format_docstring_for_one_line_summary_alone_but_too_long( self, test_args, args, ): """""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["one_line_no_wrap"]["instring"] outstring = self.TEST_STRINGS["one_line_no_wrap"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_docstring_with_class_attributes(self, test_args, args): """Wrap long class attribute docstrings.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["class_attribute_wrap"]["instring"] outstring = self.TEST_STRINGS["class_attribute_wrap"]["outstring"] assert outstring == uut._do_format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_docstring_no_newline_in_summary_with_symbol(self, test_args, args): """Wrap summary with symbol should not add newline. See issue #79. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_79"]["instring"] outstring = self.TEST_STRINGS["issue_79"]["outstring"] assert outstring == uut._do_format_code( instring, ) @pytest.mark.unit @pytest.mark.parametrize( "args", [["--wrap-descriptions", "120", "--wrap-summaries", "120", ""]] ) def test_format_docstring_with_multi_paragraph_description( self, test_args, args, ): """Wrap each paragraph in the long description separately. See issue #127. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) instring = self.TEST_STRINGS["issue_127"]["instring"] outstring = self.TEST_STRINGS["issue_127"]["outstring"] assert outstring == uut._do_format_docstring( INDENTATION, instring, ) docformatter-1.7.5/tests/test_configuration_functions.py000066400000000000000000000364711445340205700237460ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.test_configuration_functions.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. """Module for testing docformatter's Configurater class.""" # Standard Library Imports import io import sys # Third Party Imports import pytest # docformatter Package Imports from docformatter import Configurater class TestConfigurater: """Class for testing configuration functions.""" @pytest.mark.unit 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.unit 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_dct == { "recursive": "True", "wrap-summaries": "82", } @pytest.mark.unit 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_dct == { "blank": "False", "wrap-summaries": "79", "wrap-descriptions": "72", } @pytest.mark.unit 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_dct == { "blank": "False", "wrap-summaries": "79", "wrap-descriptions": "72", } @pytest.mark.unit 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_dct == {} @pytest.mark.unit 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_dct == { "blank": "False", "wrap-descriptions": "72", "wrap-summaries": "79", } 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 == 88 assert uut.args.wrap_descriptions == 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.unit 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.unit 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.unit 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.unit 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.unit 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.unit 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.unit @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 == 88 assert uut.args.wrap_descriptions == 88 @pytest.mark.unit @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 == 80 assert uut.args.wrap_descriptions == 88 assert uut.flargs_dct == { "black": "True", "wrap-summaries": "80", } @pytest.mark.unit @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 == 88 assert uut.args.wrap_descriptions == 80 assert uut.flargs_dct == { "black": "True", "wrap-descriptions": "80", } @pytest.mark.unit @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_dct == { "recursive": "True", "check": "True", "diff": "True", "exclude": ["src/", "tests/"], } @pytest.mark.unit @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_dct == { "recursive": "true", "check": "true", "diff": "true", "exclude": '["src/", "tests/"]', } @pytest.mark.unit 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.unit @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_dct == { "recursive": "True", "check": "True", "diff": "True", "non-cap": ["qBittorrent", "iPad", "iOS", "eBay"] } @pytest.mark.unit @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_dct == { "recursive": "true", "check": "true", "diff": "true", "non-cap": '["qBittorrent", "iPad", "iOS", "eBay"]' } docformatter-1.7.5/tests/test_docformatter.py000066400000000000000000001047641445340205700215010ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.test_docformatter.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. """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, ) assert "no such file" in output_file.getvalue().lower() @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", [ ''' """ 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.check_failed 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", [ '''\ """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,10 +1,9 @@ """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( '''\ """ Hello world""" '''.encode() )[0] .decode() .replace("\r", "") ) assert 0 == run_docformatter.returncode assert '''"""Hello world."""\n''' == 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,3 +1,2 @@ class TestFoo(): """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.5/tests/test_encoding_functions.py000066400000000000000000000132461445340205700226600ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.test_encoding_functions.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. """Module for testing functions used determine file encodings. Encoding functions are: - detect_encoding() - find_newline() - open_with_encoding() """ # Standard Library Imports import io import sys # Third Party Imports import pytest # docformatter Package Imports from docformatter import Encoder SYSTEM_ENCODING = sys.getdefaultencoding() @pytest.mark.usefixtures("temporary_file") class TestDetectEncoding: """Class for testing the detect_encoding() function.""" @pytest.mark.unit @pytest.mark.parametrize("contents", ["# -*- coding: utf-8 -*-\n"]) def test_detect_encoding_with_explicit_utf_8( self, temporary_file, contents ): """Return utf-8 when explicitly set in file.""" uut = Encoder() uut.do_detect_encoding(temporary_file) assert "utf_8" == uut.encoding @pytest.mark.unit @pytest.mark.parametrize( "contents", ["# Wow! docformatter is super-cool.\n"] ) def test_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.unit @pytest.mark.parametrize("contents", ["# -*- coding: blah -*-"]) def test_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 class TestFindNewline: """Class for testing the find_newline() function.""" @pytest.mark.unit def test_find_newline_only_cr(self): """Return carriage return as newline type.""" uut = Encoder() source = ["print 1\r", "print 2\r", "print3\r"] assert uut.CR == uut.do_find_newline(source) @pytest.mark.unit def test_find_newline_only_lf(self): """Return line feed as newline type.""" uut = Encoder() source = ["print 1\n", "print 2\n", "print3\n"] assert uut.LF == uut.do_find_newline(source) @pytest.mark.unit def test_find_newline_only_crlf(self): """Return carriage return, line feed as newline type.""" uut = Encoder() source = ["print 1\r\n", "print 2\r\n", "print3\r\n"] assert uut.CRLF == uut.do_find_newline(source) @pytest.mark.unit def test_find_newline_cr1_and_lf2(self): """Favor line feed over carriage return when both are found.""" uut = Encoder() source = ["print 1\n", "print 2\r", "print3\n"] assert uut.LF == uut.do_find_newline(source) @pytest.mark.unit def test_find_newline_cr1_and_crlf2(self): """Favor carriage return, line feed when mix of newline types.""" uut = Encoder() source = ["print 1\r\n", "print 2\r", "print3\r\n"] assert uut.CRLF == uut.do_find_newline(source) @pytest.mark.unit def test_find_newline_should_default_to_lf(self): """Default to line feed when no newline type found.""" uut = Encoder() assert uut.LF == uut.do_find_newline([]) assert uut.LF == uut.do_find_newline(["", ""]) @pytest.mark.unit def test_find_dominant_newline(self): """Should detect carriage return as the dominant line endings.""" uut = Encoder() goes_in = '''\ def foo():\r """\r Hello\r foo. This is a docstring.\r """\r ''' assert uut.CRLF == uut.do_find_newline(goes_in.splitlines(True)) @pytest.mark.usefixtures("temporary_file") class TestOpenWithEncoding: """Class for testing the open_with_encoding() function.""" @pytest.mark.unit @pytest.mark.parametrize("contents", ["# -*- coding: utf-8 -*-\n"]) def test_open_with_utf_8_encoding(self, temporary_file, contents): """Return TextIOWrapper object when opening file with encoding.""" uut = Encoder() uut.do_detect_encoding(temporary_file) assert isinstance( uut.do_open_with_encoding(temporary_file), io.TextIOWrapper, ) @pytest.mark.unit @pytest.mark.parametrize("contents", ["# -*- coding: utf-8 -*-\n"]) def test_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) docformatter-1.7.5/tests/test_string_functions.py000066400000000000000000000345021445340205700223760ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.test_string_functions.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. """Module for testing functions that manipulate text. This module contains tests for string functions. String functions are those: reindent() find_shortest_indentation() normalize_line() normalize_line_endings() normalize_summary() remove_section_headers() split_first_sentence() split_summary_and_description() strip_leading_blank_lines() strip_quotes() strip_newlines() """ # Third Party Imports import pytest # docformatter Package Imports import docformatter class TestIndenters: """Class for testing the indentation related function. Includes tests for: - reindent() - find_shortest_indentation() """ @pytest.mark.unit def test_reindent(self): """Should add four spaces to the beginning of each docstring line.""" assert """\ This should be dedented. 1. This too. 2. And this. """ == docformatter.reindent( """\ This should be dedented. 1. This too. 2. And this. """, indentation=" ", ) @pytest.mark.unit def test_reindent_should_expand_tabs_to_indentation(self): """Should convert tabs to indentation type (four spaces).""" assert """\ This should be dedented. 1. This too. 2. And this. """ == docformatter.reindent( """\ This should be dedented. 1. This too. \t2. And this. """, indentation=" ", ) @pytest.mark.unit def test_reindent_with_no_indentation_expand_tabs(self): """Should convert tabs to indentation type (four spaces).""" assert """\ The below should be indented with spaces: 1. This too. 2. And this. """ == docformatter.reindent( """\ The below should be indented with spaces: \t1. This too. \t2. And this. """, indentation="", ) @pytest.mark.unit def test_reindent_should_maintain_indentation(self): """Should make no changes with existing indentation same as type.""" description = """\ Parameters: - a - b """ assert description == docformatter.reindent( description, indentation=" ", ) @pytest.mark.unit def test_reindent_tab_indentation(self): """Should maintain tabs for the indentation.""" assert """\ \tThis should be indented with a tab. \tSo should this. """ == docformatter.reindent( """\ \tThis should be indented with a tab. \tSo should this. """, indentation="\t", ) @pytest.mark.unit def testfind_shortest_indentation(self): """Should find the shortest indentation to be one space.""" assert " " == docformatter.find_shortest_indentation( [" ", " b", " a"], ) class TestNormalizers: """Class for testing the string normalizing functions. Includes tests for: - normalize_line() - normalize_line_endings() - normalize_summary() """ @pytest.mark.unit def test_normalize_summary(self): """Add period and strip spaces to line.""" assert "This is a sentence." == docformatter.normalize_summary( "This is a sentence " ) @pytest.mark.unit def test_normalize_summary_multiline(self): """Add period to line even with line return character.""" assert "This \n\t is\na sentence." == docformatter.normalize_summary( "This \n\t is\na sentence " ) @pytest.mark.unit def test_normalize_summary_with_different_punctuation(self): """Do not add period for line ending in question mark.""" summary = "This is a question?" assert summary == docformatter.normalize_summary(summary) @pytest.mark.unit def test_normalize_summary_formatted_as_title(self): """Do not add period for markup title (line begins with #). See issue #56. """ summary = "# This is a title" assert summary == docformatter.normalize_summary(summary) @pytest.mark.unit def test_normalize_summary_capitalize_first_letter(self): """Capitalize the first letter of the summary. See issue #76. See requirement docformatter_4.5.1. """ assert ( "This is a summary that needs to be capped." == docformatter.normalize_summary( "this is a summary that needs to be capped" ) ) assert "Don't lower case I'm." == docformatter.normalize_summary( "don't lower case I'm" ) @pytest.mark.unit def test_normalize_summary_capitalize_first_letter_with_period(self): """Capitalize the first letter of the summary even when ends in period. See issue #184. See requirement docformatter_4.5.1. """ assert ( "This is a summary that needs to be capped." == docformatter.normalize_summary( "this is a summary that needs to be capped." ) ) @pytest.mark.unit def test_normalize_summary_dont_capitalize_first_letter_if_variable(self): """Capitalize the first word unless it looks like a variable.""" assert ( "num_iterations should not be capitalized in this summary." == docformatter.normalize_summary( "num_iterations should not be capitalized in this summary" ) ) class TestSplitters: """Class for testing the string splitting function. Includes tests for: - split_first_sentence() - split_summary_and_description() """ @pytest.mark.unit def test_split_first_sentence(self): """""" assert ( "This is a sentence.", " More stuff. And more stuff. .!@#$%", ) == docformatter.split_first_sentence( "This is a sentence. More stuff. And more stuff. .!@#$%" ) assert ( "This e.g. sentence.", " More stuff. And more stuff. .!@#$%", ) == docformatter.split_first_sentence( "This e.g. sentence. More stuff. And more stuff. .!@#$%" ) assert ( "This is the first:", "\none\ntwo", ) == docformatter.split_first_sentence("This is the first:\none\ntwo") @pytest.mark.unit def test_split_summary_and_description(self): """""" assert ( "This is the first.", "This is the second. This is the third.", ) == docformatter.split_summary_and_description( "This is the first. This is the second. This is the third." ) @pytest.mark.unit def test_split_summary_and_description_complex(self): """""" assert ( "This is the first", "\nThis is the second. This is the third.", ) == docformatter.split_summary_and_description( "This is the first\n\nThis is the second. This is the third." ) @pytest.mark.unit def test_split_summary_and_description_more_complex(self): """""" assert ( "This is the first.", "This is the second. This is the third.", ) == docformatter.split_summary_and_description( "This is the first.\nThis is the second. This is the third." ) @pytest.mark.unit def test_split_summary_and_description_with_list(self): """""" assert ( "This is the first", "- one\n- two", ) == docformatter.split_summary_and_description( "This is the first\n- one\n- two" ) @pytest.mark.unit def test_split_summary_and_description_with_list_of_parameters(self): """""" assert ( "This is the first", "one - one\ntwo - two", ) == docformatter.split_summary_and_description( "This is the first\none - one\ntwo - two" ) @pytest.mark.unit def test_split_summary_and_description_with_capital(self): """""" assert ( "This is the first\nWashington", "", ) == docformatter.split_summary_and_description( "This is the first\nWashington" ) @pytest.mark.unit def test_split_summary_and_description_with_list_on_other_line(self): """""" assert ( "Test", " test\n @blah", ) == docformatter.split_summary_and_description( """\ Test test @blah """ ) @pytest.mark.unit def test_split_summary_and_description_with_other_symbol(self): """""" assert ( "This is the first", "@ one\n@ two", ) == docformatter.split_summary_and_description( "This is the first\n@ one\n@ two" ) @pytest.mark.unit def test_split_summary_and_description_with_colon(self): """""" assert ( "This is the first:", "one\ntwo", ) == docformatter.split_summary_and_description( "This is the first:\none\ntwo" ) @pytest.mark.unit def test_split_summary_and_description_with_exclamation(self): """""" assert ( "This is the first!", "one\ntwo", ) == docformatter.split_summary_and_description( "This is the first!\none\ntwo" ) @pytest.mark.unit def test_split_summary_and_description_with_question_mark(self): """""" assert ( "This is the first?", "one\ntwo", ) == docformatter.split_summary_and_description( "This is the first?\none\ntwo" ) @pytest.mark.unit def test_split_summary_and_description_with_quote(self): """""" assert ( 'This is the first\n"one".', "", ) == docformatter.split_summary_and_description( 'This is the first\n"one".' ) assert ( "This is the first\n'one'.", "", ) == docformatter.split_summary_and_description( "This is the first\n'one'." ) assert ( "This is the first\n``one``.", "", ) == docformatter.split_summary_and_description( "This is the first\n``one``." ) @pytest.mark.unit def test_split_summary_and_description_with_punctuation(self): """""" assert ( ( """\ Try this and this and this and this and this and this and this at https://example.com/""", """ Parameters ---------- email : string""", ) == docformatter.split_summary_and_description( """\ Try this and this and this and this and this and this and this at https://example.com/ Parameters ---------- email : string """ ) ) @pytest.mark.unit def test_split_summary_and_description_without_punctuation(self): """""" assert ( ( """\ Try this and this and this and this and this and this and this at this other line""", """ Parameters ---------- email : string""", ) == docformatter.split_summary_and_description( """\ Try this and this and this and this and this and this and this at this other line Parameters ---------- email : string """ ) ) @pytest.mark.unit def test_split_summary_and_description_with_abbreviation(self): """""" for text in [ "Test e.g. now" "Test i.e. now", "Test Dr. now", "Test Mr. now", "Test Mrs. now", "Test Ms. now", ]: assert (text, "") == docformatter.split_summary_and_description( text ) @pytest.mark.unit def test_split_summary_and_description_with_url(self): """Retain URL on second line with summary.""" text = '''\ """Sequence of package managers as defined by `XKCD #1654: Universal Install Script `_. See the corresponding :issue:`implementation rationale in issue #10 <10>`. """\ ''' assert ( '"""Sequence of package managers as defined by `XKCD #1654: Universal Install Script\n' "`_.", "\nSee the corresponding :issue:`implementation rationale in issue #10 <10>`." '\n"""', ) == docformatter.split_summary_and_description(text) class TestStrippers: """Class for testing the string stripping functions. Includes tests for: - strip_leading_blank_lines() - strip_quotes() - strip_newlines() - strip_docstring() """ @pytest.mark.unit def test_remove_section_header(self): """Remove section header directives.""" assert "foo\nbar\n" == docformatter.remove_section_header( "----\nfoo\nbar\n" ) line = "foo\nbar\n" assert line == docformatter.remove_section_header(line) line = " \nfoo\nbar\n" assert line == docformatter.remove_section_header(line) docformatter-1.7.5/tests/test_strip_docstring.py000066400000000000000000000136011445340205700222120ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.test_strip_docstring.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. """Module for testing the _do_strip_docstring() method.""" # Standard Library Imports import sys # Third Party Imports import pytest # docformatter Package Imports from docformatter import Formatter INDENTATION = " " class TestStripDocstring: """Class for testing _do_strip_docstring().""" @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_strip_docstring( self, test_args, args, ): """Strip triple double quotes from docstring.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) docstring, open_quote = uut._do_strip_docstring( ''' """Hello. """ ''' ) assert docstring == "Hello." assert open_quote == '"""' @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_strip_docstring_with_triple_single_quotes( self, test_args, args, ): """Strip triple single quotes from docstring.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) docstring, open_quote = uut._do_strip_docstring( """ '''Hello. ''' """ ) assert docstring == "Hello." assert open_quote == '"""' @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_strip_docstring_with_empty_string( self, test_args, args, ): """Return series of six double quotes when passed empty string.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) docstring, open_quote = uut._do_strip_docstring('""""""') assert not docstring assert open_quote == '"""' @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_strip_docstring_with_raw_string( self, test_args, args, ): """Return docstring and raw open quote.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) docstring, open_quote = uut._do_strip_docstring('r"""foo"""') assert docstring == "foo" assert open_quote == 'r"""' docstring, open_quote = uut._do_strip_docstring("R'''foo'''") assert docstring == "foo" assert open_quote == 'R"""' @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_strip_docstring_with_unicode_string( self, test_args, args, ): """Return docstring and unicode open quote.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) docstring, open_quote = uut._do_strip_docstring("u'''foo'''") assert docstring == "foo" assert open_quote == 'u"""' docstring, open_quote = uut._do_strip_docstring('U"""foo"""') assert docstring == "foo" assert open_quote == 'U"""' @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_strip_docstring_with_unknown( self, test_args, args, ): """Raise ValueError with single quotes.""" uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) with pytest.raises(ValueError): uut._do_strip_docstring("foo") @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_strip_docstring_with_single_quotes( self, test_args, args, ): """Raise ValueError when strings begin with single single quotes. See requirement PEP_257_1. See issue #66 for example of docformatter breaking code when encountering single quote. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) with pytest.raises(ValueError): uut._do_strip_docstring("'hello\\''") @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_strip_docstring_with_double_quotes( self, test_args, args, ): """Raise ValueError when strings begin with single double quotes. See requirement PEP_257_1. See issue #66 for example of docformatter breaking code when encountering single quote. """ uut = Formatter( test_args, sys.stderr, sys.stdin, sys.stdout, ) with pytest.raises(ValueError): uut._do_strip_docstring('"hello\\""') docformatter-1.7.5/tests/test_syntax_functions.py000066400000000000000000000146441445340205700224230ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.test_syntax_functions.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. """Module for testing functions that deal with syntax. This module contains tests for syntax functions. Syntax functions are those: do_clean_link() do_find_directives() do_find_links() do_skip_link() """ # Third Party Imports import pytest # docformatter Package Imports import docformatter class TestURLHandlers: """Class for testing the URL handling functions. Includes tests for: - do_clean_link() - do_find_links() - do_skip_link() """ @pytest.mark.unit def test_find_in_line_link(self): """Should find link pattern in a text block.""" assert [(53, 162)] == docformatter.do_find_links( "The text file can be retrieved via the Chrome plugin `Get \ Cookies.txt ` while browsing." ) assert [(95, 106), (110, 123)] == docformatter.do_find_links( "``pattern`` is considered as an URL only if it is parseable as such\ and starts with ``http://`` or ``https://``." ) @pytest.mark.unit def test_skip_link_with_manual_wrap(self): """Should skip a link that has been manually wrapped by the user.""" assert docformatter.do_skip_link( "``pattern`` is considered as an URL only if it is parseable as such\ and starts with ``http://`` or ``https://``.", (95, 106), ) assert docformatter.do_skip_link( "``pattern`` is considered as an URL only if it is parseable as such\ and starts with ``http://`` or ``https://``.", (110, 123), ) @pytest.mark.unit def test_do_clean_link(self): """Should remove line breaks from links.""" assert ( " `Get Cookies.txt `" ) == docformatter.do_clean_url( "`Get \ Cookies.txt `", " ", ) assert ( " `custom types provided by Click `_." ) == docformatter.do_clean_url( "`custom types provided by Click\ `_.", " ", ) class TestreSTHandlers: """Class for testing the reST directive handling functions. Includes tests for: - do_find_directives() """ @pytest.mark.unit def test_find_in_line_directives(self): """Should find reST directieves in a text block.""" assert docformatter.do_find_directives( "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." ) @pytest.mark.unit def test_find_double_dot_directives(self): """Should find reST directives preceeded by ..""" assert docformatter.do_find_directives( ".. _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" ) assert docformatter.do_find_directives( "``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" "`_," "hich is only `available in the standard library starting with Python v3.9" "`." ) @pytest.mark.unit def test_find_double_backtick_directives(self): """Should find reST directives preceeded by ``.""" assert docformatter.do_find_directives( "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." ) docformatter-1.7.5/tests/test_utility_functions.py000066400000000000000000000603251445340205700225750ustar00rootroot00000000000000# pylint: skip-file # type: ignore # # tests.test_utility_functions.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. """Module for testing utility functions used when processing docstrings. This module contains tests for utility functions. Utility functions are: - find_py_files() - has_correct_length() - is_in_range() - is_probably_beginning_of_sentence() - is_some_sort_of_list() - is_some_sort_of_code() """ # Third Party Imports import pytest from mock import patch # docformatter Package Imports import docformatter REST_SECTION_REGEX = r"[=\-`:'\"~^_*+#<>]{4,}" class TestFindPyFiles: """Class for testing the find_py_files() function.""" @pytest.mark.unit def test_is_hidden(self): """Skip files that are .hidden.""" assert docformatter.find_py_files("not_hidden", ".hidden_file.py") @pytest.mark.xfail( reason="function only checks for python files in recursive mode." ) def test_non_recursive_ignore_non_py_files(self): """Only process python (*.py) files.""" sources = ["one.py", "two.py", "three.toml"] test_only_py = list(docformatter.find_py_files(sources, False)) assert test_only_py == ["one.py", "two.py"] @pytest.mark.unit def test_recursive_ignore_non_py_files(self): """Only process python (*.py) files when recursing directories.""" sources = {"/root"} patch_data = [ ("/root", [], ["one.py", "two.py", "three.toml"]), ] with patch("os.walk", return_value=patch_data), patch( "os.path.isdir", return_value=True ): test_only_py = list(docformatter.find_py_files(sources, True)) assert test_only_py == ["/root/one.py", "/root/two.py"] @pytest.mark.unit def test_is_excluded(self): """Skip excluded *.py files.""" sources = {"/root"} patch_data = [ ("/root", ["folder_one", "folder_two"], []), ("/root/folder_one", ["folder_three"], ["one.py"]), ("/root/folder_one/folder_three", [], ["three.py"]), ("/root/folder_two", [], ["two.py"]), ] with patch("os.walk", return_value=patch_data), patch( "os.path.isdir", return_value=True ): test_exclude_one = list( docformatter.find_py_files(sources, True, ["folder_one"]) ) assert test_exclude_one == ["/root/folder_two/two.py"] test_exclude_two = list( docformatter.find_py_files(sources, True, ["folder_two"]) ) assert test_exclude_two == [ "/root/folder_one/one.py", "/root/folder_one/folder_three/three.py", ] test_exclude_three = list( docformatter.find_py_files(sources, True, ["folder_three"]) ) assert test_exclude_three == [ "/root/folder_one/one.py", "/root/folder_two/two.py", ] test_exclude_py = list(docformatter.find_py_files(sources, True, ".py")) assert not test_exclude_py test_exclude_two_and_three = list( docformatter.find_py_files( sources, True, ["folder_two", "folder_three"] ) ) assert test_exclude_two_and_three == ["/root/folder_one/one.py"] test_exclude_files = list( docformatter.find_py_files(sources, True, ["one.py", "two.py"]) ) assert test_exclude_files == ["/root/folder_one/folder_three/three.py"] @pytest.mark.unit def test_nothing_is_excluded(self): """Include all *.py files found.""" sources = {"/root"} patch_data = [ ("/root", ["folder_one", "folder_two"], []), ("/root/folder_one", ["folder_three"], ["one.py"]), ("/root/folder_one/folder_three", [], ["three.py"]), ("/root/folder_two", [], ["two.py"]), ] with patch("os.walk", return_value=patch_data), patch( "os.path.isdir", return_value=True ): test_exclude_nothing = list(docformatter.find_py_files(sources, True, [])) assert test_exclude_nothing == [ "/root/folder_one/one.py", "/root/folder_one/folder_three/three.py", "/root/folder_two/two.py", ] test_exclude_nothing = list(docformatter.find_py_files(sources, True)) assert test_exclude_nothing == [ "/root/folder_one/one.py", "/root/folder_one/folder_three/three.py", "/root/folder_two/two.py", ] class TestHasCorrectLength: """Class for testing the has_correct_length() function.""" @pytest.mark.unit def test_has_correct_length_none(self): """Return True when passed line_length=None.""" assert docformatter.has_correct_length(None, 1, 9) @pytest.mark.unit def test_has_correct_length(self): """Return True if the line is within the line_length.""" assert docformatter.has_correct_length([1, 3], 3, 5) assert docformatter.has_correct_length([1, 1], 1, 1) assert docformatter.has_correct_length([1, 10], 5, 10) @pytest.mark.unit def test_not_correct_length(self): """Return False if the line is outside the line_length.""" assert not docformatter.has_correct_length([1, 1], 2, 9) assert not docformatter.has_correct_length([10, 20], 2, 9) class TestIsInRange: """Class for testing the is_in_range() function.""" @pytest.mark.unit def test_is_in_range_none(self): """Return True when passed line_range=None.""" assert docformatter.is_in_range(None, 1, 9) @pytest.mark.unit def test_is_in_range(self): """Return True if the line is within the line_range.""" assert docformatter.is_in_range([1, 4], 3, 5) assert docformatter.is_in_range([1, 4], 4, 10) assert docformatter.is_in_range([2, 10], 1, 2) @pytest.mark.unit def test_not_in_range(self): """Return False if the line outside the line_range.""" assert not docformatter.is_in_range([1, 1], 2, 9) assert not docformatter.is_in_range([10, 20], 1, 9) class TestIsProbablySentence: """Class for testing the is_probably_beginning_of_senstence() function.""" @pytest.mark.unit def test_is_probably_beginning_of_sentence(self): """Ignore special characters as sentence starters.""" assert docformatter.is_probably_beginning_of_sentence( "- This is part of a list." ) assert not docformatter.is_probably_beginning_of_sentence( "(this just continues an existing sentence)." ) @pytest.mark.unit def test_is_probably_beginning_of_sentence_pydoc_ref(self): """Ignore colon as sentence starter.""" assert not docformatter.is_probably_beginning_of_sentence( ":see:MyClass This is not the start of a sentence." ) class TestDoFindLinks: """Class for testing the do_find_links() function.""" @pytest.mark.unit def test_do_find_file_system_link(self): """Identify afp://, nfs://, smb:// as a link.""" text = "This is an Apple Filing Protocol URL pattern: afp://[[:][/[]]" assert docformatter.do_find_links(text) == [(46, 86)] text = "This is an Network File System URL pattern: nfs://server<:port>/" assert docformatter.do_find_links(text) == [(44, 70)] text = "This is an Samba URL pattern: smb://[@][:][/[]][?=[;=]]" assert docformatter.do_find_links(text) == [(30, 111)] @pytest.mark.unit def test_do_find_miscellaneous_link(self): """Identify apt:, bitcoin:, chrome://, and jar: as a link.""" text = "This is an apt URL pattern: apt:docformatter" assert docformatter.do_find_links(text) == [(28, 44)] text = "This is a bitcoin URL pattern: bitcoin:
[?[amount=][&][label=