├── .flake8 ├── .gitattributes ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── checks.yaml │ ├── primer.yaml │ ├── primer_comment.yaml │ ├── publish.yaml │ └── tests.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .pre-commit-hooks.yaml ├── .readthedocs.yaml ├── LICENSE ├── README.md ├── codecov.yml ├── docs ├── Makefile ├── _ext │ └── usage_page.py ├── conf.py ├── development.rst ├── index.rst ├── make.bat ├── requirements-doc.txt └── usage.rst ├── pydocstringformatter ├── __init__.py ├── __main__.py ├── _configuration │ ├── __init__.py │ ├── arguments_manager.py │ ├── boolean_option_action.py │ ├── command_line_parsing.py │ ├── formatter_options.py │ ├── toml_parsing.py │ └── validators.py ├── _formatting │ ├── __init__.py │ ├── _utils.py │ ├── base.py │ ├── formatters_default.py │ ├── formatters_numpydoc.py │ └── formatters_pep257.py ├── _testutils │ ├── __init__.py │ ├── example_formatters.py │ └── primer │ │ ├── __init__.py │ │ ├── const.py │ │ ├── packages.py │ │ └── primer.py ├── _utils │ ├── __init__.py │ ├── exceptions.py │ ├── file_diference.py │ ├── find_docstrings.py │ ├── find_python_file.py │ ├── issue_template.py │ └── output.py └── run.py ├── pyproject.toml ├── requirements-coverage.txt ├── requirements.txt └── tests ├── conftest.py ├── data ├── config │ ├── exclude_match │ │ ├── pyproject.toml │ │ └── test_package │ │ │ └── incorrect_docstring.py │ ├── exclude_match_csv │ │ ├── pyproject.toml │ │ └── test_package │ │ │ └── incorrect_docstring.py │ ├── exclude_match_csv_list │ │ ├── pyproject.toml │ │ └── test_package │ │ │ └── incorrect_docstring.py │ ├── exclude_match_inner │ │ ├── pyproject.toml │ │ └── test_package │ │ │ └── inner_dir │ │ │ └── incorrect_docstring.py │ ├── exclude_non_match │ │ ├── pyproject.toml │ │ └── test_package │ │ │ └── incorrect_docstring.py │ ├── invalid_toml │ │ ├── pyproject.toml │ │ └── test_package │ │ │ └── correct_docstring.py │ ├── no_toml │ │ └── test_package │ │ │ └── incorrect_docstring.py │ ├── non_existing_options │ │ ├── pyproject.toml │ │ └── test_package │ │ │ └── correct_docstring.py │ ├── non_valid_toml_boolopt │ │ ├── pyproject.toml │ │ └── test_package │ │ │ └── numpydoc_style.py │ ├── non_valid_toml_boolopt_two │ │ ├── pyproject.toml │ │ └── test_package │ │ │ └── numpydoc_style.py │ ├── valid_toml │ │ ├── pyproject.toml │ │ └── test_package │ │ │ └── correct_docstring.py │ ├── valid_toml_boolopt │ │ ├── pyproject.toml │ │ └── test_package │ │ │ └── numpydoc_style.py │ ├── valid_toml_numpydoc │ │ ├── pyproject.toml │ │ └── test_package │ │ │ └── numpydoc_style.py │ ├── valid_toml_numpydoc_pep257 │ │ ├── pyproject.toml │ │ └── test_package │ │ │ └── numpydoc_style.py │ ├── valid_toml_pep257 │ │ ├── pyproject.toml │ │ └── test_package │ │ │ └── numpydoc_style.py │ ├── valid_toml_two │ │ ├── pyproject.toml │ │ └── test_package │ │ │ └── incorrect_docstring.py │ └── valid_toml_without_section │ │ ├── pyproject.toml │ │ └── test_package │ │ └── correct_docstring.py ├── format │ ├── beginning_quote │ │ ├── class_docstring.py │ │ ├── class_docstring.py.out │ │ ├── function_docstrings.py │ │ ├── function_docstrings.py.out │ │ ├── module_docstring.py │ │ ├── module_docstring.py.out │ │ ├── multi_line_no_push │ │ │ ├── class_docstring.py │ │ │ ├── class_docstring.py.out │ │ │ ├── function_docstrings.py │ │ │ ├── function_docstrings.py.out │ │ │ ├── module_docstring.py │ │ │ ├── module_docstring.py.out │ │ │ ├── variable_docstring.py │ │ │ └── variable_docstring.py.out │ │ ├── multi_line_push │ │ │ ├── class_docstring.args │ │ │ ├── class_docstring.py │ │ │ ├── class_docstring.py.out │ │ │ ├── function_docstrings.args │ │ │ ├── function_docstrings.py │ │ │ ├── function_docstrings.py.out │ │ │ ├── module_docstring.args │ │ │ ├── module_docstring.py │ │ │ ├── module_docstring.py.out │ │ │ ├── variable_docstring.args │ │ │ ├── variable_docstring.py │ │ │ └── variable_docstring.py.out │ │ ├── variable_docstring.py │ │ └── variable_docstring.py.out │ ├── capitalization_first_letter │ │ ├── class_docstring.py │ │ ├── class_docstring.py.out │ │ ├── function_docstrings.py │ │ ├── function_docstrings.py.out │ │ ├── module_docstring.py │ │ ├── module_docstring.py.out │ │ ├── single_quote_docstring.args │ │ ├── single_quote_docstring.py │ │ ├── single_quote_docstring.py.out │ │ ├── uncapitalized_starting_letter.py │ │ ├── uncapitalized_starting_letter.py.out │ │ ├── variable_docstring.py │ │ └── variable_docstring.py.out │ ├── ending_quote │ │ ├── class_docstring.py │ │ ├── class_docstring.py.out │ │ ├── function_docstrings.py │ │ ├── function_docstrings.py.out │ │ ├── module_docstring.py │ │ ├── module_docstring.py.out │ │ ├── variable_docstring.py │ │ └── variable_docstring.py.out │ ├── final_period │ │ ├── class_docstring.py │ │ ├── class_docstring.py.out │ │ ├── exotic_line_ending.py │ │ ├── exotic_line_ending.py.out │ │ ├── function_docstrings.py │ │ ├── function_docstrings.py.out │ │ ├── function_title_docstrings.py │ │ ├── function_title_docstrings.py.out │ │ ├── module_docstring.py │ │ ├── module_docstring.py.out │ │ ├── single_quote_docstring.args │ │ ├── single_quote_docstring.py │ │ ├── single_quote_docstring.py.out │ │ ├── variable_docstring.py │ │ └── variable_docstring.py.out │ ├── linewrap_summary │ │ ├── function_docstrings.args │ │ ├── function_docstrings.py │ │ ├── function_docstrings.py.out │ │ ├── max_line_length_50.args │ │ ├── max_line_length_50.py │ │ └── max_line_length_50.py.out │ ├── multi_line │ │ ├── class_docstring.py │ │ ├── class_docstring.py.out │ │ ├── function_docstrings.py │ │ ├── function_docstrings.py.out │ │ ├── module_docstring.py │ │ ├── module_docstring.py.out │ │ ├── variable_docstring.py │ │ └── variable_docstring.py.out │ ├── newlines │ │ ├── dos.py │ │ ├── dos.py.out │ │ ├── mac.py │ │ ├── mac.py.out │ │ ├── mixed.err │ │ ├── mixed.py │ │ ├── mixed.py.out │ │ ├── none.args │ │ ├── none.py │ │ ├── none.py-0d0a.out │ │ ├── none.py.out │ │ ├── unix.py │ │ └── unix.py.out │ ├── no_changes │ │ ├── multi_line_variables.py │ │ └── multi_line_variables.py.out │ ├── no_whitespace_stripper │ │ ├── module_docstring.args │ │ ├── module_docstring.py │ │ └── module_docstring.py.out │ ├── numpydoc │ │ ├── numpydoc_closing_quotes.args │ │ ├── numpydoc_closing_quotes.py │ │ ├── numpydoc_closing_quotes.py.out │ │ ├── numpydoc_header_line.args │ │ ├── numpydoc_header_line.py │ │ ├── numpydoc_header_line.py.out │ │ ├── numpydoc_parameter_spacing.args │ │ ├── numpydoc_parameter_spacing.py │ │ ├── numpydoc_parameter_spacing.py.out │ │ ├── numpydoc_regression.args │ │ ├── numpydoc_regression.py │ │ ├── numpydoc_regression.py.out │ │ ├── numpydoc_section_ordering.args │ │ ├── numpydoc_section_ordering.py │ │ ├── numpydoc_section_ordering.py.out │ │ ├── numpydoc_section_spacing.args │ │ ├── numpydoc_section_spacing.py │ │ ├── numpydoc_section_spacing.py.out │ │ ├── numpydoc_style.args │ │ ├── numpydoc_style.py │ │ ├── numpydoc_style.py.out │ │ ├── numpydoc_style_without_type.args │ │ ├── numpydoc_style_without_type.py │ │ └── numpydoc_style_without_type.py.out │ ├── quotes_type │ │ ├── class_docstring.py │ │ ├── class_docstring.py.out │ │ ├── function_docstrings.py │ │ ├── function_docstrings.py.out │ │ ├── module_docstring.py │ │ ├── module_docstring.py.out │ │ ├── single_quote_docstring.args │ │ ├── single_quote_docstring.py │ │ ├── single_quote_docstring.py.out │ │ ├── variable_docstring.py │ │ └── variable_docstring.py.out │ ├── summary_splitter │ │ ├── class_docstring.py │ │ ├── class_docstring.py.out │ │ ├── dots.args │ │ ├── dots.py │ │ ├── dots.py.out │ │ ├── empty_summary.args │ │ ├── empty_summary.py │ │ ├── empty_summary.py.out │ │ ├── ending_quotes.args │ │ ├── ending_quotes.py │ │ ├── ending_quotes.py.out │ │ ├── function_docstrings.py │ │ ├── function_docstrings.py.out │ │ ├── max_summary_lines │ │ │ ├── max_lines_is_1.args │ │ │ ├── max_lines_is_1.py │ │ │ ├── max_lines_is_1.py.out │ │ │ ├── max_lines_is_2.args │ │ │ ├── max_lines_is_2.py │ │ │ ├── max_lines_is_2.py.out │ │ │ ├── max_lines_is_3.args │ │ │ ├── max_lines_is_3.py │ │ │ ├── max_lines_is_3.py.out │ │ │ ├── max_lines_is_default.py │ │ │ ├── max_lines_is_default.py.out │ │ │ ├── max_lines_with_dot.py │ │ │ └── max_lines_with_dot.py.out │ │ ├── module_docstring.py │ │ ├── module_docstring.py.out │ │ ├── regression_151.py │ │ ├── regression_151.py.out │ │ ├── variable_docstring.py │ │ └── variable_docstring.py.out │ └── whitespace_stripper │ │ ├── class_docstring.py │ │ ├── class_docstring.py.out │ │ ├── function_docstrings.py │ │ ├── function_docstrings.py.out │ │ ├── module_docstring.py │ │ ├── module_docstring.py.out │ │ ├── single_quote_docstring.args │ │ ├── single_quote_docstring.py │ │ ├── single_quote_docstring.py.out │ │ ├── variable_docstring.py │ │ └── variable_docstring.py.out ├── incorrect_python_file.py └── utils │ ├── find_docstrings │ ├── dictionary.py │ ├── function_docstrings.py │ └── module_docstrings.py │ ├── find_nothing │ └── README.md │ ├── find_recursive_files │ ├── file_one.py │ ├── file_two.txt │ └── inner_directory │ │ ├── inner_file_one.py │ │ ├── inner_file_two.txt │ │ └── inner_inner_directory │ │ ├── inner_inner_file_one.py │ │ └── inner_inner_file_one.txt │ └── find_underscore_files │ ├── ____file_five.py │ ├── ___file_four.txt │ ├── __file_three.py │ ├── _file_two.py │ └── file_one.py ├── test_config.py ├── test_conflicting_formatters.py ├── test_exceptions.py ├── test_formatter.py ├── test_formatting.py ├── test_run.py └── test_utils.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = 3 | E203, 4 | W503, 5 | W293, 6 | max-line-length=88 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /tests/data/format/newlines/* -text 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [DanielNoord, Pierre-Sassoulas] 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" # Location of package manifests 5 | schedule: 6 | interval: "daily" 7 | labels: 8 | - "dependency" 9 | reviewers: 10 | - "danielnoord" 11 | - package-ecosystem: "github-actions" 12 | directory: "/" 13 | schedule: 14 | interval: "daily" 15 | labels: 16 | - "dependency" 17 | reviewers: 18 | - "danielnoord" 19 | -------------------------------------------------------------------------------- /.github/workflows/checks.yaml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | docs-build: 11 | name: Docs / Build 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check out code from GitHub 15 | uses: actions/checkout@v4.2.2 16 | with: 17 | fetch-depth: 2 18 | - name: Set up Python 3.13 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: "3.13" 22 | - name: Install dependencies 23 | run: pip install -U -r docs/requirements-doc.txt 24 | - name: Make docs 25 | run: | 26 | cd docs 27 | make html 28 | - name: Compare the diff 29 | run: | 30 | git diff > DIFF 31 | if [ -s DIFF ]; then 32 | echo "Documentation is not up to date." 33 | echo "Please perform the following commands locally and then re-commit." 34 | echo "pip install -e ." 35 | echo "cd docs" 36 | echo "make html" 37 | exit 1 38 | else 39 | echo "Documentation is up to date." 40 | fi 41 | -------------------------------------------------------------------------------- /.github/workflows/primer.yaml: -------------------------------------------------------------------------------- 1 | # Most of this is inspired by the mypy primer 2 | 3 | name: Primer 4 | 5 | on: 6 | pull_request: 7 | 8 | jobs: 9 | primer-run: 10 | name: Run 11 | runs-on: ubuntu-latest 12 | permissions: write-all 13 | steps: 14 | - name: Check out working version 15 | uses: actions/checkout@v4.2.2 16 | with: 17 | fetch-depth: 2 18 | - name: Check out changeable version 19 | uses: actions/checkout@v4.2.2 20 | with: 21 | path: program_to_test 22 | fetch-depth: 0 23 | - name: Set up Python 3.13 24 | uses: actions/setup-python@v5 25 | with: 26 | python-version: "3.13" 27 | - name: Install dependencies 28 | run: pip install -U -r requirements.txt 29 | - name: Run primer 30 | shell: bash 31 | run: | 32 | cd program_to_test 33 | echo "new commit" 34 | git rev-list --format=%s --max-count=1 $GITHUB_SHA 35 | 36 | MERGE_BASE=$(git merge-base $GITHUB_SHA origin/main) 37 | git checkout -b base_commit $MERGE_BASE 38 | echo "base commit" 39 | git rev-list --format=%s --max-count=1 base_commit 40 | 41 | echo "installing changeable version" 42 | pip install -e . 43 | cd .. 44 | 45 | python -m pydocstringformatter._testutils.primer.primer --prepare 46 | python -m pydocstringformatter._testutils.primer.primer --step-one 47 | 48 | cd program_to_test 49 | git checkout $GITHUB_SHA 50 | cd .. 51 | python -m pydocstringformatter._testutils.primer.primer --step-two 52 | - name: Save PR number and copy fulldiff.txt 53 | run: | 54 | echo ${{ github.event.pull_request.number }} | tee pr_number.txt 55 | cp .pydocstringformatter_primer_tests/fulldiff.txt fulldiff.txt 56 | - name: Upload primer diff and PR number 57 | uses: actions/upload-artifact@v4.6.2 58 | with: 59 | name: primer_diffs 60 | path: | 61 | pr_number.txt 62 | fulldiff.txt 63 | -------------------------------------------------------------------------------- /.github/workflows/primer_comment.yaml: -------------------------------------------------------------------------------- 1 | # Most of this is inspired by the mypy primer 2 | 3 | name: Comment with primer diff 4 | 5 | on: 6 | workflow_run: 7 | workflows: [Primer] 8 | types: 9 | - completed 10 | 11 | permissions: 12 | contents: read 13 | pull-requests: write 14 | 15 | jobs: 16 | primer-comment: 17 | name: Run 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Download diffs 21 | uses: actions/github-script@v7 22 | with: 23 | script: | 24 | const fs = require('fs'); 25 | const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ 26 | owner: context.repo.owner, 27 | repo: context.repo.repo, 28 | run_id: ${{ github.event.workflow_run.id }}, 29 | }); 30 | const [matchArtifact] = artifacts.data.artifacts.filter((artifact) => 31 | artifact.name == "primer_diffs"); 32 | const download = await github.rest.actions.downloadArtifact({ 33 | owner: context.repo.owner, 34 | repo: context.repo.repo, 35 | artifact_id: matchArtifact.id, 36 | archive_format: "zip", 37 | }); 38 | fs.writeFileSync("diff.zip", Buffer.from(download.data)); 39 | - run: unzip diff.zip 40 | - name: Post comment 41 | id: post-comment 42 | uses: actions/github-script@v7 43 | with: 44 | github-token: ${{ secrets.GITHUB_TOKEN }} 45 | script: | 46 | const fs = require('fs') 47 | const data = fs.readFileSync('fulldiff.txt', { encoding: 'utf8' }) 48 | console.log("Diff from primer:") 49 | console.log(data) 50 | let body 51 | if (data.trim()) { 52 | body = 'Diff from the primer, showing the effect of this PR on open source code:\n' + data 53 | } else { 54 | body = 'According to the primer, this change has no effect on the checked open source code. 🤖🎉' 55 | } 56 | const prNumber = parseInt(fs.readFileSync("pr_number.txt", { encoding: "utf8" })) 57 | await github.rest.issues.createComment({ 58 | issue_number: prNumber, 59 | owner: context.repo.owner, 60 | repo: context.repo.repo, 61 | body 62 | }) 63 | return prNumber 64 | - name: Hide old comments 65 | # Taken from mypy primer 66 | # v0.3.0 67 | uses: kanga333/comment-hider@c12bb20b48aeb8fc098e35967de8d4f8018fffdf 68 | with: 69 | github_token: ${{ secrets.GITHUB_TOKEN }} 70 | leave_visible: 1 71 | issue_number: ${{ steps.post-comment.outputs.result }} 72 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish-to-pypi: 9 | name: Publish to pypi 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out code from GitHub 13 | uses: actions/checkout@v4.2.2 14 | - name: Set up Python 3.11 15 | uses: actions/setup-python@v5 16 | with: 17 | python-version: "3.11" 18 | - name: Install dependencies 19 | run: | 20 | pip install -U -r requirements.txt 21 | pip install -U build 22 | - name: Build package 23 | run: python -m build 24 | - name: Publish to PyPI 25 | uses: pypa/gh-action-pypi-publish@release/v1 26 | with: 27 | user: __token__ 28 | password: ${{ secrets.PYPI_API_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | tests-linux: 11 | name: Run / ${{ matrix.python-version }} / Linux 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | python-version: [3.9, "3.10", "3.11", "3.12", "3.13"] 17 | steps: 18 | - name: Check out code from GitHub 19 | uses: actions/checkout@v4.2.2 20 | - name: Set up Python ${{ matrix.python-version }} 21 | id: python 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Install dependencies 26 | run: pip install -U -r requirements.txt 27 | - name: Run pytest 28 | run: pytest -vv --cov 29 | - name: Upload coverage artifact 30 | uses: actions/upload-artifact@v4.6.2 31 | with: 32 | include-hidden-files: true 33 | name: coverage-linux-${{ matrix.python-version }} 34 | path: .coverage 35 | 36 | tests-windows: 37 | name: Run / ${{ matrix.python-version }} / Windows 38 | runs-on: windows-latest 39 | strategy: 40 | fail-fast: false 41 | matrix: 42 | python-version: [3.9, "3.10", "3.11", "3.12", "3.13"] 43 | steps: 44 | - name: Check out code from GitHub 45 | uses: actions/checkout@v4.2.2 46 | - name: Set temp directory 47 | run: echo "TEMP=$env:USERPROFILE\AppData\Local\Temp" >> $env:GITHUB_ENV 48 | # Workaround to set correct temp directory on Windows 49 | # https://github.com/actions/virtual-environments/issues/712 50 | - name: Set up Python ${{ matrix.python-version }} 51 | id: python 52 | uses: actions/setup-python@v5 53 | with: 54 | python-version: ${{ matrix.python-version }} 55 | - name: Install dependencies 56 | run: pip install -U -r requirements.txt 57 | - name: Run pytest 58 | run: pytest -vv --cov 59 | - name: Upload coverage artifact 60 | uses: actions/upload-artifact@v4.6.2 61 | with: 62 | include-hidden-files: true 63 | name: coverage-windows-${{ matrix.python-version }} 64 | path: .coverage 65 | 66 | coverage: 67 | name: Process / Coverage 68 | runs-on: ubuntu-latest 69 | needs: ["tests-linux", "tests-windows"] 70 | steps: 71 | - name: Check out code from GitHub 72 | uses: actions/checkout@v4.2.2 73 | - name: Set up Python 3.11 74 | id: python 75 | uses: actions/setup-python@v5 76 | with: 77 | python-version: "3.11" 78 | - name: Install dependencies 79 | run: pip install -U -r requirements-coverage.txt 80 | - name: Download all coverage artifacts 81 | uses: actions/download-artifact@v4.3.0 82 | - name: Combine Linux coverage results 83 | run: | 84 | coverage combine coverage-linux*/.coverage 85 | coverage xml -o coverage-linux.xml 86 | - uses: codecov/codecov-action@v5 87 | with: 88 | fail_ci_if_error: true 89 | verbose: true 90 | flags: linux 91 | files: coverage-linux.xml 92 | token: ${{ secrets.CODECOV_TOKEN }} 93 | - name: Combine Windows coverage results 94 | run: | 95 | coverage combine coverage-windows*/.coverage 96 | coverage xml -o coverage-windows.xml 97 | - uses: codecov/codecov-action@v5 98 | with: 99 | fail_ci_if_error: true 100 | verbose: true 101 | flags: windows 102 | files: coverage-windows.xml 103 | token: ${{ secrets.CODECOV_TOKEN }} 104 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pydocstringformatter_primer_tests/ 2 | pydocstringformatter.egg-info 3 | dist/ 4 | docs/_build 5 | 6 | .coverage 7 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: trailing-whitespace 6 | exclude: "tests/data/format/no_whitespace_stripper|tests/data/format/whitespace_stripper|tests/data/format/quotes_type|tests/data/format/newlines|tests/test_config.py" 7 | - id: end-of-file-fixer 8 | exclude: "tests/data/format/newlines" 9 | - id: check-yaml 10 | - id: check-toml 11 | exclude: &test-data "tests/data" 12 | - repo: https://github.com/PyCQA/autoflake 13 | rev: v2.3.1 14 | hooks: 15 | - id: autoflake 16 | exclude: *test-data 17 | args: 18 | - --in-place 19 | - --remove-all-unused-imports 20 | - --expand-star-imports 21 | - --remove-duplicate-keys 22 | - --remove-unused-variables 23 | - repo: https://github.com/asottile/pyupgrade 24 | rev: v3.20.0 25 | hooks: 26 | - id: pyupgrade 27 | args: [--py38-plus] 28 | - repo: https://github.com/PyCQA/isort 29 | rev: 6.0.1 30 | hooks: 31 | - id: isort 32 | - repo: https://github.com/psf/black 33 | rev: 25.1.0 34 | hooks: 35 | - id: black 36 | - repo: https://github.com/PyCQA/flake8 37 | rev: 7.2.0 38 | hooks: 39 | - id: flake8 40 | exclude: *test-data 41 | - repo: https://github.com/pylint-dev/pylint 42 | rev: v3.3.7 43 | hooks: 44 | - id: pylint 45 | exclude: *test-data 46 | args: ["--disable=import-error, cyclic-import"] 47 | - repo: https://github.com/pre-commit/mirrors-mypy 48 | rev: v1.16.0 49 | hooks: 50 | - id: mypy 51 | args: [--config-file=pyproject.toml] 52 | exclude: *test-data 53 | additional_dependencies: 54 | [pytest-stub==1.1.0, types-docutils~=0.17.5, sphinx~=4.4] 55 | - repo: https://github.com/DanielNoord/pydocstringformatter 56 | rev: v0.7.3 57 | hooks: 58 | - id: pydocstringformatter 59 | exclude: *test-data 60 | args: [] 61 | - repo: https://github.com/pre-commit/mirrors-prettier 62 | rev: v4.0.0-alpha.8 63 | hooks: 64 | - id: prettier 65 | exclude: *test-data 66 | args: [--prose-wrap=always, --print-width=88] 67 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: pydocstringformatter 2 | name: pydocstringformatter 3 | entry: pydocstringformatter --write --quiet 4 | language: python 5 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 2 | 3 | version: 2 4 | 5 | python: 6 | install: 7 | - requirements: docs/requirements-doc.txt 8 | 9 | sphinx: 10 | builder: html 11 | fail_on_warning: true 12 | configuration: docs/conf.py 13 | 14 | build: 15 | os: "ubuntu-lts-latest" 16 | tools: 17 | python: "latest" 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Daniël van Noord 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PyPI pyversions](https://img.shields.io/pypi/pyversions/pydocstringformatter.svg)](https://pypi.python.org/pypi/pydocstringformatter/) 2 | [![codecov](https://codecov.io/gh/DanielNoord/pydocstringformatter/branch/main/graph/badge.svg?token=TR61QNMBZG)](https://codecov.io/gh/DanielNoord/pydocstringformatter) 3 | [![Tests](https://github.com/DanielNoord/pydocstringformatter/actions/workflows/tests.yaml/badge.svg?branch=main)](https://github.com/DanielNoord/pydocstringformatter/actions/workflows/tests.yaml) 4 | [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/DanielNoord/pydocstringformatter/main.svg)](https://results.pre-commit.ci/latest/github/DanielNoord/pydocstringformatter/main) 5 | [![Documentation Status](https://readthedocs.org/projects/pydocstringformatter/badge/?version=latest)](https://pydocstringformatter.readthedocs.io/en/latest/?badge=latest) 6 | 7 | # Pydocstringformatter 8 | 9 | A tool to automatically format Python docstrings to follow recommendations from 10 | [`PEP 8`](https://www.python.org/dev/peps/pep-0008/) and 11 | [`PEP 257`](https://www.python.org/dev/peps/pep-0257/) (or other supported style 12 | guides.) 13 | 14 | See [What it does](#what-it-does) for currently supported auto-formatting. 15 | 16 | **Rationale** 17 | 18 | This project is heavily inspired by 19 | [`docformatter`](https://github.com/PyCQA/docformatter). 20 | 21 | When this project was started `docformatter` did not meet all of the requirements the 22 | [`pylint`](https://github.com/PyCQA/pylint) project had for its docstring formatter and 23 | was no longer actively maintained (this has changed since then). Therefore, some 24 | contributors of `pylint` got together and started working on our own formatter to 25 | fulfill our needs. 26 | 27 | When asked we defined the objective of the tool as: 28 | 29 | _"A docstring formatter that follows PEP8 and PEP257 but makes some of the more 30 | 'controversial' elements of the PEPs optional"_ 31 | 32 | See 33 | [the original answer](https://github.com/DanielNoord/pydocstringformatter/issues/38). 34 | 35 | As such, the biggest difference between the two is that `pydocstringformatter` fixes 36 | some of the open issues we found in `docformatter`. In general, the output of both 37 | formatters (and any other docstring formatter) should be relatively similar. 38 | 39 | ## How to install 40 | 41 | ```shell 42 | pip install pydocstringformatter 43 | ``` 44 | 45 | ## Usage 46 | 47 | [`Click here`](https://pydocstringformatter.readthedocs.io/en/latest/usage.html) for a 48 | full Usage overview. 49 | 50 | ### Configuration 51 | 52 | Pydocstringformatter will also read any configuration added to the 53 | `[tool.pydocstringformatter]` section of a `pyproject.toml` file. 54 | 55 | For example: 56 | 57 | ```toml 58 | [tool.pydocstringformatter] 59 | write = true 60 | exclude = "**/my_dir/**,**/my_other_dir/**" 61 | # Or: 62 | exclude = ["**/my_dir/**", "**/my_other_dir/**"] 63 | strip-whitespaces = true 64 | split-summary-body = false 65 | numpydoc-section-hyphen-length = false 66 | ``` 67 | 68 | #### Style 69 | 70 | Pydocstringformatter can be configured to use a specific style. The default is `pep257` 71 | but we support other styles as well. These can also be used at the same time. For 72 | example with: 73 | 74 | ```console 75 | pydocstringformatter --style=pep257 --style=numpydoc myfile.py 76 | ``` 77 | 78 | ## Pre-commit 79 | 80 | Pydocstringformatter can also be used as a [pre-commit hook](https://pre-commit.com). 81 | Add the following to your `.pre-commit-config.yaml` file: 82 | 83 | ```yaml 84 | - repo: https://github.com/DanielNoord/pydocstringformatter 85 | rev: SPECIFY VERSION HERE 86 | hooks: 87 | - id: pydocstringformatter 88 | ``` 89 | 90 | ## What it does 91 | 92 | The following examples show some of the changes pydocstringformatter will apply. For a 93 | full overview of all potential changes you can check out the 94 | [`Usage`](https://pydocstringformatter.readthedocs.io/en/latest/usage.html) page which 95 | shows an up to date list of all formatters and their description. 96 | 97 | ```python 98 | # Bad 99 | ''' 100 | my docstring''' 101 | 102 | """ my 103 | multi-line docstring """ 104 | 105 | """my title 106 | =========== 107 | 108 | my docstring 109 | """ 110 | 111 | 112 | # Good 113 | """My docstring.""" 114 | 115 | """My 116 | multi-line docstring. 117 | """ 118 | 119 | """My title 120 | =========== 121 | 122 | My docstring 123 | """ 124 | 125 | # With --summary-quotes-same-line 126 | # Bad 127 | """ 128 | My 129 | multi-line docstring 130 | """ 131 | 132 | # Good 133 | """My 134 | multi-line docstring 135 | """ 136 | ``` 137 | 138 | ## Development 139 | 140 | For development and contributing guidelines please see 141 | [`Development`](https://pydocstringformatter.readthedocs.io/en/latest/development.html). 142 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: 4 | default: 5 | target: 100% 6 | project: 7 | default: 8 | target: 100% 9 | comment: 10 | layout: "reach, diff, flags, files" 11 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_ext/usage_page.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | from sphinx import application 4 | 5 | 6 | def write_usage_page() -> None: 7 | """Write docs/usage.rst.""" 8 | 9 | # Get the help message 10 | with subprocess.Popen( 11 | ["pydocstringformatter", "-h"], stdout=subprocess.PIPE 12 | ) as proc: 13 | assert proc.stdout 14 | out = proc.stdout.readlines() 15 | 16 | # Add correct indentation for the code block 17 | help_message = " ".join(i.decode("utf-8") for i in out) 18 | # Remove indentation from completely empty lines 19 | help_message = help_message.replace(" \n", "\n") 20 | 21 | with open("usage.rst", mode="w", encoding="utf-8") as file: 22 | file.write( 23 | f"""Usage 24 | ===== 25 | 26 | Current usage of ``pydocstringformatter``: 27 | 28 | .. code-block:: shell 29 | 30 | {help_message}""" 31 | ) 32 | 33 | 34 | def setup(_: application.Sphinx) -> None: 35 | """Required function to register the Sphinx extension.""" 36 | write_usage_page() 37 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=invalid-name 2 | # flake8: noqa 3 | 4 | """Configuration file for the Sphinx documentation builder.""" 5 | 6 | # -- Path setup -------------------------------------------------------------- 7 | 8 | # Add parent directory so we can import pydocstringformatter 9 | import os 10 | import sys 11 | 12 | sys.path.insert(0, os.path.abspath("..")) 13 | 14 | # pylint: disable-next=import-error, wrong-import-position 15 | import pydocstringformatter # noqa: E402 16 | 17 | # -- Project information ----------------------------------------------------- 18 | 19 | project = "pydocstringformatter" 20 | copyright = "2022, Github Contributors" # pylint: disable=redefined-builtin 21 | author = "Github Contributors" 22 | release = pydocstringformatter.__version__ 23 | 24 | # -- General configuration --------------------------------------------------- 25 | 26 | extensions = ["myst_parser", "docs._ext.usage_page"] 27 | myst_heading_anchors = 2 28 | source_suffix = [".rst", ".md"] 29 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 30 | 31 | # -- Options for HTML output ------------------------------------------------- 32 | 33 | html_theme = "furo" 34 | html_title = "pydocstringformatter" 35 | 36 | html_theme_options = { 37 | "light_css_variables": {"font-size-sidebar-brand-text": "10px"}, 38 | "footer_icons": [ 39 | { 40 | "name": "GitHub", 41 | "url": "https://github.com/danielnoord/pydocstringformatter/", 42 | "html": """ 43 | 44 | 45 | 46 | """, 47 | "class": "", 48 | }, 49 | ], 50 | } 51 | -------------------------------------------------------------------------------- /docs/development.rst: -------------------------------------------------------------------------------- 1 | Development 2 | =========== 3 | 4 | Linting and checks 5 | ------------------ 6 | 7 | Use ``pip`` to install ``pre-commit`` and then use ``pre-commit install`` to 8 | install the pre-commit hook for the repository. 9 | 10 | Creating a new formatter 11 | ------------------------ 12 | 13 | - Implement a Formatter by inheriting from ``pydocstringformatter.formatting.Formatter``. 14 | - Add your new formatter to ``pydocstringformatter.formatting.FORMATTERS``. 15 | - Write a clear docstring because this will be user-facing: it's what will be seen in 16 | the help message for the formatter's command line option. 17 | - Choose a proper name because this will be user-facing: the name will be used to turn 18 | the formatter on and off via the command line or config files. 19 | - Rebuild the documentation by running: 20 | 21 | .. code-block:: shell 22 | 23 | cd docs 24 | make html 25 | 26 | 27 | Testing 28 | ------- 29 | 30 | To run all the tests: 31 | 32 | .. code-block:: shell 33 | 34 | pytest 35 | 36 | To create test for a specific formatting option use the ``tests/data/format`` directory. 37 | For each ``.py`` file create a ``.py.out`` file with the expected output after formatting. 38 | The test suite will automatically pickup the new tests. 39 | 40 | To only run a specific test from that directory, for example 41 | ``tests/data/format/multi_line/class_docstring.py``, use: 42 | 43 | .. code-block:: shell 44 | 45 | pytest -k multi_line-class_docstring 46 | 47 | 48 | Primer 49 | ------- 50 | 51 | To check if any changes create any unforeseen regressions all pull requests are tested 52 | with our primer. This process is heavily inspired on other open source projects and our 53 | own primer is based on the one used by `mypy `_. 54 | 55 | You can also run the primer locally with minimal set-up. First you will need to make 56 | a duplicate of your ``pydocstringformatter`` directory in a new ``./program_to_test`` 57 | directory. This is required so that the primer can check out multiple versions of the 58 | program and compare the difference. 59 | 60 | The next step is to follow the bash script in the ``Run primer`` step in the ``primer`` 61 | workflow file, found at ``.github/workflows/primer.yaml``. This should allow you to run 62 | all necessary steps locally. 63 | 64 | The final output of the primer run can be found in ``.pydocstringformatter_primer_tests/fulldiff.txt`` 65 | 66 | New projects to run the primer over can be added to the ``pydocstringformatter/testutils/primer/packages.py`` 67 | file. 68 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | :hidden: 3 | 4 | Home 5 | usage 6 | development 7 | 8 | .. include:: ../README.md 9 | :parser: myst_parser.sphinx_ 10 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.https://www.sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements-doc.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | myst-parser~=4.0 3 | sphinx~=8.2.3 4 | furo==2024.8.6 5 | types-docutils~=0.21 6 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | Current usage of ``pydocstringformatter``: 5 | 6 | .. code-block:: shell 7 | 8 | usage: pydocstringformatter [-h] [-w] [--quiet] [-v] [--exclude EXCLUDE] 9 | [--exit-code] [--max-summary-lines int] 10 | [--summary-quotes-same-line] 11 | [--max-line-length int] 12 | [--style {pep257,numpydoc} [{pep257,numpydoc} ...]] 13 | [--strip-whitespaces --no-strip-whitespaces] 14 | [--split-summary-body --no-split-summary-body] 15 | [--numpydoc-section-order --no-numpydoc-section-order] 16 | [--numpydoc-name-type-spacing --no-numpydoc-name-type-spacing] 17 | [--numpydoc-section-spacing --no-numpydoc-section-spacing] 18 | [--numpydoc-section-hyphen-length --no-numpydoc-section-hyphen-length] 19 | [--linewrap-full-docstring --no-linewrap-full-docstring] 20 | [--beginning-quotes --no-beginning-quotes] 21 | [--closing-quotes --no-closing-quotes] 22 | [--capitalize-first-letter --no-capitalize-first-letter] 23 | [--final-period --no-final-period] 24 | [--quotes-type --no-quotes-type] 25 | [files ...] 26 | 27 | positional arguments: 28 | files The directory or files to format. 29 | 30 | options: 31 | -h, --help show this help message and exit 32 | -w, --write Write the changes to file instead of printing the 33 | diffs to stdout. 34 | --quiet Do not print any logging or status messages to stdout. 35 | -v, --version Show version number and exit. 36 | 37 | configuration: 38 | --exclude EXCLUDE A comma separated list of glob patterns of file path 39 | names not to be formatted. 40 | --exit-code Turn on if the program should exit with bitwise exit 41 | codes. 0 = No changes, 32 = Changed files or printed 42 | diff. 43 | --max-summary-lines int 44 | The maximum numbers of lines a summary can span. The 45 | default value is 1. 46 | --summary-quotes-same-line 47 | Force the start of a multi-line docstring to be on the 48 | same line as the opening quotes. Similar to how this 49 | is enforced for single line docstrings. 50 | --max-line-length int 51 | Maximum line length of docstrings. 52 | --style {pep257,numpydoc} [{pep257,numpydoc} ...] 53 | Docstring styles that are used in the project. Can be 54 | more than one. 55 | 56 | default formatters: 57 | these formatters are turned on by default 58 | 59 | --strip-whitespaces, --no-strip-whitespaces 60 | Activate or deactivate strip-whitespaces: Strip 1) 61 | docstring start, 2) docstring end and 3) end of line. 62 | Styles: default. (default: True) 63 | --split-summary-body, --no-split-summary-body 64 | Activate or deactivate split-summary-body: Split the 65 | summary and body of a docstring based on a period and 66 | max length. The maximum length of a summary can be set 67 | with the --max-summary-lines option. Styles: pep257. 68 | (default: True) 69 | --numpydoc-section-order, --no-numpydoc-section-order 70 | Activate or deactivate numpydoc-section-order: Change 71 | section order to match numpydoc guidelines. Styles: 72 | numpydoc. (default: True) 73 | --numpydoc-name-type-spacing, --no-numpydoc-name-type-spacing 74 | Activate or deactivate numpydoc-name-type-spacing: 75 | Ensure proper spacing around the colon separating 76 | names from types. Styles: numpydoc. (default: True) 77 | --numpydoc-section-spacing, --no-numpydoc-section-spacing 78 | Activate or deactivate numpydoc-section-spacing: 79 | Ensure proper spacing between sections. Styles: 80 | numpydoc. (default: True) 81 | --numpydoc-section-hyphen-length, --no-numpydoc-section-hyphen-length 82 | Activate or deactivate numpydoc-section-hyphen-length: 83 | Ensure hyphens after section header lines are proper 84 | length. Styles: numpydoc. (default: True) 85 | --beginning-quotes, --no-beginning-quotes 86 | Activate or deactivate beginning-quotes: Fix the 87 | position of the opening quotes. Styles: default. 88 | (default: True) 89 | --closing-quotes, --no-closing-quotes 90 | Activate or deactivate closing-quotes: Fix the 91 | position of the closing quotes. Styles: default. 92 | (default: True) 93 | --capitalize-first-letter, --no-capitalize-first-letter 94 | Activate or deactivate capitalize-first-letter: 95 | Capitalize the first letter of the docstring if 96 | appropriate. Styles: default. (default: True) 97 | --final-period, --no-final-period 98 | Activate or deactivate final-period: Add a period to 99 | the end of single line docstrings and summaries. 100 | Styles: default. (default: True) 101 | --quotes-type, --no-quotes-type 102 | Activate or deactivate quotes-type: Change all opening 103 | and closing quotes to be triple quotes. Styles: 104 | default. (default: True) 105 | 106 | optional formatters: 107 | these formatters are turned off by default 108 | 109 | --linewrap-full-docstring, --no-linewrap-full-docstring 110 | Activate or deactivate linewrap-full-docstring: 111 | Linewrap the docstring by the pre-defined line length. 112 | Styles: default. (default: False) 113 | -------------------------------------------------------------------------------- /pydocstringformatter/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable = import-outside-toplevel 2 | from __future__ import annotations 3 | 4 | import sys 5 | 6 | from pydocstringformatter._utils.exceptions import ( 7 | ParsingError, 8 | PydocstringFormatterError, 9 | ) 10 | 11 | __version__ = "0.8.0-dev" 12 | 13 | 14 | def run_docstring_formatter(argv: list[str] | None = None) -> None: 15 | """Run the formatter.""" 16 | from pydocstringformatter.run import _Run 17 | 18 | _Run(argv or sys.argv[1:]) 19 | 20 | 21 | __all__ = ("run_docstring_formatter", "PydocstringFormatterError", "ParsingError") 22 | -------------------------------------------------------------------------------- /pydocstringformatter/__main__.py: -------------------------------------------------------------------------------- 1 | import pydocstringformatter 2 | 3 | pydocstringformatter.run_docstring_formatter() 4 | -------------------------------------------------------------------------------- /pydocstringformatter/_configuration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanielNoord/pydocstringformatter/60e016d59f4b02cdd07a0f1a9492b807be76ed85/pydocstringformatter/_configuration/__init__.py -------------------------------------------------------------------------------- /pydocstringformatter/_configuration/arguments_manager.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import argparse 4 | 5 | from pydocstringformatter._configuration import ( 6 | command_line_parsing, 7 | formatter_options, 8 | toml_parsing, 9 | ) 10 | from pydocstringformatter._configuration.validators import VALIDATORS 11 | from pydocstringformatter._formatting.base import Formatter 12 | 13 | 14 | class ArgumentsManager: 15 | """Handler for arugments adding and parsing.""" 16 | 17 | def __init__(self, version: str, formatters: list[Formatter]) -> None: 18 | # Initialize instance attributes for argument parsing 19 | self.parser = argparse.ArgumentParser(prog="pydocstringformatter") 20 | self.namespace = argparse.Namespace() 21 | 22 | self.formatters = formatters 23 | """List of loaded formatters.""" 24 | 25 | # First register all argument groups, then add arguments 26 | self.configuration_group = self.parser.add_argument_group("configuration") 27 | self.default_formatters_group = self.parser.add_argument_group( 28 | "default formatters", "these formatters are turned on by default" 29 | ) 30 | self.optional_formatters_group = self.parser.add_argument_group( 31 | "optional formatters", "these formatters are turned off by default" 32 | ) 33 | 34 | # Register all arguments 35 | self.register_arguments(version) 36 | formatter_options.register_arguments_formatters( 37 | self.default_formatters_group, 38 | self.optional_formatters_group, 39 | self.formatters, 40 | ) 41 | 42 | def register_arguments(self, version: str) -> None: 43 | """Register all standard arguments on the parser.""" 44 | self.parser.add_argument( 45 | "files", nargs="*", type=str, help="The directory or files to format." 46 | ) 47 | 48 | self.parser.add_argument( 49 | "-w", 50 | "--write", 51 | action="store_true", 52 | help="Write the changes to file instead of printing the diffs to stdout.", 53 | ) 54 | 55 | self.parser.add_argument( 56 | "--quiet", 57 | action="store_true", 58 | help="Do not print any logging or status messages to stdout.", 59 | ) 60 | 61 | self.parser.add_argument( 62 | "-v", 63 | "--version", 64 | action="version", 65 | version=version, 66 | help="Show version number and exit.", 67 | ) 68 | 69 | self.configuration_group.add_argument( 70 | "--exclude", 71 | action="store", 72 | default=[""], 73 | type=VALIDATORS["csv"], 74 | help=( 75 | "A comma separated list of glob patterns of " 76 | "file path names not to be formatted." 77 | ), 78 | ) 79 | 80 | self.configuration_group.add_argument( 81 | "--exit-code", 82 | action="store_true", 83 | default=False, 84 | help=( 85 | "Turn on if the program should exit with bitwise exit codes. " 86 | "0 = No changes, 32 = Changed files or printed diff." 87 | ), 88 | ) 89 | 90 | self.configuration_group.add_argument( 91 | "--max-summary-lines", 92 | action="store", 93 | default=1, 94 | type=int, 95 | help=( 96 | "The maximum numbers of lines a summary can span. " 97 | "The default value is 1." 98 | ), 99 | metavar="int", 100 | ) 101 | 102 | self.configuration_group.add_argument( 103 | "--summary-quotes-same-line", 104 | action="store_true", 105 | help=( 106 | "Force the start of a multi-line docstring to be on the " 107 | "same line as the opening quotes. Similar to how this is enforced " 108 | "for single line docstrings." 109 | ), 110 | ) 111 | 112 | self.configuration_group.add_argument( 113 | "--max-line-length", 114 | action="store", 115 | default=88, 116 | type=int, 117 | help="Maximum line length of docstrings.", 118 | metavar="int", 119 | ) 120 | 121 | self.configuration_group.add_argument( 122 | "--style", 123 | action="extend", 124 | type=str, 125 | nargs="+", 126 | choices=["pep257", "numpydoc"], 127 | help="Docstring styles that are used in the project. Can be more than one.", 128 | ) 129 | 130 | def parse_options( 131 | self, 132 | argv: list[str], 133 | ) -> None: 134 | """Load all default option values. 135 | 136 | The order of parsing is: 137 | 1. configuration files, 2. command line arguments, 3. set default values. 138 | """ 139 | # pylint: disable=protected-access 140 | toml_parsing.parse_toml_file(self.parser, self.namespace) 141 | 142 | command_line_parsing.parse_command_line_arguments( 143 | self.parser, self.namespace, argv 144 | ) 145 | 146 | # 'style' uses the 'extend' action. If we use a normal default value, 147 | # and the default gets supplied as well style == ["pep257", "pep257"]. 148 | if self.namespace.style is None: 149 | self.namespace.style = ["pep257"] 150 | 151 | def print_help(self) -> None: 152 | """Print the help or usage message.""" 153 | self.parser.print_help() 154 | -------------------------------------------------------------------------------- /pydocstringformatter/_configuration/boolean_option_action.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import argparse 4 | from collections.abc import Sequence 5 | from typing import Any, Literal 6 | 7 | 8 | class BooleanOptionalAction(argparse.Action): 9 | """Boolean action that combines the '--no' argument. 10 | 11 | This action is directly copied from argparse with minor modifications. 12 | This action class is only available in 3.9+. Hence the need to backport it. 13 | """ 14 | 15 | # pylint: disable-next=too-many-arguments,too-many-positional-arguments 16 | def __init__( 17 | self, 18 | option_strings: Sequence[str], 19 | dest: str, 20 | default: bool, 21 | type: Literal[None] = None, # pylint: disable=redefined-builtin 22 | choices: Literal[None] = None, 23 | required: bool = False, 24 | help: str | None = None, # pylint: disable=redefined-builtin 25 | metavar: Literal[None] = None, 26 | ) -> None: 27 | # Non-argparse changes 28 | assert help, "All BooleanOptionalAction's should have a help message." 29 | 30 | # Rest of implementation directly copied from argparse, expect for the asserts 31 | _option_strings = [] 32 | for option_string in option_strings: 33 | _option_strings.append(option_string) 34 | 35 | assert option_string.startswith("--") 36 | option_string = "--no-" + option_string[2:] 37 | _option_strings.append(option_string) 38 | 39 | assert help is not None and default is not None 40 | help += " (default: %(default)s)" 41 | 42 | super().__init__( 43 | option_strings=_option_strings, 44 | dest=dest, 45 | nargs=0, 46 | default=default, 47 | type=type, 48 | choices=choices, 49 | required=required, 50 | help=help, 51 | metavar=metavar, 52 | ) 53 | 54 | def __call__( 55 | self, 56 | parser: argparse.ArgumentParser, 57 | namespace: argparse.Namespace, 58 | values: str | Sequence[Any] | None, 59 | option_string: str | None = None, 60 | ) -> None: 61 | """Store correct value on the namespace object.""" 62 | assert option_string, ( 63 | "BooleanOptionalAction can't be a positional argument. " 64 | f"Something is wrong with {self.option_strings[0]}" 65 | ) 66 | assert option_string in self.option_strings 67 | setattr(namespace, self.dest, not option_string.startswith("--no-")) 68 | 69 | def format_usage(self) -> str: 70 | """Return usage string.""" 71 | return " ".join(self.option_strings) 72 | -------------------------------------------------------------------------------- /pydocstringformatter/_configuration/command_line_parsing.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import argparse 4 | 5 | 6 | def parse_command_line_arguments( 7 | parser: argparse.ArgumentParser, namespace: argparse.Namespace, args: list[str] 8 | ) -> None: 9 | """Parse all arguments on the provided argument parser.""" 10 | parser.parse_known_args(args, namespace) 11 | -------------------------------------------------------------------------------- /pydocstringformatter/_configuration/formatter_options.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import argparse 4 | 5 | from pydocstringformatter._configuration.boolean_option_action import ( 6 | BooleanOptionalAction, 7 | ) 8 | from pydocstringformatter._formatting.base import Formatter 9 | 10 | 11 | def register_arguments_formatters( 12 | default_arg_group: argparse._ArgumentGroup, 13 | optional_arg_group: argparse._ArgumentGroup, 14 | formatters: list[Formatter], 15 | ) -> None: 16 | """Register a list of formatters, so they can all be deactivated or activated.""" 17 | for formatter in formatters: 18 | arg_group = default_arg_group 19 | if formatter.optional: 20 | arg_group = optional_arg_group 21 | 22 | name = formatter.name 23 | arg_group.add_argument( 24 | formatter.activate_option, 25 | action=BooleanOptionalAction, 26 | dest=name, 27 | help=f"Activate or deactivate {name}: {formatter.__doc__}" 28 | f" Styles: {','.join(formatter.style)}.", 29 | default=not formatter.optional, 30 | ) 31 | -------------------------------------------------------------------------------- /pydocstringformatter/_configuration/toml_parsing.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import argparse 4 | import os 5 | import sys 6 | from typing import Any 7 | 8 | from pydocstringformatter._configuration.boolean_option_action import ( 9 | BooleanOptionalAction, 10 | ) 11 | from pydocstringformatter._utils.exceptions import TomlParsingError, UnrecognizedOption 12 | 13 | if sys.version_info < (3, 11): 14 | import tomli as tomllib 15 | else: 16 | import tomllib 17 | 18 | 19 | def get_toml_file() -> dict[str, Any] | None: 20 | """See if there is a pyproject.toml and extract the correct section if it exists.""" 21 | if os.path.isfile("pyproject.toml"): 22 | with open("pyproject.toml", "rb") as file: 23 | try: 24 | toml_dict = tomllib.load(file) 25 | except tomllib.TOMLDecodeError as exc: 26 | raise TomlParsingError from exc 27 | if tool_section := toml_dict.get("tool", None): 28 | if pydocformat_sect := tool_section.get("pydocstringformatter", None): 29 | assert isinstance(pydocformat_sect, dict) 30 | return pydocformat_sect 31 | return None 32 | 33 | 34 | def parse_toml_option( # pylint: disable=too-many-branches 35 | parser: argparse.ArgumentParser, opt: str, value: Any 36 | ) -> list[str]: 37 | """Parse an options value in the correct argument type for argparse.""" 38 | # pylint: disable=protected-access 39 | try: 40 | action = parser._option_string_actions[f"--{opt}"] 41 | except KeyError as exc: 42 | try: 43 | action = parser._option_string_actions[f"-{opt}"] 44 | except KeyError: 45 | raise UnrecognizedOption(f"Don't recognize option {opt}") from exc 46 | 47 | if isinstance(action, BooleanOptionalAction): 48 | if not isinstance(value, bool): 49 | error_msg = f"{{'{value}'}} {type(value)} is not a supported argument for" 50 | error_msg += f" '{opt}', please use either {{true}} or {{false}}." 51 | raise ValueError(error_msg) 52 | 53 | if opt.startswith("no") and f"--{opt[3:]}" in action.option_strings: 54 | opposite_opt = opt[3:] 55 | val = ["false", "true"][value] 56 | opp_val = ["true", "false"][value] 57 | error_msg = ( 58 | "TOML file contains an unsupported option " 59 | f"'{opt}: {val}', try using '{opposite_opt}: {opp_val}' instead" 60 | ) 61 | raise TomlParsingError(error_msg) 62 | 63 | return [f"--{'no-' if not value else ''}{opt}"] 64 | 65 | if isinstance(action, argparse._StoreTrueAction): 66 | if value is True: 67 | return [action.option_strings[0]] 68 | return [] 69 | 70 | if isinstance(action, argparse._StoreAction): 71 | if isinstance(value, int): 72 | value = str(value) 73 | return [action.option_strings[0], value] 74 | 75 | if isinstance(action, argparse._ExtendAction): 76 | out_args = [] 77 | if isinstance(value, list): 78 | for item in value: 79 | out_args += [action.option_strings[0], item] 80 | else: 81 | out_args = [action.option_strings[0], value] 82 | 83 | return out_args 84 | 85 | raise NotImplementedError # pragma: no cover 86 | 87 | 88 | def parse_toml_file( 89 | parser: argparse.ArgumentParser, namespace: argparse.Namespace 90 | ) -> None: 91 | """Get and parse the relevant section form a pyproject.toml file.""" 92 | if toml_sect := get_toml_file(): 93 | arguments: list[str] = [] 94 | 95 | for key, value in toml_sect.items(): 96 | arguments += parse_toml_option(parser, key, value) 97 | 98 | parser.parse_args(arguments, namespace) 99 | -------------------------------------------------------------------------------- /pydocstringformatter/_configuration/validators.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Callable 4 | from typing import Final, List 5 | 6 | 7 | def comma_separated_list_validator(value: str | list[str]) -> list[str]: 8 | """Validate a comma separated list.""" 9 | if isinstance(value, list): 10 | return value 11 | return value.split(",") 12 | 13 | 14 | ValidatedTypes = List[str] 15 | VALIDATORS: Final[dict[str, Callable[[str], ValidatedTypes]]] = { 16 | "csv": comma_separated_list_validator 17 | } 18 | -------------------------------------------------------------------------------- /pydocstringformatter/_formatting/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | __all__ = ["FORMATTERS", "Formatter"] 4 | 5 | 6 | from pydocstringformatter._formatting.base import Formatter 7 | from pydocstringformatter._formatting.formatters_default import ( 8 | BeginningQuotesFormatter, 9 | CapitalizeFirstLetterFormatter, 10 | ClosingQuotesFormatter, 11 | FinalPeriodFormatter, 12 | LineWrapperFormatter, 13 | QuotesTypeFormatter, 14 | StripWhitespacesFormatter, 15 | ) 16 | from pydocstringformatter._formatting.formatters_numpydoc import ( 17 | NumpydocNameColonTypeFormatter, 18 | NumpydocSectionHyphenLengthFormatter, 19 | NumpydocSectionOrderingFormatter, 20 | NumpydocSectionSpacingFormatter, 21 | ) 22 | from pydocstringformatter._formatting.formatters_pep257 import ( 23 | SplitSummaryAndDocstringFormatter, 24 | ) 25 | 26 | # The order of these formatters is important as they are called in order. 27 | # The order is currently: 28 | # String manipulation such as adding extra new lines 29 | # Determine if multi-line or single line and position quotes accordingly 30 | # String manipulation in which being multi-line or single line matters 31 | FORMATTERS: list[Formatter] = [ 32 | StripWhitespacesFormatter(), 33 | SplitSummaryAndDocstringFormatter(), 34 | NumpydocSectionOrderingFormatter(), 35 | NumpydocNameColonTypeFormatter(), 36 | NumpydocSectionSpacingFormatter(), 37 | NumpydocSectionHyphenLengthFormatter(), 38 | LineWrapperFormatter(), 39 | BeginningQuotesFormatter(), 40 | ClosingQuotesFormatter(), 41 | CapitalizeFirstLetterFormatter(), 42 | FinalPeriodFormatter(), 43 | QuotesTypeFormatter(), 44 | ] 45 | -------------------------------------------------------------------------------- /pydocstringformatter/_formatting/_utils.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | 4 | @functools.lru_cache(maxsize=None) 5 | def is_rst_title(summary: str) -> bool: 6 | """Check if the second line of a summary is one recurring character.""" 7 | # If second line is one recurring character we're dealing with a rst title 8 | if not (last_line := summary.splitlines()[-1].lstrip()): 9 | return False 10 | return last_line.count(last_line[0]) == len(last_line) 11 | -------------------------------------------------------------------------------- /pydocstringformatter/_formatting/formatters_default.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | import textwrap 5 | import tokenize 6 | from typing import Literal 7 | 8 | from pydocstringformatter._formatting import _utils 9 | from pydocstringformatter._formatting.base import ( 10 | StringAndQuotesFormatter, 11 | StringFormatter, 12 | SummaryFormatter, 13 | ) 14 | 15 | 16 | class BeginningQuotesFormatter(StringFormatter): 17 | """Fix the position of the opening quotes.""" 18 | 19 | name = "beginning-quotes" 20 | potential_single_line = re.compile( 21 | r""" 22 | ['"]{1,3} # 3 opening quotes 23 | \n\s*.+ # A line with any length of characters 24 | \n\s* # A line with only whitespace 25 | ['"]{1,3} # 3 ending quote 26 | """, 27 | re.X, 28 | ) 29 | """Regex pattern to match against a potential single line docstring.""" 30 | 31 | def treat_string(self, tokeninfo: tokenize.TokenInfo, _: int) -> str: 32 | new_string = tokeninfo.string 33 | if new_string[3] == "\n": 34 | if ( 35 | new_string.count("\n") == 1 # Single line docstring 36 | or self.config.summary_quotes_same_line # Config for multi-line 37 | or self.potential_single_line.match(new_string) # Potential single line 38 | ): 39 | new_string = re.sub(r"\n *", "", new_string, 1) 40 | return new_string 41 | 42 | 43 | class CapitalizeFirstLetterFormatter(StringFormatter): 44 | """Capitalize the first letter of the docstring if appropriate.""" 45 | 46 | name = "capitalize-first-letter" 47 | first_letter_re = re.compile(r"""['"]{1,3}\s*(\w)""", re.DOTALL) 48 | 49 | def treat_string(self, tokeninfo: tokenize.TokenInfo, _: int) -> str: 50 | new_string = None 51 | if match := self.first_letter_re.match(tokeninfo.string): 52 | first_letter = match.end() - 1 53 | new_string = ( 54 | tokeninfo.string[:first_letter] 55 | + tokeninfo.string[first_letter].upper() 56 | + tokeninfo.string[first_letter + 1 :] 57 | ) 58 | return new_string or tokeninfo.string 59 | 60 | 61 | class LineWrapperFormatter(SummaryFormatter): 62 | """Linewrap the docstring by the pre-defined line length.""" 63 | 64 | name = "linewrap-full-docstring" 65 | optional = True 66 | 67 | def treat_summary( 68 | self, 69 | summary: str, 70 | indent_length: int, 71 | quotes_length: Literal[1, 3], 72 | description_exists: bool, 73 | ) -> str: 74 | """Wrap the summary of a docstring.""" 75 | 76 | line_length = self.config.max_line_length 77 | 78 | # Without a description we need to consider the length including closing quotes 79 | if not description_exists: 80 | # Calculate length without the ending quotes 81 | length_without_ending = indent_length + quotes_length + len(summary) 82 | 83 | # If potential length is less than line length we need to consider ending 84 | # quotes as well for the line length 85 | if length_without_ending < line_length: 86 | # We subtract one more because we don't want a new line with just the 87 | # ending quotes 88 | line_length -= quotes_length + 1 89 | 90 | if not (summary_lines := summary.splitlines()): 91 | summary_lines = [""] 92 | 93 | new_summary = "\n".join( 94 | textwrap.wrap( 95 | summary_lines[0], 96 | width=line_length, 97 | initial_indent=" " * (indent_length + quotes_length), 98 | subsequent_indent=" " * indent_length, 99 | replace_whitespace=True, 100 | ) 101 | )[indent_length + quotes_length :] 102 | 103 | if len(summary_lines) > 1: 104 | for line in summary_lines[1:]: 105 | new_summary += "\n" 106 | new_summary += "\n".join( 107 | textwrap.wrap( 108 | line, 109 | width=line_length, 110 | subsequent_indent=" " * indent_length, 111 | replace_whitespace=True, 112 | ) 113 | ) 114 | 115 | return new_summary 116 | 117 | 118 | class ClosingQuotesFormatter(StringFormatter): 119 | """Fix the position of the closing quotes.""" 120 | 121 | name = "closing-quotes" 122 | 123 | def treat_string(self, tokeninfo: tokenize.TokenInfo, _: int) -> str: 124 | """Fix the position of end quotes for multi-line docstrings.""" 125 | new_string = tokeninfo.string 126 | if "\n" not in new_string: 127 | # Not a multiline docstring, nothing to do 128 | return new_string 129 | good_end = f"{(tokeninfo.start[1]) * ' '}{(new_string[0]) * 3}" 130 | split_string = new_string.split("\n") 131 | 132 | # Add new line with only quotes 133 | if not new_string.endswith("\n" + good_end): 134 | new_string = new_string[:-3] + "\n" + good_end 135 | # Remove line with only quotes for potential single line string 136 | elif len(split_string) == 2 and split_string[-1] == good_end: 137 | new_string = "\n".join(split_string[:-1]) + tokeninfo.string[0] * 3 138 | return new_string 139 | 140 | 141 | class FinalPeriodFormatter(SummaryFormatter): 142 | """Add a period to the end of single line docstrings and summaries.""" 143 | 144 | name = "final-period" 145 | END_OF_SENTENCE_PUNCTUATION = {".", "?", "!", "‽", ":", ";"} 146 | 147 | def treat_summary( 148 | self, 149 | summary: str, 150 | indent_length: int, 151 | quotes_length: Literal[1, 3], 152 | description_exists: bool, 153 | ) -> str: 154 | """Add a period to the end of single-line docstrings and summaries.""" 155 | if not summary: 156 | return summary 157 | 158 | if summary[-1] in self.END_OF_SENTENCE_PUNCTUATION: 159 | return summary 160 | 161 | if _utils.is_rst_title(summary): 162 | return summary 163 | 164 | return summary + "." 165 | 166 | 167 | class StripWhitespacesFormatter(StringAndQuotesFormatter): 168 | """Strip 1) docstring start, 2) docstring end and 3) end of line.""" 169 | 170 | name = "strip-whitespaces" 171 | 172 | def treat_string( 173 | self, 174 | tokeninfo: tokenize.TokenInfo, 175 | indent_length: int, 176 | quotes: str, 177 | quotes_length: Literal[1, 3], 178 | ) -> str: 179 | """Strip whitespaces.""" 180 | lines = tokeninfo.string[quotes_length:-quotes_length].split("\n") 181 | new_lines: list[str] = [] 182 | 183 | for index, line in enumerate(lines): 184 | if line == "": 185 | # Remove double white lines 186 | if index and lines[index - 1] == "": 187 | continue 188 | 189 | # On the first line strip from both sides 190 | if index == 0: # pylint: disable=compare-to-zero 191 | new_lines.append(line.lstrip().rstrip()) 192 | 193 | # Check last line 194 | elif index == len(lines) - 1: 195 | # If completely whitespace, just return the indent_length 196 | if line.count(" ") == len(line): 197 | new_lines.append(indent_length * " ") 198 | else: 199 | new_lines.append(line) 200 | 201 | # Else, only strip right side 202 | else: 203 | new_lines.append(line.rstrip()) 204 | 205 | # Remove a final white line 206 | if len(new_lines) > 3 and new_lines[-2] == "": 207 | new_lines.pop(-2) 208 | 209 | return quotes + "\n".join(new_lines) + quotes 210 | 211 | 212 | class QuotesTypeFormatter(StringAndQuotesFormatter): 213 | """Change all opening and closing quotes to be triple quotes.""" 214 | 215 | name = "quotes-type" 216 | 217 | def treat_string( 218 | self, 219 | tokeninfo: tokenize.TokenInfo, 220 | _: int, 221 | __: str, 222 | quotes_length: Literal[1, 3], 223 | ) -> str: 224 | """Change all opening and closing quotes if necessary.""" 225 | return f'"""{tokeninfo.string[quotes_length:-quotes_length]}"""' 226 | -------------------------------------------------------------------------------- /pydocstringformatter/_formatting/formatters_numpydoc.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections import OrderedDict 4 | 5 | from pydocstringformatter._formatting.base import NumpydocSectionFormatter 6 | 7 | 8 | class NumpydocSectionOrderingFormatter(NumpydocSectionFormatter): 9 | """Change section order to match numpydoc guidelines.""" 10 | 11 | name = "numpydoc-section-order" 12 | 13 | numpydoc_section_order = ( 14 | "Summary", 15 | "Parameters", 16 | "Attributes", 17 | "Methods", 18 | "Returns", 19 | "Yields", 20 | "Receives", 21 | "Other Parameters", 22 | "Raises", 23 | "Warns", 24 | "Warnings", 25 | "See Also", 26 | "Notes", 27 | "References", 28 | "Examples", 29 | ) 30 | 31 | def treat_sections( 32 | self, sections: OrderedDict[str, list[str]] 33 | ) -> OrderedDict[str, list[str]]: 34 | """Sort the numpydoc sections into the numpydoc order.""" 35 | for sec_name in reversed(self.numpydoc_section_order): 36 | try: 37 | sections.move_to_end(sec_name, last=False) 38 | except KeyError: 39 | pass 40 | return sections 41 | 42 | 43 | class NumpydocNameColonTypeFormatter(NumpydocSectionFormatter): 44 | """Ensure proper spacing around the colon separating names from types.""" 45 | 46 | name = "numpydoc-name-type-spacing" 47 | 48 | numpydoc_sections_with_parameters = ( 49 | "Parameters", 50 | "Attributes", 51 | "Returns", 52 | "Yields", 53 | "Receives", 54 | "Other Parameters", 55 | "See Also", 56 | ) 57 | 58 | def treat_sections( 59 | self, sections: OrderedDict[str, list[str]] 60 | ) -> OrderedDict[str, list[str]]: 61 | """Ensure proper spacing around the colon separating names from types.""" 62 | for section_name, section_lines in sections.items(): 63 | if section_name in self.numpydoc_sections_with_parameters: 64 | # Any section that gets here has a line of hyphens 65 | initial_indent = section_lines[1].index("-") 66 | for index, line in enumerate(section_lines): 67 | if ( 68 | # There is content on this line (at least the initial indent) 69 | len(line) > initial_indent 70 | # and the first character after the indent for 71 | # the docstring is a name, not an additional 72 | # indent indicating a description rather than 73 | # a line with name and type 74 | and not line[initial_indent].isspace() 75 | # and there is a colon to separate the name 76 | # from the type (functions returning only one 77 | # thing don't put a name in their "Returns" 78 | # section) 79 | and ":" in line 80 | ): 81 | line_name, line_type = line.split(":", 1) 82 | if line_type: 83 | # Avoid adding trailing whitespace 84 | # Colon ending first line is suggested for long 85 | # "See Also" links 86 | section_lines[index] = ( 87 | f"{line_name.rstrip():s} : {line_type.lstrip():s}" 88 | ) 89 | return sections 90 | 91 | 92 | class NumpydocSectionSpacingFormatter(NumpydocSectionFormatter): 93 | """Ensure proper spacing between sections.""" 94 | 95 | name = "numpydoc-section-spacing" 96 | 97 | def treat_sections( 98 | self, sections: OrderedDict[str, list[str]] 99 | ) -> OrderedDict[str, list[str]]: 100 | """Ensure proper spacing between sections.""" 101 | for section_lines in sections.values(): 102 | last_line = section_lines[-1] 103 | if not (last_line == "" or last_line.isspace()) and len(sections) > 1: 104 | section_lines.append("") 105 | return sections 106 | 107 | 108 | class NumpydocSectionHyphenLengthFormatter(NumpydocSectionFormatter): 109 | """Ensure hyphens after section header lines are proper length.""" 110 | 111 | name = "numpydoc-section-hyphen-length" 112 | 113 | def treat_sections( 114 | self, sections: OrderedDict[str, list[str]] 115 | ) -> OrderedDict[str, list[str]]: 116 | """Ensure section header lines are proper length.""" 117 | first_section = True 118 | for section_name, section_lines in sections.items(): 119 | if section_name != "Summary": 120 | # Skip the summary, deprecation warning and extended 121 | # summary. They have neither a section header nor the 122 | # line of hyphens after it. 123 | indent_length = section_lines[1].index("-") 124 | section_lines[1] = " " * indent_length + "-" * len(section_name) 125 | if first_section: 126 | # If the second line were not hyphens, the section name 127 | # would be summary. This assumes triple quotes, but that 128 | # seems fine for a multi-line docstring. 129 | section_lines[1] = f"{section_lines[1]:s}---" 130 | first_section = False 131 | return sections 132 | -------------------------------------------------------------------------------- /pydocstringformatter/_formatting/formatters_pep257.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | from typing import Literal 5 | 6 | from pydocstringformatter._formatting import _utils 7 | from pydocstringformatter._formatting.base import SummaryFormatter 8 | 9 | 10 | class SplitSummaryAndDocstringFormatter(SummaryFormatter): 11 | """Split the summary and body of a docstring based on a period and max length. 12 | 13 | The maximum length of a summary can be set with the --max-summary-lines option. 14 | """ 15 | 16 | name = "split-summary-body" 17 | 18 | style = ["pep257"] 19 | 20 | end_of_sentence_period = re.compile( 21 | r""" 22 | (? str: 38 | """Split a summary and body if there is a period after the summary.""" 39 | new_summary = None 40 | 41 | if not summary: 42 | return summary 43 | 44 | if _utils.is_rst_title(summary): 45 | return summary 46 | 47 | # Try to split on period 48 | if match := re.search(self.end_of_sentence_period, summary): 49 | index = match.start() 50 | 51 | if summary[:index].count("\n") < self.config.max_summary_lines: 52 | if len(summary) == index + 1: 53 | new_summary = summary 54 | 55 | # Handle summaries with more text on same line after the period 56 | elif summary[index + 1] == " ": 57 | new_summary = ( 58 | summary[:index] 59 | + f"\n\n{' ' * indent_length}" 60 | + summary[index + 2 :] 61 | ) 62 | 63 | # Handle summaries that end with a period and a direct new line 64 | elif summary[index + 1] == "\n": 65 | new_summary = summary[:index] + ".\n\n" + summary[index + 2 :] 66 | 67 | # Try to split on max length 68 | if not new_summary and summary.count("\n") > self.config.max_summary_lines - 1: 69 | lines = summary.splitlines() 70 | new_summary = ( 71 | "\n".join(lines[: self.config.max_summary_lines]) 72 | + "\n\n" 73 | + "\n".join(lines[self.config.max_summary_lines :]) 74 | ) 75 | 76 | return new_summary or summary 77 | -------------------------------------------------------------------------------- /pydocstringformatter/_testutils/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import contextlib 4 | import logging 5 | from pathlib import Path 6 | from types import TracebackType 7 | 8 | import pytest 9 | 10 | from pydocstringformatter import run_docstring_formatter 11 | from pydocstringformatter._formatting import Formatter 12 | from pydocstringformatter._testutils.example_formatters import ( 13 | AddBFormatter, 14 | MakeAFormatter, 15 | MakeBFormatter, 16 | ) 17 | 18 | LOGGER = logging.getLogger(__name__) 19 | 20 | 21 | class FormatterAsserter(contextlib.AbstractContextManager): # type: ignore[type-arg] 22 | """ContextManager used to assert that a Formatter does something on a docstring. 23 | 24 | Also permit to check that nothing happens if it's deactivated. 25 | """ 26 | 27 | def __init__( 28 | self, 29 | docstring: str, 30 | formatters: list[Formatter], 31 | capsys: pytest.CaptureFixture[str], 32 | tmp_path: Path, 33 | ) -> None: 34 | self.formatters = formatters 35 | file_name = "_".join([f.name for f in self.formatters]) 36 | self.file_to_format = tmp_path / f"test_{file_name}.py" 37 | self.file_to_format.write_text(docstring) 38 | self.capsys = capsys 39 | names = [f"'{f.name}'" for f in formatters] 40 | verb = "is" if len(names) == 1 else "are" 41 | self.assert_msg = f""" 42 | {{}} was modified but {', '.join(names)} {verb} {{}}. 43 | Temp file is '{self.file_to_format}' 44 | """ 45 | 46 | def __enter__(self) -> FormatterAsserter: 47 | return self 48 | 49 | @staticmethod 50 | def __launch(commands: list[str]) -> None: 51 | """Launch pydocstringformatter while logging for easier debugging.""" 52 | run_docstring_formatter(commands) 53 | LOGGER.info("Launching 'pydocstringformatter' with: %s", commands) 54 | 55 | def assert_format_when_activated(self) -> None: 56 | """Assert that the formatter does something when activated.""" 57 | msg = self.assert_msg.format("Nothing", "activated") 58 | self.__launch( 59 | [str(self.file_to_format)] + [f.activate_option for f in self.formatters] 60 | ) 61 | out, err = self.capsys.readouterr() 62 | assert not err 63 | assert "Nothing to do!" not in out, msg 64 | expected = ["---", "@@", "+++"] 65 | assert all(e in out for e in expected), msg 66 | 67 | def assert_no_change_when_deactivated(self) -> None: 68 | """Assert that the formatter does nothing when deactivated.""" 69 | self.__launch( 70 | [str(self.file_to_format)] + [f.deactivate_option for f in self.formatters] 71 | ) 72 | out, err = self.capsys.readouterr() 73 | assert not err 74 | assert "Nothing to do!" in out, self.assert_msg.format( 75 | "Something", "deactivated" 76 | ) 77 | 78 | def __exit__( 79 | self, 80 | exc_type: type[BaseException] | None, 81 | exc_val: BaseException | None, 82 | exc_tb: TracebackType | None, 83 | ) -> None: 84 | return None 85 | 86 | 87 | __all__ = ["FormatterAsserter", "MakeAFormatter", "MakeBFormatter", "AddBFormatter"] 88 | -------------------------------------------------------------------------------- /pydocstringformatter/_testutils/example_formatters.py: -------------------------------------------------------------------------------- 1 | import tokenize 2 | 3 | from pydocstringformatter._formatting import Formatter 4 | 5 | 6 | class MakeAFormatter(Formatter): 7 | """A formatter that makes Bs into As.""" 8 | 9 | name = "make-a-formatter" 10 | style = ["default"] 11 | 12 | def treat_token(self, tokeninfo: tokenize.TokenInfo) -> tokenize.TokenInfo: 13 | """Replace Bs with As.""" 14 | token_dict = tokeninfo._asdict() 15 | token_dict["string"] = token_dict["string"].replace("B", "A") 16 | return type(tokeninfo)(**token_dict) 17 | 18 | 19 | class MakeBFormatter(Formatter): 20 | """A formatter that makes As into Bs.""" 21 | 22 | name = "make-b-formatter" 23 | style = ["default"] 24 | 25 | def treat_token(self, tokeninfo: tokenize.TokenInfo) -> tokenize.TokenInfo: 26 | """Replace As with Bs.""" 27 | token_dict = tokeninfo._asdict() 28 | token_dict["string"] = token_dict["string"].replace("A", "B") 29 | return type(tokeninfo)(**token_dict) 30 | 31 | 32 | class AddBFormatter(Formatter): 33 | """A formatter that adds Bs.""" 34 | 35 | name = "add-b-formatter" 36 | style = ["default"] 37 | 38 | def treat_token(self, tokeninfo: tokenize.TokenInfo) -> tokenize.TokenInfo: 39 | """Add a B to the end of the string.""" 40 | token_dict = tokeninfo._asdict() 41 | token_dict["string"] += "B" 42 | return type(tokeninfo)(**token_dict) 43 | -------------------------------------------------------------------------------- /pydocstringformatter/_testutils/primer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanielNoord/pydocstringformatter/60e016d59f4b02cdd07a0f1a9492b807be76ed85/pydocstringformatter/_testutils/primer/__init__.py -------------------------------------------------------------------------------- /pydocstringformatter/_testutils/primer/const.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | PRIMER_DIRECTORY_PATH = ( 4 | Path(__file__).parent.parent.parent.parent / ".pydocstringformatter_primer_tests" 5 | ) 6 | """Directory to store anything primer related in.""" 7 | 8 | DIFF_OUTPUT = PRIMER_DIRECTORY_PATH / "fulldiff.txt" 9 | """Diff output file location.""" 10 | -------------------------------------------------------------------------------- /pydocstringformatter/_testutils/primer/packages.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import shutil 5 | from dataclasses import dataclass 6 | from pathlib import Path 7 | 8 | import git 9 | 10 | from pydocstringformatter._testutils.primer.const import PRIMER_DIRECTORY_PATH 11 | 12 | 13 | @dataclass 14 | class PackageToPrime: 15 | """Represents data about a package to be tested during primer tests.""" 16 | 17 | url: str 18 | """URL of the repository to clone.""" 19 | 20 | branch: str 21 | """Branch of the repository to clone.""" 22 | 23 | directories: list[str] 24 | """Directories within the repository to run the program over.""" 25 | 26 | arguments: list[str] 27 | """List of arguments to pass when priming the package.""" 28 | 29 | @property 30 | def clone_directory(self) -> Path: 31 | """Directory to clone repository into.""" 32 | clone_name = "/".join(self.url.split("/")[-2:]).replace(".git", "") 33 | return PRIMER_DIRECTORY_PATH / clone_name 34 | 35 | @property 36 | def paths_to_lint(self) -> list[str]: 37 | """The paths we need to run against.""" 38 | return [str(self.clone_directory / path) for path in self.directories] 39 | 40 | def lazy_clone(self) -> None: 41 | """Clone the repo or pull any new commits. 42 | 43 | # TODO(#80): Allow re-using an already cloned repistory instead of removing it 44 | Currently this is therefore not really 'lazy'. 45 | """ 46 | logging.info("Lazy cloning %s", self.url) 47 | 48 | if self.clone_directory.exists(): 49 | shutil.rmtree(self.clone_directory) 50 | 51 | options: dict[str, str | int] = { 52 | "url": self.url, 53 | "to_path": str(self.clone_directory), 54 | "branch": self.branch, 55 | "depth": 1, 56 | } 57 | git.Repo.clone_from(**options) 58 | 59 | 60 | PACKAGES = { 61 | "adventofcode": PackageToPrime( 62 | "https://github.com/DanielNoord/adventofcode", 63 | "main", 64 | ["."], 65 | [], 66 | ), 67 | "ProjectInventarisGezantschappen": PackageToPrime( 68 | "https://github.com/DanielNoord/ProjectInventarisGezantschappen", 69 | "main", 70 | ["python"], 71 | [], 72 | ), 73 | "pylint": PackageToPrime( 74 | "https://github.com/PyCQA/pylint", 75 | "main", 76 | ["pylint"], 77 | ["--max-summary-lines=2"], 78 | ), 79 | "pydocstringformatter": PackageToPrime( 80 | "https://github.com/DanielNoord/pydocstringformatter", 81 | "main", 82 | ["pydocstringformatter"], 83 | [], 84 | ), 85 | "pylint-pytest-plugin": PackageToPrime( 86 | "https://github.com/DanielNoord/pylint-pytest-plugin", 87 | "main", 88 | ["pylint_pytest_plugin"], 89 | ["--linewrap-full-docstring"], 90 | ), 91 | } 92 | -------------------------------------------------------------------------------- /pydocstringformatter/_testutils/primer/primer.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import subprocess 4 | import sys 5 | from pathlib import Path 6 | 7 | from pydocstringformatter._testutils.primer.const import DIFF_OUTPUT 8 | from pydocstringformatter._testutils.primer.packages import PACKAGES, PackageToPrime 9 | 10 | 11 | def fix_diff(output: str, package: PackageToPrime) -> str: 12 | """Make the diff more readable and useful.""" 13 | new_output: list[str] = [] 14 | 15 | for index, line in enumerate(output.splitlines()): 16 | if line.startswith("--- "): 17 | if index: 18 | new_output.append("```\n") 19 | 20 | link = line.replace("--- ../.pydocstringformatter_primer_tests/", "") 21 | file = "/".join(link.split("/")[2:]) 22 | 23 | new_output.append(f"{package.url}/blob/{package.branch}/{file}") 24 | new_output.append("```diff") 25 | 26 | new_output.append(line) 27 | 28 | return "\n".join(new_output) 29 | 30 | 31 | def run_prepare() -> None: 32 | """Prepare everything for the primer to be run. 33 | 34 | This clones all packages that need to be 'primed' and 35 | does any other necessary setup. 36 | """ 37 | for package in PACKAGES.values(): 38 | package.lazy_clone() 39 | 40 | print("## Preparation of primer successful!") 41 | 42 | 43 | def run_step_one() -> None: 44 | """Run program over all packages in write mode. 45 | 46 | Runs the program in write mode over all packages that need 47 | to be 'primed'. This should be run when the local repository 48 | is checked out to upstream/main. 49 | """ 50 | for package in PACKAGES.values(): 51 | subprocess.run( 52 | [sys.executable, "-m", "pydocstringformatter", "-w"] 53 | + package.paths_to_lint 54 | + package.arguments, 55 | cwd=Path(__file__).parent.parent.parent, 56 | capture_output=True, 57 | text=True, 58 | check=False, 59 | ) 60 | 61 | print("## Step one of primer successful!") 62 | 63 | 64 | def run_step_two() -> None: 65 | """Run program over all packages and store the diff. 66 | 67 | This reiterates over all packages that need to be 'primed', 68 | runs the program in diff mode and stores the output to file. 69 | """ 70 | output: dict[str, str] = {} 71 | 72 | for name, package in PACKAGES.items(): 73 | process = subprocess.run( 74 | [sys.executable, "-m", "pydocstringformatter"] 75 | + package.paths_to_lint 76 | + package.arguments, 77 | cwd=Path(__file__).parent.parent.parent, 78 | capture_output=True, 79 | text=True, 80 | check=False, 81 | ) 82 | output[name] = fix_diff(process.stdout, package) 83 | 84 | final_output = "" 85 | for name, string in output.items(): 86 | if string.startswith("Nothing to do!"): 87 | continue 88 | final_output += f"**{name}:**\n\n{string}\n\n" 89 | 90 | with open(DIFF_OUTPUT, "w", encoding="utf-8") as file: 91 | file.write(final_output) 92 | 93 | print("## Step two of primer successful!") 94 | 95 | 96 | def run_primer() -> None: 97 | """Run the primer test.""" 98 | args = sys.argv[1:] 99 | 100 | if "--prepare" in args: 101 | run_prepare() 102 | elif "--step-one" in args: 103 | run_step_one() 104 | elif "--step-two" in args: 105 | run_step_two() 106 | 107 | 108 | if __name__ == "__main__": 109 | run_primer() 110 | -------------------------------------------------------------------------------- /pydocstringformatter/_utils/__init__.py: -------------------------------------------------------------------------------- 1 | from pydocstringformatter._utils.exceptions import ( 2 | ParsingError, 3 | PydocstringFormatterError, 4 | TomlParsingError, 5 | UnstableResultError, 6 | ) 7 | from pydocstringformatter._utils.file_diference import compare_formatters, generate_diff 8 | from pydocstringformatter._utils.find_docstrings import is_docstring 9 | from pydocstringformatter._utils.find_python_file import find_python_files 10 | from pydocstringformatter._utils.issue_template import create_gh_issue_template 11 | from pydocstringformatter._utils.output import print_to_console, sys_exit 12 | 13 | __all__ = [ 14 | "find_python_files", 15 | "compare_formatters", 16 | "generate_diff", 17 | "is_docstring", 18 | "ParsingError", 19 | "PydocstringFormatterError", 20 | "TomlParsingError", 21 | "UnstableResultError", 22 | "create_gh_issue_template", 23 | "print_to_console", 24 | "sys_exit", 25 | ] 26 | -------------------------------------------------------------------------------- /pydocstringformatter/_utils/exceptions.py: -------------------------------------------------------------------------------- 1 | class PydocstringFormatterError(Exception): 2 | """Base class to inherit all exceptions from.""" 3 | 4 | 5 | class ParsingError(PydocstringFormatterError): 6 | """Raised when we the parsing of a file failed.""" 7 | 8 | 9 | class UnrecognizedOption(PydocstringFormatterError): 10 | """Raised when an option is encountered that is not recognized.""" 11 | 12 | 13 | class TomlParsingError(PydocstringFormatterError): 14 | """Raised when there are errors with the parsing of the toml file.""" 15 | 16 | 17 | class UnstableResultError(PydocstringFormatterError): 18 | """Raised when the result of the formatting is unstable.""" 19 | -------------------------------------------------------------------------------- /pydocstringformatter/_utils/file_diference.py: -------------------------------------------------------------------------------- 1 | import difflib 2 | import tokenize 3 | 4 | from pydocstringformatter._formatting import Formatter 5 | 6 | 7 | def generate_diff(old: str, new: str, filename: str) -> str: 8 | """Generate a printable diff for two strings of sourcecode.""" 9 | return ( 10 | "\n".join( 11 | difflib.unified_diff( 12 | old.split("\n"), 13 | new.split("\n"), 14 | fromfile=filename, 15 | tofile=filename, 16 | lineterm="", 17 | ) 18 | ) 19 | + "\n" 20 | ) 21 | 22 | 23 | def compare_formatters( 24 | token: tokenize.TokenInfo, 25 | formatter_1: Formatter, 26 | formatter_2: Formatter, 27 | title_extra: str = "", 28 | ) -> str: 29 | """Modifies a token with two formatters and returns the difference.""" 30 | out_t1 = formatter_1.treat_token(token) 31 | out_t2 = formatter_2.treat_token(out_t1) 32 | 33 | title = f"{formatter_1.name} vs {formatter_2.name}" 34 | if title_extra: 35 | title += f" {title_extra}" 36 | return generate_diff(out_t1.string, out_t2.string, title) 37 | -------------------------------------------------------------------------------- /pydocstringformatter/_utils/find_docstrings.py: -------------------------------------------------------------------------------- 1 | import token 2 | import tokenize 3 | 4 | PREVIOUS_TOKEN_MARKERS = (token.INDENT, token.ENDMARKER, token.NEWLINE) 5 | 6 | 7 | def is_docstring( 8 | tokeninfo: tokenize.TokenInfo, previous_token: tokenize.TokenInfo 9 | ) -> bool: 10 | """Check if a token represents a docstring.""" 11 | if ( 12 | tokeninfo.type == token.STRING 13 | and ( 14 | previous_token.type in PREVIOUS_TOKEN_MARKERS 15 | or previous_token.type == token.NL 16 | and not tokeninfo.start[1] 17 | ) 18 | and tokeninfo.line.strip().startswith(("'", '"')) 19 | ): 20 | return True 21 | return False 22 | -------------------------------------------------------------------------------- /pydocstringformatter/_utils/find_python_file.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import glob 4 | import os 5 | from pathlib import Path 6 | 7 | 8 | def is_python_file(filename: str) -> bool: 9 | """Check if file is a Python file.""" 10 | return filename.endswith(".py") 11 | 12 | 13 | def find_python_files( 14 | filenames: list[str], exclude: list[str], recursive: bool = True 15 | ) -> list[Path]: 16 | """Find all python files for a list of potential file and directory names.""" 17 | pathnames: list[Path] = [] 18 | 19 | to_exclude: set[str] = set() 20 | for exclude_glob in exclude: 21 | to_exclude.update(set(glob.iglob(exclude_glob, recursive=recursive))) 22 | 23 | for name in filenames: 24 | if os.path.isdir(name): 25 | if recursive: 26 | for root, _, children in os.walk(name): 27 | pathnames += [ 28 | Path(os.path.abspath(root)) / child 29 | for child in children 30 | if is_python_file(child) 31 | and str(Path(root) / child) not in to_exclude 32 | ] 33 | else: 34 | pathnames += [ 35 | file for file in Path(name).iterdir() if is_python_file(str(file)) 36 | ] 37 | elif is_python_file(name): 38 | pathnames.append(Path(name)) 39 | 40 | return sorted(pathnames) 41 | -------------------------------------------------------------------------------- /pydocstringformatter/_utils/issue_template.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import tokenize 4 | 5 | from pydocstringformatter._formatting import Formatter 6 | from pydocstringformatter._utils.file_diference import compare_formatters 7 | 8 | 9 | def create_gh_issue_template( 10 | token: tokenize.TokenInfo, formatters: dict[str, Formatter], filename: str 11 | ) -> str: 12 | """Make a template for a GitHub issue. 13 | 14 | Args: 15 | token: The token that caused the issue. 16 | formatters: The formatters that caused the issue. 17 | filename: The filename of the file that caused the issue. 18 | """ 19 | formatter_names = list(formatters) 20 | msg = "" 21 | if len(formatter_names) > 2: 22 | msg = f""" 23 | Conflicting formatters: {", ".join(formatters)} 24 | """ 25 | diff = f"Diff too intricate to compute for {len(formatter_names)} formatters." 26 | else: 27 | if len(formatter_names) == 2: 28 | msg = f""" 29 | Conflicting formatters: {" and ".join(formatter_names)} 30 | These formatters conflict with each other for: 31 | 32 | ```python 33 | {token.string} 34 | ``` 35 | """ 36 | formatter_1 = formatters[formatter_names[0]] 37 | formatter_2 = formatters[formatter_names[1]] 38 | else: 39 | msg = f""" 40 | Formatter: {formatter_names[0]} 41 | This formatter is not able to make stable changes for: 42 | 43 | ```python 44 | {token.string} 45 | ``` 46 | """ 47 | formatter_1 = formatters[formatter_names[0]] 48 | formatter_2 = formatter_1 49 | 50 | diff = compare_formatters( 51 | token, 52 | formatter_1, 53 | formatter_2, 54 | title_extra=str(filename), 55 | ) 56 | 57 | out = f""" 58 | Unfortunately an error occurred while formatting a docstring. 59 | Please help us fix this bug by opening an issue at: 60 | https://github.com/DanielNoord/pydocstringformatter/issues/new 61 | 62 | {"-" * 80} 63 | 64 | You can use the following template when you open the issue: 65 | 66 | # Description: 67 | 68 | {msg} 69 | 70 | # Diff: 71 | 72 | ```diff 73 | {diff} 74 | ``` 75 | 76 | """ 77 | 78 | return out 79 | -------------------------------------------------------------------------------- /pydocstringformatter/_utils/output.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | def encode_string(string: str) -> bytes: 5 | """Encode a string to utf-8. 6 | 7 | This can be used to circumvent the issue of the standard encoding 8 | of a windows console not being utf-8. 9 | See: https://github.com/DanielNoord/pydocstringformatter/issues/13 10 | """ 11 | return string.encode("utf-8") 12 | 13 | 14 | def print_to_console(string: str, quiet: bool) -> None: 15 | """Print a string to the console while handling edge cases. 16 | 17 | This can be used instead of print() whenever we want to 18 | print emoji's or non-ASCII characters, but also to check if we are 19 | in quiet mode. 20 | """ 21 | if not quiet: 22 | sys.stdout.buffer.write(encode_string(string)) 23 | 24 | 25 | def sys_exit(value: int, option: bool) -> None: 26 | """Sys.exit if the boolean passed says to do so.""" 27 | if option: 28 | sys.exit(value) 29 | -------------------------------------------------------------------------------- /pydocstringformatter/run.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=too-few-public-methods, protected-access 2 | """Run class.""" 3 | from __future__ import annotations 4 | 5 | import os 6 | import sys 7 | import tokenize 8 | from pathlib import Path 9 | 10 | from pydocstringformatter import __version__, _formatting, _utils 11 | from pydocstringformatter._configuration.arguments_manager import ArgumentsManager 12 | from pydocstringformatter._utils.exceptions import UnstableResultError 13 | 14 | 15 | class _Run: 16 | """Main class that represent a run of the program.""" 17 | 18 | def __init__(self, argv: list[str] | None) -> None: 19 | # Load ArgumentsManager and set its namespace as instance's config attribute 20 | self._arguments_manager = ArgumentsManager(__version__, _formatting.FORMATTERS) 21 | self.config = self._arguments_manager.namespace 22 | 23 | # Display help message if nothing is passed 24 | if not (argv := argv or sys.argv[1:]): 25 | self._arguments_manager.print_help() 26 | return 27 | 28 | # Parse options and register on formatters 29 | self._arguments_manager.parse_options(argv) 30 | for formatter in _formatting.FORMATTERS: 31 | formatter.set_config_namespace(self.config) 32 | 33 | self.enabled_formatters = self.get_enabled_formatters() 34 | self.check_files(self.config.files) 35 | 36 | # pylint: disable-next=inconsistent-return-statements 37 | def check_files(self, files: list[str]) -> None: 38 | """Find all files and perform the formatting.""" 39 | filepaths = _utils.find_python_files(files, self.config.exclude) 40 | 41 | is_changed = self.format_files(filepaths) 42 | 43 | if is_changed: # pylint: disable=consider-using-assignment-expr 44 | return _utils.sys_exit(32, self.config.exit_code) 45 | 46 | files_string = f"{len(filepaths)} " 47 | files_string += "files" if len(filepaths) != 1 else "file" 48 | _utils.print_to_console( 49 | f"Nothing to do! All docstrings in {files_string} are correct 🎉\n", 50 | self.config.quiet, 51 | ) 52 | 53 | _utils.sys_exit(0, self.config.exit_code) 54 | 55 | def format_file(self, filename: Path) -> bool: 56 | """Format a file.""" 57 | with tokenize.open(filename) as file: 58 | try: 59 | tokens = list(tokenize.generate_tokens(file.readline)) 60 | except tokenize.TokenError as exc: 61 | raise _utils.ParsingError( 62 | f"Can't parse {os.path.relpath(filename)}. Is it valid Python code?" 63 | ) from exc 64 | # Record type of newlines so we can make sure to use 65 | # the same later on. 66 | newlines = file.newlines 67 | 68 | formatted_tokens, is_changed = self.format_file_tokens(tokens, filename) 69 | 70 | if is_changed: 71 | try: 72 | filename_str = os.path.relpath(filename) 73 | except ValueError: 74 | # On Windows relpath raises ValueError's when the mounts differ 75 | filename_str = str(filename) 76 | 77 | if self.config.write: 78 | if isinstance(newlines, tuple): 79 | newlines = newlines[0] 80 | print( 81 | "Found multiple newline variants in " 82 | f"{os.path.abspath(filename_str)}. " 83 | "Using variant that occurred first.", 84 | file=sys.stderr, 85 | ) 86 | with open(filename, "w", encoding="utf-8", newline=newlines) as file: 87 | file.write(tokenize.untokenize(formatted_tokens)) 88 | _utils.print_to_console( 89 | f"Formatted {filename_str} 📖\n", self.config.quiet 90 | ) 91 | else: 92 | sys.stdout.write( 93 | _utils.generate_diff( 94 | tokenize.untokenize(tokens), 95 | tokenize.untokenize(formatted_tokens), 96 | filename_str, 97 | ) 98 | ) 99 | 100 | return is_changed 101 | 102 | def get_enabled_formatters(self) -> dict[str, _formatting.Formatter]: 103 | """Returns a dict of the enabled formatters.""" 104 | 105 | enabled = {} 106 | for formatter in _formatting.FORMATTERS: 107 | if ( 108 | "default" in formatter.style 109 | or any(i in formatter.style for i in self.config.style) 110 | ) and getattr(self.config, formatter.name): 111 | enabled[formatter.name] = formatter 112 | 113 | return enabled 114 | 115 | def format_file_tokens( 116 | self, tokens: list[tokenize.TokenInfo], filename: Path 117 | ) -> tuple[list[tokenize.TokenInfo], bool]: 118 | """Format a list of tokens. 119 | 120 | tokens: List of tokens to format. 121 | filename: Name of the file the tokens are from. 122 | 123 | Returns: 124 | A tuple containing [1] the formatted tokens in a list 125 | and [2] a boolean indicating if the tokens were changed. 126 | 127 | Raises: 128 | UnstableResultError:: 129 | If the formatters are not able to get to a stable result. 130 | It reports what formatters are still modifying the tokens. 131 | """ 132 | formatted_tokens: list[tokenize.TokenInfo] = [] 133 | is_changed = False 134 | 135 | for index, tokeninfo in enumerate(tokens): 136 | new_tokeninfo = tokeninfo 137 | 138 | if _utils.is_docstring(new_tokeninfo, tokens[index - 1]): 139 | new_tokeninfo, changers = self.apply_formatters(new_tokeninfo) 140 | is_changed = is_changed or bool(changers) 141 | 142 | # Run formatters again (3rd time) to check if the result is stable 143 | _, changers = self._apply_formatters_once( 144 | new_tokeninfo, 145 | ) 146 | 147 | if changers: 148 | conflicting_formatters = { 149 | k: v 150 | for k, v in self.enabled_formatters.items() 151 | if k in changers 152 | } 153 | template = _utils.create_gh_issue_template( 154 | new_tokeninfo, conflicting_formatters, str(filename) 155 | ) 156 | 157 | raise UnstableResultError(template) 158 | 159 | formatted_tokens.append(new_tokeninfo) 160 | 161 | return formatted_tokens, is_changed 162 | 163 | def apply_formatters( 164 | self, token: tokenize.TokenInfo 165 | ) -> tuple[tokenize.TokenInfo, set[str]]: 166 | """Apply the formatters twice to a token. 167 | 168 | Also tracks which formatters changed the token. 169 | 170 | Returns: 171 | A tuple containing: 172 | [1] the formatted token and 173 | [2] a set of formatters that changed the token. 174 | """ 175 | token, changers = self._apply_formatters_once(token) 176 | if changers: 177 | token, changers2 = self._apply_formatters_once(token) 178 | changers.update(changers2) 179 | return token, changers 180 | 181 | def _apply_formatters_once( 182 | self, token: tokenize.TokenInfo 183 | ) -> tuple[tokenize.TokenInfo, set[str]]: 184 | """Applies formatters to a token and keeps track of what changes it. 185 | 186 | token: Token to apply formatters to 187 | 188 | Returns: 189 | A tuple containing [1] the formatted token and [2] a set 190 | of formatters that changed the token. 191 | """ 192 | changers: set[str] = set() 193 | for formatter_name, formatter in self.enabled_formatters.items(): 194 | if (new_token := formatter.treat_token(token)) != token: 195 | changers.add(formatter_name) 196 | token = new_token 197 | 198 | return token, changers 199 | 200 | def format_files(self, filepaths: list[Path]) -> bool: 201 | """Format a list of files.""" 202 | is_changed = [self.format_file(file) for file in filepaths] 203 | return any(is_changed) 204 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools >= 62", 4 | "wheel >= 0.37", 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | 8 | [project] 9 | name = "pydocstringformatter" 10 | dynamic = ["version"] 11 | authors = [ 12 | {name = "Daniël van Noord", email = "13665637+DanielNoord@users.noreply.github.com"} 13 | ] 14 | description = "A tool to automatically format Python docstrings that tries to follow recommendations from PEP 8 and PEP 257." 15 | readme = "README.md" 16 | license = {text = "MIT"} 17 | classifiers = [ 18 | "Development Status :: 4 - Beta", 19 | "Environment :: Console", 20 | "Intended Audience :: Developers", 21 | "License :: OSI Approved :: MIT License", 22 | "Operating System :: OS Independent", 23 | "Programming Language :: Python", 24 | "Programming Language :: Python :: 3", 25 | "Programming Language :: Python :: 3 :: Only", 26 | "Programming Language :: Python :: 3.8", 27 | "Programming Language :: Python :: 3.9", 28 | "Programming Language :: Python :: 3.10", 29 | "Programming Language :: Python :: 3.11", 30 | "Programming Language :: Python :: 3.12", 31 | "Programming Language :: Python :: Implementation :: CPython", 32 | "Topic :: Software Development :: Quality Assurance", 33 | ] 34 | keywords = ["python", "docstring", "format"] 35 | requires-python = ">=3.8" 36 | dependencies = ["tomli>=1.1.0;python_version<'3.11'"] 37 | 38 | [project.urls] 39 | "Repository" = "https://github.com/DanielNoord/pydocstringformatter" 40 | "Releases" = "https://github.com/DanielNoord/pydocstringformatter/releases" 41 | "Bug Tracker" = "https://github.com/DanielNoord/pydocstringformatter/issues" 42 | 43 | [project.scripts] 44 | pydocstringformatter = "pydocstringformatter:run_docstring_formatter" 45 | 46 | [tool.setuptools] 47 | license-files = ["LICENSE"] 48 | 49 | [tool.setuptools.dynamic] 50 | version = {attr = "pydocstringformatter.__version__"} 51 | 52 | [tool.setuptools.packages.find] 53 | include = ["pydocstringformatter*"] 54 | 55 | [tool.pytest] 56 | testpaths = "tests" 57 | 58 | [tool.mypy] 59 | files = "pydocstringformatter,tests" 60 | exclude = "tests/data.*" 61 | strict = true 62 | show_error_codes = true 63 | enable_error_code = "ignore-without-code" 64 | 65 | [[tool.mypy.overrides]] 66 | module = ["git.*"] 67 | ignore_missing_imports = true 68 | 69 | [tool.pylint] 70 | load-plugins=[ 71 | "pylint.extensions.check_elif", 72 | "pylint.extensions.code_style", 73 | "pylint.extensions.confusing_elif", 74 | "pylint.extensions.docparams", 75 | "pylint.extensions.docstyle", 76 | "pylint.extensions.empty_comment", 77 | "pylint.extensions.for_any_all", 78 | "pylint.extensions.set_membership", 79 | "pylint.extensions.typing" 80 | ] 81 | py-version="3.8" 82 | disable = [ 83 | "missing-module-docstring", 84 | "too-few-public-methods", 85 | "duplicate-code" 86 | ] 87 | enable = [ 88 | "c-extension-no-member", 89 | ] 90 | good-names = "f" 91 | notes-rgx = 'TODO(?!\(#\d+\))' 92 | 93 | [tool.isort] 94 | profile = "black" 95 | known_third_party = ["pytest"] 96 | skip_glob = "tests/data/**" 97 | 98 | [tool.coverage.run] 99 | branch = true 100 | relative_files = true 101 | 102 | [tool.black] 103 | # Use force-exclude due to the way that pre-commit passes filenames to black 104 | # See https://github.com/psf/black/issues/1985 105 | force-exclude = ".*tests/data/.*" 106 | quiet = true 107 | -------------------------------------------------------------------------------- /requirements-coverage.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | coverage[toml]==7.8.2 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Coverage + local install of package 2 | -e . 3 | -r requirements-coverage.txt 4 | 5 | # Requirements for testing and linting 6 | pytest==8.4.0 7 | pytest-cov==6.1.1 8 | gitpython>=3 9 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture 7 | def test_file(tmp_path: Path) -> str: 8 | """A test file to be used by tests.""" 9 | filename = tmp_path / "test.py" 10 | with open(filename, "w", encoding="utf-8") as file: 11 | file.write('"""A multi-line\ndocstring."""') 12 | return str(filename) 13 | -------------------------------------------------------------------------------- /tests/data/config/exclude_match/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pydocstringformatter] 2 | exclude = "**/test_package/**" 3 | -------------------------------------------------------------------------------- /tests/data/config/exclude_match/test_package/incorrect_docstring.py: -------------------------------------------------------------------------------- 1 | """ 2 | A docstring""" 3 | -------------------------------------------------------------------------------- /tests/data/config/exclude_match_csv/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pydocstringformatter] 2 | exclude = "**/test_packages/**,**/test_package/**" 3 | -------------------------------------------------------------------------------- /tests/data/config/exclude_match_csv/test_package/incorrect_docstring.py: -------------------------------------------------------------------------------- 1 | """ 2 | A docstring""" 3 | -------------------------------------------------------------------------------- /tests/data/config/exclude_match_csv_list/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pydocstringformatter] 2 | exclude = ["**/test_package/**", "**/test_packages/**"] 3 | -------------------------------------------------------------------------------- /tests/data/config/exclude_match_csv_list/test_package/incorrect_docstring.py: -------------------------------------------------------------------------------- 1 | """ 2 | A docstring""" 3 | -------------------------------------------------------------------------------- /tests/data/config/exclude_match_inner/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pydocstringformatter] 2 | exclude = "**/inner_dir/**" 3 | -------------------------------------------------------------------------------- /tests/data/config/exclude_match_inner/test_package/inner_dir/incorrect_docstring.py: -------------------------------------------------------------------------------- 1 | """ 2 | A docstring""" 3 | -------------------------------------------------------------------------------- /tests/data/config/exclude_non_match/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pydocstringformatter] 2 | exclude = "**test_packages**" 3 | -------------------------------------------------------------------------------- /tests/data/config/exclude_non_match/test_package/incorrect_docstring.py: -------------------------------------------------------------------------------- 1 | """ 2 | A docstring""" 3 | -------------------------------------------------------------------------------- /tests/data/config/invalid_toml/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pydocstringformatter] 2 | write true 3 | -------------------------------------------------------------------------------- /tests/data/config/invalid_toml/test_package/correct_docstring.py: -------------------------------------------------------------------------------- 1 | """A docstring""" 2 | -------------------------------------------------------------------------------- /tests/data/config/no_toml/test_package/incorrect_docstring.py: -------------------------------------------------------------------------------- 1 | """ 2 | A docstring""" 3 | -------------------------------------------------------------------------------- /tests/data/config/non_existing_options/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pydocstringformatter] 2 | non_existing_option = true 3 | -------------------------------------------------------------------------------- /tests/data/config/non_existing_options/test_package/correct_docstring.py: -------------------------------------------------------------------------------- 1 | """A docstring""" 2 | -------------------------------------------------------------------------------- /tests/data/config/non_valid_toml_boolopt/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pydocstringformatter] 2 | write = false 3 | style = ["numpydoc"] 4 | strip-whitespaces = true 5 | split-summary-body = false 6 | no-numpydoc-section-hyphen-length = true 7 | -------------------------------------------------------------------------------- /tests/data/config/non_valid_toml_boolopt/test_package/numpydoc_style.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | """ 3 | A docstring""" 4 | -------------------------------------------------------------------------------- /tests/data/config/non_valid_toml_boolopt_two/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pydocstringformatter] 2 | write = false 3 | style = ["numpydoc"] 4 | strip-whitespaces = true 5 | split-summary-body = false 6 | numpydoc-section-hyphen-length = 'true' 7 | -------------------------------------------------------------------------------- /tests/data/config/non_valid_toml_boolopt_two/test_package/numpydoc_style.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | """ 3 | A docstring""" 4 | -------------------------------------------------------------------------------- /tests/data/config/valid_toml/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pydocstringformatter] 2 | write = true 3 | max-summary-lines = 2 4 | # Also test short options 5 | w = true 6 | -------------------------------------------------------------------------------- /tests/data/config/valid_toml/test_package/correct_docstring.py: -------------------------------------------------------------------------------- 1 | """A docstring.""" 2 | -------------------------------------------------------------------------------- /tests/data/config/valid_toml_boolopt/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pydocstringformatter] 2 | write = false 3 | style = ["numpydoc"] 4 | strip-whitespaces = true 5 | split-summary-body = false 6 | numpydoc-section-hyphen-length = false 7 | -------------------------------------------------------------------------------- /tests/data/config/valid_toml_boolopt/test_package/numpydoc_style.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | """ 3 | A docstring""" 4 | -------------------------------------------------------------------------------- /tests/data/config/valid_toml_numpydoc/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pydocstringformatter] 2 | write = false 3 | style = 'numpydoc' 4 | -------------------------------------------------------------------------------- /tests/data/config/valid_toml_numpydoc/test_package/numpydoc_style.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | """ 3 | A docstring""" 4 | -------------------------------------------------------------------------------- /tests/data/config/valid_toml_numpydoc_pep257/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pydocstringformatter] 2 | write = false 3 | style = ["numpydoc","pep257"] 4 | -------------------------------------------------------------------------------- /tests/data/config/valid_toml_numpydoc_pep257/test_package/numpydoc_style.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | """ 3 | A docstring""" 4 | -------------------------------------------------------------------------------- /tests/data/config/valid_toml_pep257/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pydocstringformatter] 2 | write = false 3 | style = 'pep257' 4 | -------------------------------------------------------------------------------- /tests/data/config/valid_toml_pep257/test_package/numpydoc_style.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | """ 3 | A docstring""" 4 | -------------------------------------------------------------------------------- /tests/data/config/valid_toml_two/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pydocstringformatter] 2 | write = false 3 | -------------------------------------------------------------------------------- /tests/data/config/valid_toml_two/test_package/incorrect_docstring.py: -------------------------------------------------------------------------------- 1 | """ 2 | A docstring""" 3 | -------------------------------------------------------------------------------- /tests/data/config/valid_toml_without_section/pyproject.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanielNoord/pydocstringformatter/60e016d59f4b02cdd07a0f1a9492b807be76ed85/tests/data/config/valid_toml_without_section/pyproject.toml -------------------------------------------------------------------------------- /tests/data/config/valid_toml_without_section/test_package/correct_docstring.py: -------------------------------------------------------------------------------- 1 | """A docstring""" 2 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/class_docstring.py: -------------------------------------------------------------------------------- 1 | class MyClass: 2 | """A multi-line 3 | docstring""" 4 | 5 | class InnerClass: 6 | """ 7 | A multi-line 8 | docstring""" 9 | 10 | 11 | class MyClass: 12 | """A multi-line 13 | docstring 14 | """ 15 | 16 | class InnerClass: 17 | """A multi-line 18 | docstring 19 | """ 20 | 21 | 22 | class MyClass: 23 | """ 24 | A docstring.""" 25 | 26 | class InnerClass: 27 | """ 28 | A docstring.""" 29 | 30 | 31 | class MyClass: 32 | """A docstring.""" 33 | 34 | class InnerClass: 35 | """A docstring.""" 36 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/class_docstring.py.out: -------------------------------------------------------------------------------- 1 | class MyClass: 2 | """A multi-line. 3 | 4 | docstring 5 | """ 6 | 7 | class InnerClass: 8 | """ 9 | A multi-line. 10 | 11 | docstring 12 | """ 13 | 14 | 15 | class MyClass: 16 | """A multi-line. 17 | 18 | docstring 19 | """ 20 | 21 | class InnerClass: 22 | """A multi-line. 23 | 24 | docstring 25 | """ 26 | 27 | 28 | class MyClass: 29 | """A docstring.""" 30 | 31 | class InnerClass: 32 | """A docstring.""" 33 | 34 | 35 | class MyClass: 36 | """A docstring.""" 37 | 38 | class InnerClass: 39 | """A docstring.""" 40 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/function_docstrings.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | """ 3 | A docstring.""" 4 | 5 | def inner_func(): 6 | """ 7 | A docstring.""" 8 | 9 | 10 | def func(): 11 | """ 12 | A multi-line 13 | docstring 14 | """ 15 | 16 | def inner_func(): 17 | """A multi-line 18 | docstring""" 19 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/function_docstrings.py.out: -------------------------------------------------------------------------------- 1 | def func(): 2 | """A docstring.""" 3 | 4 | def inner_func(): 5 | """A docstring.""" 6 | 7 | 8 | def func(): 9 | """ 10 | A multi-line. 11 | 12 | docstring 13 | """ 14 | 15 | def inner_func(): 16 | """A multi-line. 17 | 18 | docstring 19 | """ 20 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/module_docstring.py: -------------------------------------------------------------------------------- 1 | """ 2 | A docstring.""" 3 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/module_docstring.py.out: -------------------------------------------------------------------------------- 1 | """A docstring.""" 2 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/multi_line_no_push/class_docstring.py: -------------------------------------------------------------------------------- 1 | class MyClass: 2 | """ 3 | A multi-line 4 | docstring""" 5 | 6 | class InnerClass: 7 | """ 8 | A multi-line 9 | docstring""" 10 | 11 | 12 | class MyClass: 13 | """A multi-line 14 | docstring 15 | """ 16 | 17 | class InnerClass: 18 | """A multi-line 19 | docstring 20 | """ 21 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/multi_line_no_push/class_docstring.py.out: -------------------------------------------------------------------------------- 1 | class MyClass: 2 | """ 3 | A multi-line. 4 | 5 | docstring 6 | """ 7 | 8 | class InnerClass: 9 | """ 10 | A multi-line. 11 | 12 | docstring 13 | """ 14 | 15 | 16 | class MyClass: 17 | """A multi-line. 18 | 19 | docstring 20 | """ 21 | 22 | class InnerClass: 23 | """A multi-line. 24 | 25 | docstring 26 | """ 27 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/multi_line_no_push/function_docstrings.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | """ 3 | A multi-line 4 | docstring 5 | """ 6 | 7 | def inner_func(): 8 | """ 9 | A multi-line 10 | docstring""" 11 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/multi_line_no_push/function_docstrings.py.out: -------------------------------------------------------------------------------- 1 | def func(): 2 | """ 3 | A multi-line. 4 | 5 | docstring 6 | """ 7 | 8 | def inner_func(): 9 | """ 10 | A multi-line. 11 | 12 | docstring 13 | """ 14 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/multi_line_no_push/module_docstring.py: -------------------------------------------------------------------------------- 1 | """ 2 | Summary. 3 | 4 | Body 5 | """ 6 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/multi_line_no_push/module_docstring.py.out: -------------------------------------------------------------------------------- 1 | """ 2 | Summary. 3 | 4 | Body 5 | """ 6 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/multi_line_no_push/variable_docstring.py: -------------------------------------------------------------------------------- 1 | MYVAR = 1 2 | """ 3 | A multi-line 4 | docstring""" 5 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/multi_line_no_push/variable_docstring.py.out: -------------------------------------------------------------------------------- 1 | MYVAR = 1 2 | """ 3 | A multi-line. 4 | 5 | docstring 6 | """ 7 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/multi_line_push/class_docstring.args: -------------------------------------------------------------------------------- 1 | --summary-quotes-same-line 2 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/multi_line_push/class_docstring.py: -------------------------------------------------------------------------------- 1 | class MyClass: 2 | """ 3 | A multi-line 4 | docstring""" 5 | 6 | class InnerClass: 7 | """ 8 | A multi-line 9 | docstring""" 10 | 11 | 12 | class MyClass: 13 | """A multi-line 14 | docstring 15 | """ 16 | 17 | class InnerClass: 18 | """A multi-line 19 | docstring 20 | """ 21 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/multi_line_push/class_docstring.py.out: -------------------------------------------------------------------------------- 1 | class MyClass: 2 | """A multi-line. 3 | 4 | docstring 5 | """ 6 | 7 | class InnerClass: 8 | """A multi-line. 9 | 10 | docstring 11 | """ 12 | 13 | 14 | class MyClass: 15 | """A multi-line. 16 | 17 | docstring 18 | """ 19 | 20 | class InnerClass: 21 | """A multi-line. 22 | 23 | docstring 24 | """ 25 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/multi_line_push/function_docstrings.args: -------------------------------------------------------------------------------- 1 | --summary-quotes-same-line 2 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/multi_line_push/function_docstrings.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | """ 3 | A multi-line 4 | docstring 5 | """ 6 | 7 | def inner_func(): 8 | """ 9 | A multi-line 10 | docstring""" 11 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/multi_line_push/function_docstrings.py.out: -------------------------------------------------------------------------------- 1 | def func(): 2 | """A multi-line. 3 | 4 | docstring 5 | """ 6 | 7 | def inner_func(): 8 | """A multi-line. 9 | 10 | docstring 11 | """ 12 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/multi_line_push/module_docstring.args: -------------------------------------------------------------------------------- 1 | --summary-quotes-same-line 2 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/multi_line_push/module_docstring.py: -------------------------------------------------------------------------------- 1 | """ 2 | Summary. 3 | 4 | Body 5 | """ 6 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/multi_line_push/module_docstring.py.out: -------------------------------------------------------------------------------- 1 | """Summary. 2 | 3 | Body 4 | """ 5 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/multi_line_push/variable_docstring.args: -------------------------------------------------------------------------------- 1 | --summary-quotes-same-line 2 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/multi_line_push/variable_docstring.py: -------------------------------------------------------------------------------- 1 | MYVAR = 1 2 | """ 3 | A multi-line 4 | docstring""" 5 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/multi_line_push/variable_docstring.py.out: -------------------------------------------------------------------------------- 1 | MYVAR = 1 2 | """A multi-line. 3 | 4 | docstring 5 | """ 6 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/variable_docstring.py: -------------------------------------------------------------------------------- 1 | MYVAR = 1 2 | """A multi-line 3 | docstring""" 4 | -------------------------------------------------------------------------------- /tests/data/format/beginning_quote/variable_docstring.py.out: -------------------------------------------------------------------------------- 1 | MYVAR = 1 2 | """A multi-line. 3 | 4 | docstring 5 | """ 6 | -------------------------------------------------------------------------------- /tests/data/format/capitalization_first_letter/class_docstring.py: -------------------------------------------------------------------------------- 1 | class MyClass: 2 | """a multi-line 3 | docstring""" 4 | 5 | class InnerClass: 6 | """ 7 | a multi-line 8 | docstring""" 9 | 10 | 11 | class MyClass: 12 | """a multi-line 13 | docstring 14 | """ 15 | 16 | class InnerClass: 17 | """a multi-line 18 | docstring 19 | """ 20 | 21 | 22 | class MyClass: 23 | """ 24 | a docstring""" 25 | 26 | class InnerClass: 27 | """ 28 | a docstring""" 29 | 30 | 31 | class MyClass: 32 | """A docstring""" 33 | 34 | class InnerClass: 35 | """A docstring""" 36 | -------------------------------------------------------------------------------- /tests/data/format/capitalization_first_letter/class_docstring.py.out: -------------------------------------------------------------------------------- 1 | class MyClass: 2 | """A multi-line. 3 | 4 | docstring 5 | """ 6 | 7 | class InnerClass: 8 | """ 9 | A multi-line. 10 | 11 | docstring 12 | """ 13 | 14 | 15 | class MyClass: 16 | """A multi-line. 17 | 18 | docstring 19 | """ 20 | 21 | class InnerClass: 22 | """A multi-line. 23 | 24 | docstring 25 | """ 26 | 27 | 28 | class MyClass: 29 | """A docstring.""" 30 | 31 | class InnerClass: 32 | """A docstring.""" 33 | 34 | 35 | class MyClass: 36 | """A docstring.""" 37 | 38 | class InnerClass: 39 | """A docstring.""" 40 | -------------------------------------------------------------------------------- /tests/data/format/capitalization_first_letter/function_docstrings.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | """ 3 | a docstring""" 4 | 5 | def inner_func(): 6 | """ 7 | a docstring""" 8 | 9 | 10 | def func(named_parameter: str) -> None: 11 | """'named_parameter' should have a nice name 12 | 13 | Other information here 14 | """ 15 | print(named_parameter) 16 | 17 | 18 | def func(): 19 | """ 20 | a multi-line 21 | docstring 22 | """ 23 | 24 | def inner_func(): 25 | """a multi-line 26 | docstring""" 27 | -------------------------------------------------------------------------------- /tests/data/format/capitalization_first_letter/function_docstrings.py.out: -------------------------------------------------------------------------------- 1 | def func(): 2 | """A docstring.""" 3 | 4 | def inner_func(): 5 | """A docstring.""" 6 | 7 | 8 | def func(named_parameter: str) -> None: 9 | """'named_parameter' should have a nice name. 10 | 11 | Other information here 12 | """ 13 | print(named_parameter) 14 | 15 | 16 | def func(): 17 | """ 18 | A multi-line. 19 | 20 | docstring 21 | """ 22 | 23 | def inner_func(): 24 | """A multi-line. 25 | 26 | docstring 27 | """ 28 | -------------------------------------------------------------------------------- /tests/data/format/capitalization_first_letter/module_docstring.py: -------------------------------------------------------------------------------- 1 | """ 2 | a docstring""" 3 | -------------------------------------------------------------------------------- /tests/data/format/capitalization_first_letter/module_docstring.py.out: -------------------------------------------------------------------------------- 1 | """A docstring.""" 2 | -------------------------------------------------------------------------------- /tests/data/format/capitalization_first_letter/single_quote_docstring.args: -------------------------------------------------------------------------------- 1 | --no-quotes-type 2 | -------------------------------------------------------------------------------- /tests/data/format/capitalization_first_letter/single_quote_docstring.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | "a docstring." 3 | -------------------------------------------------------------------------------- /tests/data/format/capitalization_first_letter/single_quote_docstring.py.out: -------------------------------------------------------------------------------- 1 | def func(): 2 | "A docstring." 3 | -------------------------------------------------------------------------------- /tests/data/format/capitalization_first_letter/uncapitalized_starting_letter.py: -------------------------------------------------------------------------------- 1 | class MyClass: 2 | """this is a single line docstring in plain english""" 3 | 4 | 5 | class MyClass: 6 | """1 cannot always be capitalized.""" 7 | 8 | 9 | class MyClass: 10 | """在某些语言中,大写字母是没有意义的""" 11 | -------------------------------------------------------------------------------- /tests/data/format/capitalization_first_letter/uncapitalized_starting_letter.py.out: -------------------------------------------------------------------------------- 1 | class MyClass: 2 | """This is a single line docstring in plain english.""" 3 | 4 | 5 | class MyClass: 6 | """1 cannot always be capitalized.""" 7 | 8 | 9 | class MyClass: 10 | """在某些语言中,大写字母是没有意义的.""" 11 | -------------------------------------------------------------------------------- /tests/data/format/capitalization_first_letter/variable_docstring.py: -------------------------------------------------------------------------------- 1 | MYVAR = 1 2 | """ 3 | a multi-line 4 | docstring""" 5 | 6 | MYVAR = 1 7 | """a multi-line 8 | docstring 9 | """ 10 | -------------------------------------------------------------------------------- /tests/data/format/capitalization_first_letter/variable_docstring.py.out: -------------------------------------------------------------------------------- 1 | MYVAR = 1 2 | """ 3 | A multi-line. 4 | 5 | docstring 6 | """ 7 | 8 | MYVAR = 1 9 | """A multi-line. 10 | 11 | docstring 12 | """ 13 | -------------------------------------------------------------------------------- /tests/data/format/ending_quote/class_docstring.py: -------------------------------------------------------------------------------- 1 | class MyClass: 2 | """ 3 | A docstring. 4 | """ 5 | 6 | class InnerClass: 7 | """ 8 | A docstring. 9 | """ 10 | -------------------------------------------------------------------------------- /tests/data/format/ending_quote/class_docstring.py.out: -------------------------------------------------------------------------------- 1 | class MyClass: 2 | """A docstring.""" 3 | 4 | class InnerClass: 5 | """A docstring.""" 6 | -------------------------------------------------------------------------------- /tests/data/format/ending_quote/function_docstrings.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | """A docstring. 3 | """ 4 | 5 | def inner_func(): 6 | """A docstring. 7 | """ 8 | -------------------------------------------------------------------------------- /tests/data/format/ending_quote/function_docstrings.py.out: -------------------------------------------------------------------------------- 1 | def func(): 2 | """A docstring.""" 3 | 4 | def inner_func(): 5 | """A docstring.""" 6 | -------------------------------------------------------------------------------- /tests/data/format/ending_quote/module_docstring.py: -------------------------------------------------------------------------------- 1 | """ 2 | A docstring. 3 | """ 4 | -------------------------------------------------------------------------------- /tests/data/format/ending_quote/module_docstring.py.out: -------------------------------------------------------------------------------- 1 | """A docstring.""" 2 | -------------------------------------------------------------------------------- /tests/data/format/ending_quote/variable_docstring.py: -------------------------------------------------------------------------------- 1 | MYVAR = 1 2 | """A docstring. 3 | """ 4 | -------------------------------------------------------------------------------- /tests/data/format/ending_quote/variable_docstring.py.out: -------------------------------------------------------------------------------- 1 | MYVAR = 1 2 | """A docstring.""" 3 | -------------------------------------------------------------------------------- /tests/data/format/final_period/class_docstring.py: -------------------------------------------------------------------------------- 1 | class MyClass: 2 | """Docstring""" 3 | 4 | class InnerClass: 5 | """Docstring""" 6 | 7 | 8 | class MyClass: 9 | """A multi-line 10 | docstring 11 | """ 12 | 13 | class InnerClass: 14 | """A multi-line 15 | docstring 16 | """ 17 | 18 | 19 | class MyClass: 20 | """Summary 21 | 22 | docstring 23 | """ 24 | 25 | class InnerClass: 26 | """Summary 27 | 28 | docstring 29 | """ 30 | -------------------------------------------------------------------------------- /tests/data/format/final_period/class_docstring.py.out: -------------------------------------------------------------------------------- 1 | class MyClass: 2 | """Docstring.""" 3 | 4 | class InnerClass: 5 | """Docstring.""" 6 | 7 | 8 | class MyClass: 9 | """A multi-line. 10 | 11 | docstring 12 | """ 13 | 14 | class InnerClass: 15 | """A multi-line. 16 | 17 | docstring 18 | """ 19 | 20 | 21 | class MyClass: 22 | """Summary. 23 | 24 | docstring 25 | """ 26 | 27 | class InnerClass: 28 | """Summary. 29 | 30 | docstring 31 | """ 32 | -------------------------------------------------------------------------------- /tests/data/format/final_period/exotic_line_ending.py: -------------------------------------------------------------------------------- 1 | def function(): 2 | """Check the name of first argument, expect: 3 | 4 | *'self' for a regular method 5 | *'cls' for a class method or a metaclass regular method""" 6 | 7 | def function(): 8 | """This has peculiar line ending; 9 | 10 | but it should not matter. 11 | """ 12 | -------------------------------------------------------------------------------- /tests/data/format/final_period/exotic_line_ending.py.out: -------------------------------------------------------------------------------- 1 | def function(): 2 | """Check the name of first argument, expect: 3 | 4 | *'self' for a regular method 5 | *'cls' for a class method or a metaclass regular method 6 | """ 7 | 8 | def function(): 9 | """This has peculiar line ending; 10 | 11 | but it should not matter. 12 | """ 13 | -------------------------------------------------------------------------------- /tests/data/format/final_period/function_docstrings.py: -------------------------------------------------------------------------------- 1 | def func() 2 | """Docstring""" 3 | 4 | def inner_func() 5 | """Docstring""" 6 | 7 | 8 | def func() 9 | """A multi-line 10 | docstring 11 | """ 12 | 13 | def inner_func() 14 | """A multi-line 15 | docstring 16 | """ 17 | 18 | 19 | def func() 20 | """Summary 21 | 22 | docstring 23 | """ 24 | 25 | def inner_func() 26 | """Summary 27 | 28 | docstring 29 | """ 30 | -------------------------------------------------------------------------------- /tests/data/format/final_period/function_docstrings.py.out: -------------------------------------------------------------------------------- 1 | def func() 2 | """Docstring.""" 3 | 4 | def inner_func() 5 | """Docstring.""" 6 | 7 | 8 | def func() 9 | """A multi-line. 10 | 11 | docstring 12 | """ 13 | 14 | def inner_func() 15 | """A multi-line. 16 | 17 | docstring 18 | """ 19 | 20 | 21 | def func() 22 | """Summary. 23 | 24 | docstring 25 | """ 26 | 27 | def inner_func() 28 | """Summary. 29 | 30 | docstring 31 | """ 32 | -------------------------------------------------------------------------------- /tests/data/format/final_period/function_title_docstrings.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | def inner_func(): 3 | """Summary 4 | ========== 5 | 6 | docstring 7 | """ 8 | 9 | def inner_func(): 10 | """Summary 11 | ---------- 12 | 13 | docstring 14 | """ 15 | 16 | def inner_func(): 17 | """Summary 18 | ^^^^^^^^^^ 19 | 20 | docstring 21 | """ 22 | 23 | def inner_func(): 24 | """Summary 25 | ********** 26 | 27 | docstring 28 | """ 29 | 30 | def inner_func(): 31 | """Summary 32 | ^^^^^^^^^^ 33 | 34 | docstring 35 | """ 36 | 37 | def inner_func(): 38 | """Summary 39 | aaaaaaaaaa 40 | 41 | docstring 42 | """ 43 | -------------------------------------------------------------------------------- /tests/data/format/final_period/function_title_docstrings.py.out: -------------------------------------------------------------------------------- 1 | def func(): 2 | def inner_func(): 3 | """Summary 4 | ========== 5 | 6 | docstring 7 | """ 8 | 9 | def inner_func(): 10 | """Summary 11 | ---------- 12 | 13 | docstring 14 | """ 15 | 16 | def inner_func(): 17 | """Summary 18 | ^^^^^^^^^^ 19 | 20 | docstring 21 | """ 22 | 23 | def inner_func(): 24 | """Summary 25 | ********** 26 | 27 | docstring 28 | """ 29 | 30 | def inner_func(): 31 | """Summary 32 | ^^^^^^^^^^ 33 | 34 | docstring 35 | """ 36 | 37 | def inner_func(): 38 | """Summary 39 | aaaaaaaaaa 40 | 41 | docstring 42 | """ 43 | -------------------------------------------------------------------------------- /tests/data/format/final_period/module_docstring.py: -------------------------------------------------------------------------------- 1 | """Docstring""" 2 | 3 | """A multi-line 4 | docstring 5 | """ 6 | 7 | """Summary 8 | 9 | docstring 10 | """ 11 | 12 | """Docstring?""" 13 | 14 | """Docstring!""" 15 | 16 | """Docstring‽""" 17 | -------------------------------------------------------------------------------- /tests/data/format/final_period/module_docstring.py.out: -------------------------------------------------------------------------------- 1 | """Docstring.""" 2 | 3 | """A multi-line. 4 | 5 | docstring 6 | """ 7 | 8 | """Summary. 9 | 10 | docstring 11 | """ 12 | 13 | """Docstring?""" 14 | 15 | """Docstring!""" 16 | 17 | """Docstring‽""" 18 | -------------------------------------------------------------------------------- /tests/data/format/final_period/single_quote_docstring.args: -------------------------------------------------------------------------------- 1 | --no-quotes-type 2 | -------------------------------------------------------------------------------- /tests/data/format/final_period/single_quote_docstring.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | "A docstring" 3 | -------------------------------------------------------------------------------- /tests/data/format/final_period/single_quote_docstring.py.out: -------------------------------------------------------------------------------- 1 | def func(): 2 | "A docstring." 3 | -------------------------------------------------------------------------------- /tests/data/format/final_period/variable_docstring.py: -------------------------------------------------------------------------------- 1 | MYVAR = 1 2 | """Docstring""" 3 | 4 | MYVAR = 1 5 | """A multi-line 6 | docstring 7 | """ 8 | 9 | MYVAR = 1 10 | """Summary 11 | 12 | A docstring 13 | """ 14 | -------------------------------------------------------------------------------- /tests/data/format/final_period/variable_docstring.py.out: -------------------------------------------------------------------------------- 1 | MYVAR = 1 2 | """Docstring.""" 3 | 4 | MYVAR = 1 5 | """A multi-line. 6 | 7 | docstring 8 | """ 9 | 10 | MYVAR = 1 11 | """Summary. 12 | 13 | A docstring 14 | """ 15 | -------------------------------------------------------------------------------- /tests/data/format/linewrap_summary/function_docstrings.args: -------------------------------------------------------------------------------- 1 | --linewrap-full-docstring 2 | -------------------------------------------------------------------------------- /tests/data/format/linewrap_summary/function_docstrings.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | """A very long summary line that needs to be wrapped, A very long summary line that needs to be wrapped. 3 | 4 | A description that is not too long. 5 | """ 6 | 7 | 8 | def func(): 9 | """A very long multi-line summary line that needs to be wrapped, A very long multi-line summary line that needs to be wrapped. 10 | A very long summary line that needs to be wrapped. 11 | 12 | A description that is not too long. 13 | """ 14 | 15 | 16 | def func(): 17 | """A multi-line summary that can be on one line, 18 | But it has a new line so it isn't. 19 | 20 | A description that is not too long. 21 | """ 22 | 23 | 24 | # Since the ending quotes will be appended on the same line this 25 | # exceeds the max length. 26 | def func(): 27 | """A multi-line summary that can be on one line, Something that is just too longgg.""" 28 | 29 | 30 | def func(): 31 | """A multi-line summary that can be on one line, Something that is just too long.""" 32 | 33 | 34 | def func(): 35 | """A multi-line summary that can be on one line, Something that is just too lon.""" 36 | 37 | 38 | # Regression for bug found in pylint 39 | # We should re-add the quotes to line length if they will never be on the first line. 40 | class LinesChunk: 41 | """The LinesChunk object computes and stores the hash of some consecutive stripped lines of a lineset.""" 42 | 43 | 44 | # Test for multiple periods at the end of the line 45 | def func(): 46 | """A very long summary line that needs to be wrapped, A very long summary line that needs to be wrapp... 47 | 48 | A description that is not too long. 49 | """ 50 | -------------------------------------------------------------------------------- /tests/data/format/linewrap_summary/function_docstrings.py.out: -------------------------------------------------------------------------------- 1 | def func(): 2 | """A very long summary line that needs to be wrapped, A very long summary line that. 3 | 4 | needs to be wrapped. 5 | 6 | A description that is not too long. 7 | """ 8 | 9 | 10 | def func(): 11 | """A very long multi-line summary line that needs to be wrapped, A very long multi-. 12 | 13 | line summary line that needs to be wrapped. 14 | 15 | A very long summary line that needs to be wrapped. 16 | 17 | A description that is not too long. 18 | """ 19 | 20 | 21 | def func(): 22 | """A multi-line summary that can be on one line,. 23 | 24 | But it has a new line so it isn't. 25 | 26 | A description that is not too long. 27 | """ 28 | 29 | 30 | # Since the ending quotes will be appended on the same line this 31 | # exceeds the max length. 32 | def func(): 33 | """A multi-line summary that can be on one line, Something that is just too. 34 | 35 | longgg. 36 | """ 37 | 38 | 39 | def func(): 40 | """A multi-line summary that can be on one line, Something that is just too. 41 | 42 | long. 43 | """ 44 | 45 | 46 | def func(): 47 | """A multi-line summary that can be on one line, Something that is just too lon.""" 48 | 49 | 50 | # Regression for bug found in pylint 51 | # We should re-add the quotes to line length if they will never be on the first line. 52 | class LinesChunk: 53 | """The LinesChunk object computes and stores the hash of some consecutive stripped. 54 | 55 | lines of a lineset. 56 | """ 57 | 58 | 59 | # Test for multiple periods at the end of the line 60 | def func(): 61 | """A very long summary line that needs to be wrapped, A very long summary line that. 62 | 63 | needs to be wrapp... 64 | 65 | A description that is not too long. 66 | """ 67 | -------------------------------------------------------------------------------- /tests/data/format/linewrap_summary/max_line_length_50.args: -------------------------------------------------------------------------------- 1 | --linewrap-full-docstring 2 | --max-line-length=50 3 | --max-summary-lines=2 4 | -------------------------------------------------------------------------------- /tests/data/format/linewrap_summary/max_line_length_50.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | """A very long line that needs to be wrapped especially this sentence.""" 3 | 4 | def func(): 5 | """Event multi line docstrings need to be wrapped. 6 | 7 | This description is way too long. It definitely needs to be wrapped. 8 | """ 9 | 10 | # Regression for bug found in pylint 11 | # We should re-add the quotes to line length if they will never be on the first line. 12 | class LinesChunk: 13 | """The LinesChunk object computes and store.""" 14 | -------------------------------------------------------------------------------- /tests/data/format/linewrap_summary/max_line_length_50.py.out: -------------------------------------------------------------------------------- 1 | def func(): 2 | """A very long line that needs to be wrapped 3 | especially this sentence. 4 | """ 5 | 6 | def func(): 7 | """Event multi line docstrings need to be 8 | wrapped. 9 | 10 | This description is way too long. It definitely needs to be wrapped. 11 | """ 12 | 13 | # Regression for bug found in pylint 14 | # We should re-add the quotes to line length if they will never be on the first line. 15 | class LinesChunk: 16 | """The LinesChunk object computes and 17 | store. 18 | """ 19 | -------------------------------------------------------------------------------- /tests/data/format/multi_line/class_docstring.py: -------------------------------------------------------------------------------- 1 | class MyClass: 2 | """A multi-line 3 | docstring""" 4 | 5 | class InnerClass: 6 | """A multi-line 7 | docstring""" 8 | 9 | 10 | class MyClass: 11 | """A multi-line 12 | docstring 13 | """ 14 | 15 | class InnerClass: 16 | """A multi-line 17 | docstring 18 | """ 19 | -------------------------------------------------------------------------------- /tests/data/format/multi_line/class_docstring.py.out: -------------------------------------------------------------------------------- 1 | class MyClass: 2 | """A multi-line. 3 | 4 | docstring 5 | """ 6 | 7 | class InnerClass: 8 | """A multi-line. 9 | 10 | docstring 11 | """ 12 | 13 | 14 | class MyClass: 15 | """A multi-line. 16 | 17 | docstring 18 | """ 19 | 20 | class InnerClass: 21 | """A multi-line. 22 | 23 | docstring 24 | """ 25 | -------------------------------------------------------------------------------- /tests/data/format/multi_line/function_docstrings.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | """A docstring.""" 3 | 4 | def inner_func(): 5 | """A docstring.""" 6 | 7 | 8 | def func(): 9 | """A multi-line 10 | docstring""" 11 | 12 | def inner_func(): 13 | """A multi-line 14 | docstring""" 15 | 16 | 17 | def func(): 18 | """A multi-line 19 | docstring 20 | """ 21 | 22 | def inner_func(): 23 | """A multi-line 24 | docstring 25 | """ 26 | 27 | def func(): 28 | '''A multi-line 29 | docstring''' 30 | 31 | def inner_func(): 32 | '''A multi-line 33 | docstring''' 34 | 35 | 36 | def func(): 37 | '''A multi-line 38 | docstring 39 | ''' 40 | 41 | def inner_func(): 42 | '''A multi-line 43 | docstring 44 | ''' 45 | -------------------------------------------------------------------------------- /tests/data/format/multi_line/function_docstrings.py.out: -------------------------------------------------------------------------------- 1 | def func(): 2 | """A docstring.""" 3 | 4 | def inner_func(): 5 | """A docstring.""" 6 | 7 | 8 | def func(): 9 | """A multi-line. 10 | 11 | docstring 12 | """ 13 | 14 | def inner_func(): 15 | """A multi-line. 16 | 17 | docstring 18 | """ 19 | 20 | 21 | def func(): 22 | """A multi-line. 23 | 24 | docstring 25 | """ 26 | 27 | def inner_func(): 28 | """A multi-line. 29 | 30 | docstring 31 | """ 32 | 33 | def func(): 34 | """A multi-line. 35 | 36 | docstring 37 | """ 38 | 39 | def inner_func(): 40 | """A multi-line. 41 | 42 | docstring 43 | """ 44 | 45 | 46 | def func(): 47 | """A multi-line. 48 | 49 | docstring 50 | """ 51 | 52 | def inner_func(): 53 | """A multi-line. 54 | 55 | docstring 56 | """ 57 | -------------------------------------------------------------------------------- /tests/data/format/multi_line/module_docstring.py: -------------------------------------------------------------------------------- 1 | """A multi-line 2 | docstring""" 3 | -------------------------------------------------------------------------------- /tests/data/format/multi_line/module_docstring.py.out: -------------------------------------------------------------------------------- 1 | """A multi-line. 2 | 3 | docstring 4 | """ 5 | -------------------------------------------------------------------------------- /tests/data/format/multi_line/variable_docstring.py: -------------------------------------------------------------------------------- 1 | MYVAR = 1 2 | """A multi-line 3 | docstring""" 4 | 5 | MYVAR = 1 6 | """A multi-line 7 | docstring 8 | """ 9 | -------------------------------------------------------------------------------- /tests/data/format/multi_line/variable_docstring.py.out: -------------------------------------------------------------------------------- 1 | MYVAR = 1 2 | """A multi-line. 3 | 4 | docstring 5 | """ 6 | 7 | MYVAR = 1 8 | """A multi-line. 9 | 10 | docstring 11 | """ 12 | -------------------------------------------------------------------------------- /tests/data/format/newlines/dos.py: -------------------------------------------------------------------------------- 1 | var = 1 2 | """A multi-line. 3 | docstring.""" 4 | 5 | var = 1 6 | """A single-line docstring. 7 | """ 8 | -------------------------------------------------------------------------------- /tests/data/format/newlines/dos.py.out: -------------------------------------------------------------------------------- 1 | var = 1 2 | """A multi-line. 3 | 4 | docstring. 5 | """ 6 | 7 | var = 1 8 | """A single-line docstring.""" 9 | -------------------------------------------------------------------------------- /tests/data/format/newlines/mac.py: -------------------------------------------------------------------------------- 1 | var = 1 """A multi-line. docstring.""" var = 1 """A single-line docstring. """ -------------------------------------------------------------------------------- /tests/data/format/newlines/mac.py.out: -------------------------------------------------------------------------------- 1 | var = 1 """A multi-line. docstring. """ var = 1 """A single-line docstring.""" -------------------------------------------------------------------------------- /tests/data/format/newlines/mixed.err: -------------------------------------------------------------------------------- 1 | Found multiple newline variants in {testfile}. Using variant that occurred first. 2 | -------------------------------------------------------------------------------- /tests/data/format/newlines/mixed.py: -------------------------------------------------------------------------------- 1 | var = 1 2 | """A multi-line. 3 | docstring.""" 4 | 5 | var = 1 6 | """A single-line docstring. 7 | """ 8 | 9 | var = 1 10 | """A multi-line. 11 | docstring.""" 12 | 13 | var = 1 14 | """A single-line docstring. 15 | """ 16 | -------------------------------------------------------------------------------- /tests/data/format/newlines/mixed.py.out: -------------------------------------------------------------------------------- 1 | var = 1 2 | """A multi-line. 3 | 4 | docstring. 5 | """ 6 | 7 | var = 1 8 | """A single-line docstring.""" 9 | 10 | var = 1 11 | """A multi-line. 12 | 13 | docstring. 14 | """ 15 | 16 | var = 1 17 | """A single-line docstring.""" 18 | -------------------------------------------------------------------------------- /tests/data/format/newlines/none.args: -------------------------------------------------------------------------------- 1 | --linewrap-full-docstring 2 | -------------------------------------------------------------------------------- /tests/data/format/newlines/none.py: -------------------------------------------------------------------------------- 1 | """A multi-line docstring that should get split at this period. and not afterwards as it is way too long.""" -------------------------------------------------------------------------------- /tests/data/format/newlines/none.py-0d0a.out: -------------------------------------------------------------------------------- 1 | """A multi-line docstring that should get split at this period. 2 | 3 | and not afterwards as it is way too long. 4 | """ -------------------------------------------------------------------------------- /tests/data/format/newlines/none.py.out: -------------------------------------------------------------------------------- 1 | """A multi-line docstring that should get split at this period. 2 | 3 | and not afterwards as it is way too long. 4 | """ -------------------------------------------------------------------------------- /tests/data/format/newlines/unix.py: -------------------------------------------------------------------------------- 1 | var = 1 2 | """A multi-line. 3 | docstring.""" 4 | 5 | var = 1 6 | """A single-line docstring. 7 | """ 8 | -------------------------------------------------------------------------------- /tests/data/format/newlines/unix.py.out: -------------------------------------------------------------------------------- 1 | var = 1 2 | """A multi-line. 3 | 4 | docstring. 5 | """ 6 | 7 | var = 1 8 | """A single-line docstring.""" 9 | -------------------------------------------------------------------------------- /tests/data/format/no_changes/multi_line_variables.py: -------------------------------------------------------------------------------- 1 | var = """A multi-line 2 | docstring""" 3 | 4 | var = """A multi-line 5 | docstring 6 | """ 7 | -------------------------------------------------------------------------------- /tests/data/format/no_changes/multi_line_variables.py.out: -------------------------------------------------------------------------------- 1 | var = """A multi-line 2 | docstring""" 3 | 4 | var = """A multi-line 5 | docstring 6 | """ 7 | -------------------------------------------------------------------------------- /tests/data/format/no_whitespace_stripper/module_docstring.args: -------------------------------------------------------------------------------- 1 | --no-strip-whitespaces 2 | -------------------------------------------------------------------------------- /tests/data/format/no_whitespace_stripper/module_docstring.py: -------------------------------------------------------------------------------- 1 | """ A multi-line 2 | docstring 3 | """ 4 | 5 | """ A multi-line 6 | docstring 7 | """ 8 | 9 | """ My docstring. 10 | 11 | My indented section 12 | """ 13 | -------------------------------------------------------------------------------- /tests/data/format/no_whitespace_stripper/module_docstring.py.out: -------------------------------------------------------------------------------- 1 | """ A multi-line . 2 | 3 | docstring 4 | """ 5 | 6 | """ A multi-line . 7 | 8 | docstring 9 | 10 | """ 11 | 12 | """ My docstring. 13 | 14 | 15 | 16 | My indented section 17 | 18 | """ 19 | -------------------------------------------------------------------------------- /tests/data/format/numpydoc/numpydoc_closing_quotes.args: -------------------------------------------------------------------------------- 1 | --style=numpydoc 2 | --no-closing-quotes 3 | -------------------------------------------------------------------------------- /tests/data/format/numpydoc/numpydoc_closing_quotes.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | """ 3 | A docstring""" 4 | 5 | def func(): 6 | """ 7 | A docstring 8 | """ 9 | 10 | def func(): 11 | """A docstring.""" 12 | -------------------------------------------------------------------------------- /tests/data/format/numpydoc/numpydoc_closing_quotes.py.out: -------------------------------------------------------------------------------- 1 | def func(): 2 | """A docstring.""" 3 | 4 | def func(): 5 | """A docstring. 6 | """ 7 | 8 | def func(): 9 | """A docstring.""" 10 | -------------------------------------------------------------------------------- /tests/data/format/numpydoc/numpydoc_header_line.args: -------------------------------------------------------------------------------- 1 | --style=numpydoc 2 | --no-numpydoc-name-type-spacing 3 | --no-numpydoc-section-order 4 | --no-numpydoc-section-spacing 5 | --no-final-period 6 | --no-closing-quotes 7 | -------------------------------------------------------------------------------- /tests/data/format/numpydoc/numpydoc_header_line.py: -------------------------------------------------------------------------------- 1 | numpydoc_style.py -------------------------------------------------------------------------------- /tests/data/format/numpydoc/numpydoc_header_line.py.out: -------------------------------------------------------------------------------- 1 | """Example module for numpydoc docstring style. 2 | References 3 | ---------- 4 | NumPy docstring style guide: 5 | https://numpydoc.readthedocs.io/en/latest/format.html#documenting-modules""" 6 | import math 7 | 8 | EULER_NUMBER = math.e 9 | """Euler's number. 10 | 11 | Not related to Euler's constant (sometimes called the Euler-Mascheroni 12 | constant. 13 | References 14 | ---------- 15 | E (mathematical constant) 16 | https://en.wikipedia.org/wiki/E_(mathematical_constant) 17 | Notes 18 | ----- 19 | It is the limit of ``(1 + 1/n)**n`` as n approaches infinity, so it 20 | is used in the equation for continuously-compouned interest. 21 | 22 | It is also the sum of the reciprocals of the whole numbers starting 23 | with zero, which is related to some calculus-related properties 24 | mathemeticians find elegant. 25 | """ 26 | 27 | 28 | def sincos(theta): 29 | """Returns 30 | ---------- 31 | sin: float 32 | the sine of theta 33 | cos: float 34 | the cosine of theta 35 | Raises 36 | ------ 37 | TypeError 38 | If `theta` is not a float. 39 | Parameters 40 | ---------- 41 | theta: float 42 | the angle at which to calculate the sine and cosine. 43 | """ 44 | return math.sin(theta), math.cos(theta) 45 | 46 | 47 | def fibbonacci(): 48 | """Generate the Fibonacci sequence. 49 | 50 | Each term is the sum of the two previous; conventionally starts 51 | with two ones. 52 | References 53 | ---------- 54 | Fibonacci numbers 55 | https://en.wikipedia.org/wiki/Fibonacci_number 56 | Yields 57 | ------ 58 | int""" 59 | curr = 1 60 | last = 0 61 | while True: 62 | yield curr 63 | curr, last = curr + last, curr 64 | -------------------------------------------------------------------------------- /tests/data/format/numpydoc/numpydoc_parameter_spacing.args: -------------------------------------------------------------------------------- 1 | --style=numpydoc 2 | --numpydoc-name-type-spacing 3 | --no-numpydoc-section-order 4 | --no-numpydoc-section-spacing 5 | --no-numpydoc-section-hyphen-length 6 | --no-final-period 7 | --no-closing-quotes 8 | -------------------------------------------------------------------------------- /tests/data/format/numpydoc/numpydoc_parameter_spacing.py: -------------------------------------------------------------------------------- 1 | numpydoc_style.py -------------------------------------------------------------------------------- /tests/data/format/numpydoc/numpydoc_parameter_spacing.py.out: -------------------------------------------------------------------------------- 1 | """Example module for numpydoc docstring style. 2 | References 3 | ----- 4 | NumPy docstring style guide: 5 | https://numpydoc.readthedocs.io/en/latest/format.html#documenting-modules""" 6 | import math 7 | 8 | EULER_NUMBER = math.e 9 | """Euler's number. 10 | 11 | Not related to Euler's constant (sometimes called the Euler-Mascheroni 12 | constant. 13 | References 14 | ----- 15 | E (mathematical constant) 16 | https://en.wikipedia.org/wiki/E_(mathematical_constant) 17 | Notes 18 | --- 19 | It is the limit of ``(1 + 1/n)**n`` as n approaches infinity, so it 20 | is used in the equation for continuously-compouned interest. 21 | 22 | It is also the sum of the reciprocals of the whole numbers starting 23 | with zero, which is related to some calculus-related properties 24 | mathemeticians find elegant. 25 | """ 26 | 27 | 28 | def sincos(theta): 29 | """Returns 30 | ---- 31 | sin : float 32 | the sine of theta 33 | cos : float 34 | the cosine of theta 35 | Raises 36 | --- 37 | TypeError 38 | If `theta` is not a float. 39 | Parameters 40 | ----- 41 | theta : float 42 | the angle at which to calculate the sine and cosine. 43 | """ 44 | return math.sin(theta), math.cos(theta) 45 | 46 | 47 | def fibbonacci(): 48 | """Generate the Fibonacci sequence. 49 | 50 | Each term is the sum of the two previous; conventionally starts 51 | with two ones. 52 | References 53 | ----- 54 | Fibonacci numbers 55 | https://en.wikipedia.org/wiki/Fibonacci_number 56 | Yields 57 | --- 58 | int""" 59 | curr = 1 60 | last = 0 61 | while True: 62 | yield curr 63 | curr, last = curr + last, curr 64 | -------------------------------------------------------------------------------- /tests/data/format/numpydoc/numpydoc_regression.args: -------------------------------------------------------------------------------- 1 | --style=numpydoc 2 | -------------------------------------------------------------------------------- /tests/data/format/numpydoc/numpydoc_regression.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | """ 3 | A docstring""" 4 | 5 | def func(): 6 | """ 7 | A docstring 8 | """ 9 | -------------------------------------------------------------------------------- /tests/data/format/numpydoc/numpydoc_regression.py.out: -------------------------------------------------------------------------------- 1 | def func(): 2 | """A docstring.""" 3 | 4 | def func(): 5 | """A docstring.""" 6 | -------------------------------------------------------------------------------- /tests/data/format/numpydoc/numpydoc_section_ordering.args: -------------------------------------------------------------------------------- 1 | --style=numpydoc 2 | --no-numpydoc-name-type-spacing 3 | --no-numpydoc-section-spacing 4 | --no-numpydoc-section-hyphen-length 5 | --no-final-period 6 | --no-closing-quotes 7 | -------------------------------------------------------------------------------- /tests/data/format/numpydoc/numpydoc_section_ordering.py: -------------------------------------------------------------------------------- 1 | numpydoc_style.py -------------------------------------------------------------------------------- /tests/data/format/numpydoc/numpydoc_section_ordering.py.out: -------------------------------------------------------------------------------- 1 | """Example module for numpydoc docstring style. 2 | References 3 | ----- 4 | NumPy docstring style guide: 5 | https://numpydoc.readthedocs.io/en/latest/format.html#documenting-modules""" 6 | import math 7 | 8 | EULER_NUMBER = math.e 9 | """Euler's number. 10 | 11 | Not related to Euler's constant (sometimes called the Euler-Mascheroni 12 | constant. 13 | Notes 14 | --- 15 | It is the limit of ``(1 + 1/n)**n`` as n approaches infinity, so it 16 | is used in the equation for continuously-compouned interest. 17 | 18 | It is also the sum of the reciprocals of the whole numbers starting 19 | with zero, which is related to some calculus-related properties 20 | mathemeticians find elegant. 21 | 22 | References 23 | ----- 24 | E (mathematical constant) 25 | https://en.wikipedia.org/wiki/E_(mathematical_constant)""" 26 | 27 | 28 | def sincos(theta): 29 | """Parameters 30 | ----- 31 | theta: float 32 | the angle at which to calculate the sine and cosine. 33 | 34 | Returns 35 | ---- 36 | sin: float 37 | the sine of theta 38 | cos: float 39 | the cosine of theta 40 | Raises 41 | --- 42 | TypeError 43 | If `theta` is not a float.""" 44 | return math.sin(theta), math.cos(theta) 45 | 46 | 47 | def fibbonacci(): 48 | """Generate the Fibonacci sequence. 49 | 50 | Each term is the sum of the two previous; conventionally starts 51 | with two ones. 52 | Yields 53 | --- 54 | int 55 | References 56 | ----- 57 | Fibonacci numbers 58 | https://en.wikipedia.org/wiki/Fibonacci_number""" 59 | curr = 1 60 | last = 0 61 | while True: 62 | yield curr 63 | curr, last = curr + last, curr 64 | -------------------------------------------------------------------------------- /tests/data/format/numpydoc/numpydoc_section_spacing.args: -------------------------------------------------------------------------------- 1 | --style=numpydoc 2 | --no-numpydoc-name-type-spacing 3 | --no-numpydoc-section-order 4 | --no-numpydoc-section-hyphen-length 5 | --no-final-period 6 | --no-closing-quotes 7 | -------------------------------------------------------------------------------- /tests/data/format/numpydoc/numpydoc_section_spacing.py: -------------------------------------------------------------------------------- 1 | numpydoc_style.py -------------------------------------------------------------------------------- /tests/data/format/numpydoc/numpydoc_section_spacing.py.out: -------------------------------------------------------------------------------- 1 | """Example module for numpydoc docstring style. 2 | 3 | References 4 | ----- 5 | NumPy docstring style guide: 6 | https://numpydoc.readthedocs.io/en/latest/format.html#documenting-modules 7 | """ 8 | import math 9 | 10 | EULER_NUMBER = math.e 11 | """Euler's number. 12 | 13 | Not related to Euler's constant (sometimes called the Euler-Mascheroni 14 | constant. 15 | 16 | References 17 | ----- 18 | E (mathematical constant) 19 | https://en.wikipedia.org/wiki/E_(mathematical_constant) 20 | 21 | Notes 22 | --- 23 | It is the limit of ``(1 + 1/n)**n`` as n approaches infinity, so it 24 | is used in the equation for continuously-compouned interest. 25 | 26 | It is also the sum of the reciprocals of the whole numbers starting 27 | with zero, which is related to some calculus-related properties 28 | mathemeticians find elegant. 29 | """ 30 | 31 | 32 | def sincos(theta): 33 | """Returns 34 | ---- 35 | sin: float 36 | the sine of theta 37 | cos: float 38 | the cosine of theta 39 | 40 | Raises 41 | --- 42 | TypeError 43 | If `theta` is not a float. 44 | 45 | Parameters 46 | ----- 47 | theta: float 48 | the angle at which to calculate the sine and cosine. 49 | """ 50 | return math.sin(theta), math.cos(theta) 51 | 52 | 53 | def fibbonacci(): 54 | """Generate the Fibonacci sequence. 55 | 56 | Each term is the sum of the two previous; conventionally starts 57 | with two ones. 58 | 59 | References 60 | ----- 61 | Fibonacci numbers 62 | https://en.wikipedia.org/wiki/Fibonacci_number 63 | 64 | Yields 65 | --- 66 | int 67 | """ 68 | curr = 1 69 | last = 0 70 | while True: 71 | yield curr 72 | curr, last = curr + last, curr 73 | -------------------------------------------------------------------------------- /tests/data/format/numpydoc/numpydoc_style.args: -------------------------------------------------------------------------------- 1 | --style=numpydoc 2 | --no-final-period 3 | --no-closing-quotes 4 | -------------------------------------------------------------------------------- /tests/data/format/numpydoc/numpydoc_style.py: -------------------------------------------------------------------------------- 1 | """Example module for numpydoc docstring style. 2 | References 3 | ----- 4 | NumPy docstring style guide: 5 | https://numpydoc.readthedocs.io/en/latest/format.html#documenting-modules""" 6 | import math 7 | 8 | EULER_NUMBER = math.e 9 | """Euler's number. 10 | 11 | Not related to Euler's constant (sometimes called the Euler-Mascheroni 12 | constant. 13 | References 14 | ----- 15 | E (mathematical constant) 16 | https://en.wikipedia.org/wiki/E_(mathematical_constant) 17 | Notes 18 | --- 19 | It is the limit of ``(1 + 1/n)**n`` as n approaches infinity, so it 20 | is used in the equation for continuously-compouned interest. 21 | 22 | It is also the sum of the reciprocals of the whole numbers starting 23 | with zero, which is related to some calculus-related properties 24 | mathemeticians find elegant. 25 | """ 26 | 27 | 28 | def sincos(theta): 29 | """Returns 30 | ---- 31 | sin: float 32 | the sine of theta 33 | cos: float 34 | the cosine of theta 35 | Raises 36 | --- 37 | TypeError 38 | If `theta` is not a float. 39 | Parameters 40 | ----- 41 | theta: float 42 | the angle at which to calculate the sine and cosine. 43 | """ 44 | return math.sin(theta), math.cos(theta) 45 | 46 | 47 | def fibbonacci(): 48 | """Generate the Fibonacci sequence. 49 | 50 | Each term is the sum of the two previous; conventionally starts 51 | with two ones. 52 | References 53 | ----- 54 | Fibonacci numbers 55 | https://en.wikipedia.org/wiki/Fibonacci_number 56 | Yields 57 | --- 58 | int""" 59 | curr = 1 60 | last = 0 61 | while True: 62 | yield curr 63 | curr, last = curr + last, curr 64 | -------------------------------------------------------------------------------- /tests/data/format/numpydoc/numpydoc_style.py.out: -------------------------------------------------------------------------------- 1 | """Example module for numpydoc docstring style. 2 | 3 | References 4 | ---------- 5 | NumPy docstring style guide: 6 | https://numpydoc.readthedocs.io/en/latest/format.html#documenting-modules 7 | """ 8 | import math 9 | 10 | EULER_NUMBER = math.e 11 | """Euler's number. 12 | 13 | Not related to Euler's constant (sometimes called the Euler-Mascheroni 14 | constant. 15 | 16 | Notes 17 | ----- 18 | It is the limit of ``(1 + 1/n)**n`` as n approaches infinity, so it 19 | is used in the equation for continuously-compouned interest. 20 | 21 | It is also the sum of the reciprocals of the whole numbers starting 22 | with zero, which is related to some calculus-related properties 23 | mathemeticians find elegant. 24 | 25 | References 26 | ---------- 27 | E (mathematical constant) 28 | https://en.wikipedia.org/wiki/E_(mathematical_constant) 29 | """ 30 | 31 | 32 | def sincos(theta): 33 | """Parameters 34 | ------------- 35 | theta : float 36 | the angle at which to calculate the sine and cosine. 37 | 38 | Returns 39 | ------- 40 | sin : float 41 | the sine of theta 42 | cos : float 43 | the cosine of theta 44 | 45 | Raises 46 | ------ 47 | TypeError 48 | If `theta` is not a float. 49 | """ 50 | return math.sin(theta), math.cos(theta) 51 | 52 | 53 | def fibbonacci(): 54 | """Generate the Fibonacci sequence. 55 | 56 | Each term is the sum of the two previous; conventionally starts 57 | with two ones. 58 | 59 | Yields 60 | ------ 61 | int 62 | 63 | References 64 | ---------- 65 | Fibonacci numbers 66 | https://en.wikipedia.org/wiki/Fibonacci_number 67 | """ 68 | curr = 1 69 | last = 0 70 | while True: 71 | yield curr 72 | curr, last = curr + last, curr 73 | -------------------------------------------------------------------------------- /tests/data/format/numpydoc/numpydoc_style_without_type.args: -------------------------------------------------------------------------------- 1 | --style=numpydoc 2 | --no-final-period 3 | --no-closing-quotes 4 | -------------------------------------------------------------------------------- /tests/data/format/numpydoc/numpydoc_style_without_type.py: -------------------------------------------------------------------------------- 1 | def sincos(theta): 2 | """Returns 3 | ---- 4 | sin: float 5 | the sine of theta 6 | cos: float 7 | the cosine of theta 8 | Parameters 9 | ----- 10 | theta: 11 | the angle at which to calculate the sine and cosine. 12 | """ 13 | return math.sin(theta), math.cos(theta) 14 | -------------------------------------------------------------------------------- /tests/data/format/numpydoc/numpydoc_style_without_type.py.out: -------------------------------------------------------------------------------- 1 | def sincos(theta): 2 | """Parameters 3 | ------------- 4 | theta: 5 | the angle at which to calculate the sine and cosine. 6 | 7 | Returns 8 | ------- 9 | sin : float 10 | the sine of theta 11 | cos : float 12 | the cosine of theta 13 | """ 14 | return math.sin(theta), math.cos(theta) 15 | -------------------------------------------------------------------------------- /tests/data/format/quotes_type/class_docstring.py: -------------------------------------------------------------------------------- 1 | class MyClass: 2 | ''' A multi-line 3 | docstring 4 | ''' 5 | 6 | class InnerClass: 7 | ''' A multi-line 8 | docstring 9 | ''' 10 | -------------------------------------------------------------------------------- /tests/data/format/quotes_type/class_docstring.py.out: -------------------------------------------------------------------------------- 1 | class MyClass: 2 | """A multi-line. 3 | 4 | docstring 5 | """ 6 | 7 | class InnerClass: 8 | """A multi-line. 9 | 10 | docstring 11 | """ 12 | -------------------------------------------------------------------------------- /tests/data/format/quotes_type/function_docstrings.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | ''' A docstring. ''' 3 | 4 | def inner_func(): 5 | ''' A docstring. ''' 6 | 7 | 8 | def func(): 9 | ''' A multi-line 10 | docstring 11 | ''' 12 | 13 | def inner_func(): 14 | ''' A multi-line 15 | docstring 16 | ''' 17 | -------------------------------------------------------------------------------- /tests/data/format/quotes_type/function_docstrings.py.out: -------------------------------------------------------------------------------- 1 | def func(): 2 | """A docstring.""" 3 | 4 | def inner_func(): 5 | """A docstring.""" 6 | 7 | 8 | def func(): 9 | """A multi-line. 10 | 11 | docstring 12 | """ 13 | 14 | def inner_func(): 15 | """A multi-line. 16 | 17 | docstring 18 | """ 19 | -------------------------------------------------------------------------------- /tests/data/format/quotes_type/module_docstring.py: -------------------------------------------------------------------------------- 1 | ''' A multi-line 2 | docstring 3 | ''' 4 | 5 | ''' A multi-line 6 | docstring 7 | ''' 8 | 9 | ''' My docstring. 10 | 11 | My indented section 12 | ''' 13 | -------------------------------------------------------------------------------- /tests/data/format/quotes_type/module_docstring.py.out: -------------------------------------------------------------------------------- 1 | """A multi-line. 2 | 3 | docstring 4 | """ 5 | 6 | """A multi-line. 7 | 8 | docstring 9 | """ 10 | 11 | """My docstring. 12 | 13 | My indented section 14 | """ 15 | -------------------------------------------------------------------------------- /tests/data/format/quotes_type/single_quote_docstring.args: -------------------------------------------------------------------------------- 1 | --no-quotes-type 2 | -------------------------------------------------------------------------------- /tests/data/format/quotes_type/single_quote_docstring.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | " A docstring. " 3 | -------------------------------------------------------------------------------- /tests/data/format/quotes_type/single_quote_docstring.py.out: -------------------------------------------------------------------------------- 1 | def func(): 2 | "A docstring." 3 | -------------------------------------------------------------------------------- /tests/data/format/quotes_type/variable_docstring.py: -------------------------------------------------------------------------------- 1 | MYVAR = 1 2 | ''' A multi-line 3 | docstring 4 | ''' 5 | 6 | MYVAR = 1 7 | ''' A multi-line 8 | docstring 9 | ''' 10 | -------------------------------------------------------------------------------- /tests/data/format/quotes_type/variable_docstring.py.out: -------------------------------------------------------------------------------- 1 | MYVAR = 1 2 | """A multi-line. 3 | 4 | docstring 5 | """ 6 | 7 | MYVAR = 1 8 | """A multi-line. 9 | 10 | docstring 11 | """ 12 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/class_docstring.py: -------------------------------------------------------------------------------- 1 | class MyClass: 2 | """Summary. Body.""" 3 | 4 | class InnerClass: 5 | """Summary. Body.""" 6 | 7 | 8 | class MyClass: 9 | """Summary without period Body.""" 10 | 11 | class InnerClass: 12 | """Summary without period Body.""" 13 | 14 | 15 | class MyClass: 16 | """Summary. Body without period""" 17 | 18 | class InnerClass: 19 | """Summary. Body without period""" 20 | 21 | 22 | class MyClass: 23 | """Summary. 24 | Body without period""" 25 | 26 | class InnerClass: 27 | """Summary. 28 | Body without period""" 29 | 30 | 31 | class MyClass: 32 | """Summary over multiple 33 | lines. 34 | """ 35 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/class_docstring.py.out: -------------------------------------------------------------------------------- 1 | class MyClass: 2 | """Summary. 3 | 4 | Body. 5 | """ 6 | 7 | class InnerClass: 8 | """Summary. 9 | 10 | Body. 11 | """ 12 | 13 | 14 | class MyClass: 15 | """Summary without period Body.""" 16 | 17 | class InnerClass: 18 | """Summary without period Body.""" 19 | 20 | 21 | class MyClass: 22 | """Summary. 23 | 24 | Body without period 25 | """ 26 | 27 | class InnerClass: 28 | """Summary. 29 | 30 | Body without period 31 | """ 32 | 33 | 34 | class MyClass: 35 | """Summary. 36 | 37 | Body without period 38 | """ 39 | 40 | class InnerClass: 41 | """Summary. 42 | 43 | Body without period 44 | """ 45 | 46 | 47 | class MyClass: 48 | """Summary over multiple. 49 | 50 | lines. 51 | """ 52 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/dots.args: -------------------------------------------------------------------------------- 1 | --max-summary-lines=2 2 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/dots.py: -------------------------------------------------------------------------------- 1 | class MyClass: 2 | """We shouldn't split sys.path.""" 3 | 4 | 5 | def func(): 6 | """We should not add extra lines after the dot 7 | to it. 8 | """ 9 | 10 | 11 | def func(): 12 | """We should not split for e.g. here.""" 13 | 14 | 15 | def func(): 16 | """We should not split for i.e. here.""" 17 | 18 | 19 | def func(): 20 | """We should not add line after i.e., sys.path, e.g., etc. but etc. in particular 21 | 22 | is harder right ? 23 | """ 24 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/dots.py.out: -------------------------------------------------------------------------------- 1 | class MyClass: 2 | """We shouldn't split sys.path.""" 3 | 4 | 5 | def func(): 6 | """We should not add extra lines after the dot 7 | to it. 8 | """ 9 | 10 | 11 | def func(): 12 | """We should not split for e.g. here.""" 13 | 14 | 15 | def func(): 16 | """We should not split for i.e. here.""" 17 | 18 | 19 | def func(): 20 | """We should not add line after i.e., sys.path, e.g., etc. but etc. in particular. 21 | 22 | is harder right ? 23 | """ 24 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/empty_summary.args: -------------------------------------------------------------------------------- 1 | --linewrap-full-docstring 2 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/empty_summary.py: -------------------------------------------------------------------------------- 1 | class GithubIssue151: 2 | """ """ 3 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/empty_summary.py.out: -------------------------------------------------------------------------------- 1 | class GithubIssue151: 2 | """""" 3 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/ending_quotes.args: -------------------------------------------------------------------------------- 1 | --no-closing-quotes 2 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/ending_quotes.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | """We should keep the position of the quotes 3 | as is. 4 | """ 5 | 6 | 7 | def func(): 8 | """We should keep the position of the quotes 9 | as is.""" 10 | 11 | 12 | def func(): 13 | """We should keep the position of the quotes 14 | 15 | as is. 16 | """ 17 | 18 | 19 | def func(): 20 | """We should keep the position of the quotes 21 | 22 | as is.""" 23 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/ending_quotes.py.out: -------------------------------------------------------------------------------- 1 | def func(): 2 | """We should keep the position of the quotes. 3 | 4 | as is. 5 | """ 6 | 7 | 8 | def func(): 9 | """We should keep the position of the quotes. 10 | 11 | as is.""" 12 | 13 | 14 | def func(): 15 | """We should keep the position of the quotes. 16 | 17 | as is. 18 | """ 19 | 20 | 21 | def func(): 22 | """We should keep the position of the quotes. 23 | 24 | as is.""" 25 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/function_docstrings.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | """Summary. Body.""" 3 | 4 | def inner_func(): 5 | """Summary. Body.""" 6 | 7 | 8 | def func(): 9 | """Summary without period Body.""" 10 | 11 | def inner_func(): 12 | """Summary without period Body.""" 13 | 14 | 15 | def func(): 16 | """Summary. Body without period""" 17 | 18 | def inner_func(): 19 | """Summary. Body without period""" 20 | 21 | 22 | def func(): 23 | """Summary. 24 | Body without period""" 25 | 26 | def inner_func(): 27 | """Summary. 28 | Body without period""" 29 | 30 | 31 | def func(): 32 | """ 33 | Summary. 34 | 35 | Body of the docstring. 36 | """ 37 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/function_docstrings.py.out: -------------------------------------------------------------------------------- 1 | def func(): 2 | """Summary. 3 | 4 | Body. 5 | """ 6 | 7 | def inner_func(): 8 | """Summary. 9 | 10 | Body. 11 | """ 12 | 13 | 14 | def func(): 15 | """Summary without period Body.""" 16 | 17 | def inner_func(): 18 | """Summary without period Body.""" 19 | 20 | 21 | def func(): 22 | """Summary. 23 | 24 | Body without period 25 | """ 26 | 27 | def inner_func(): 28 | """Summary. 29 | 30 | Body without period 31 | """ 32 | 33 | 34 | def func(): 35 | """Summary. 36 | 37 | Body without period 38 | """ 39 | 40 | def inner_func(): 41 | """Summary. 42 | 43 | Body without period 44 | """ 45 | 46 | 47 | def func(): 48 | """ 49 | Summary. 50 | 51 | Body of the docstring. 52 | """ 53 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/max_summary_lines/max_lines_is_1.args: -------------------------------------------------------------------------------- 1 | --max-summary-lines=1 2 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/max_summary_lines/max_lines_is_1.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | """My long 3 | summary 4 | is way 5 | too long. 6 | 7 | Description 8 | """ 9 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/max_summary_lines/max_lines_is_1.py.out: -------------------------------------------------------------------------------- 1 | def func(): 2 | """My long. 3 | 4 | summary 5 | is way 6 | too long. 7 | 8 | Description 9 | """ 10 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/max_summary_lines/max_lines_is_2.args: -------------------------------------------------------------------------------- 1 | --max-summary-lines=2 2 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/max_summary_lines/max_lines_is_2.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | """My long 3 | summary 4 | is way 5 | too long. 6 | 7 | Description 8 | """ 9 | 10 | 11 | def func(): 12 | """A long summary 13 | without a period 14 | """ 15 | 16 | 17 | def func(): 18 | """A summary. 19 | 20 | A body 21 | """ 22 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/max_summary_lines/max_lines_is_2.py.out: -------------------------------------------------------------------------------- 1 | def func(): 2 | """My long 3 | summary. 4 | 5 | is way 6 | too long. 7 | 8 | Description 9 | """ 10 | 11 | 12 | def func(): 13 | """A long summary 14 | without a period. 15 | """ 16 | 17 | 18 | def func(): 19 | """A summary. 20 | 21 | A body 22 | """ 23 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/max_summary_lines/max_lines_is_3.args: -------------------------------------------------------------------------------- 1 | --max-summary-lines=3 2 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/max_summary_lines/max_lines_is_3.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | """My long 3 | summary 4 | is way 5 | too long. 6 | 7 | Description 8 | """ 9 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/max_summary_lines/max_lines_is_3.py.out: -------------------------------------------------------------------------------- 1 | def func(): 2 | """My long 3 | summary 4 | is way. 5 | 6 | too long. 7 | 8 | Description 9 | """ 10 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/max_summary_lines/max_lines_is_default.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | """My long 3 | summary 4 | is way 5 | too long. 6 | 7 | Description 8 | """ 9 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/max_summary_lines/max_lines_is_default.py.out: -------------------------------------------------------------------------------- 1 | def func(): 2 | """My long. 3 | 4 | summary 5 | is way 6 | too long. 7 | 8 | Description 9 | """ 10 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/max_summary_lines/max_lines_with_dot.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | """My long. summary 3 | is way 4 | too long. 5 | 6 | Description 7 | """ 8 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/max_summary_lines/max_lines_with_dot.py.out: -------------------------------------------------------------------------------- 1 | def func(): 2 | """My long. 3 | 4 | summary 5 | is way 6 | too long. 7 | 8 | Description 9 | """ 10 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/module_docstring.py: -------------------------------------------------------------------------------- 1 | """Summary. Body.""" 2 | 3 | """Summary without period Body.""" 4 | 5 | """Summary. Body without period""" 6 | 7 | """Summary. 8 | Body without period""" 9 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/module_docstring.py.out: -------------------------------------------------------------------------------- 1 | """Summary. 2 | 3 | Body. 4 | """ 5 | 6 | """Summary without period Body.""" 7 | 8 | """Summary. 9 | 10 | Body without period 11 | """ 12 | 13 | """Summary. 14 | 15 | Body without period 16 | """ 17 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/regression_151.py: -------------------------------------------------------------------------------- 1 | class GithubIssue151: 2 | """ """ 3 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/regression_151.py.out: -------------------------------------------------------------------------------- 1 | class GithubIssue151: 2 | """""" 3 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/variable_docstring.py: -------------------------------------------------------------------------------- 1 | MYVAR = 1 2 | """Summary. Body.""" 3 | 4 | MYVAR = 1 5 | """Summary without period Body.""" 6 | 7 | MYVAR = 1 8 | """Summary. Body without period""" 9 | 10 | MYVAR = 1 11 | """Summary. 12 | Body without period""" 13 | -------------------------------------------------------------------------------- /tests/data/format/summary_splitter/variable_docstring.py.out: -------------------------------------------------------------------------------- 1 | MYVAR = 1 2 | """Summary. 3 | 4 | Body. 5 | """ 6 | 7 | MYVAR = 1 8 | """Summary without period Body.""" 9 | 10 | MYVAR = 1 11 | """Summary. 12 | 13 | Body without period 14 | """ 15 | 16 | MYVAR = 1 17 | """Summary. 18 | 19 | Body without period 20 | """ 21 | -------------------------------------------------------------------------------- /tests/data/format/whitespace_stripper/class_docstring.py: -------------------------------------------------------------------------------- 1 | class MyClass: 2 | """ A multi-line 3 | docstring 4 | """ 5 | 6 | class InnerClass: 7 | """ A multi-line 8 | docstring 9 | """ 10 | -------------------------------------------------------------------------------- /tests/data/format/whitespace_stripper/class_docstring.py.out: -------------------------------------------------------------------------------- 1 | class MyClass: 2 | """A multi-line. 3 | 4 | docstring 5 | """ 6 | 7 | class InnerClass: 8 | """A multi-line. 9 | 10 | docstring 11 | """ 12 | -------------------------------------------------------------------------------- /tests/data/format/whitespace_stripper/function_docstrings.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | """ A docstring. """ 3 | 4 | def inner_func(): 5 | """ A docstring. """ 6 | 7 | 8 | def func(): 9 | """ A multi-line 10 | docstring 11 | """ 12 | 13 | def inner_func(): 14 | """ A multi-line 15 | docstring 16 | """ 17 | 18 | 19 | def func(): 20 | """Summary. 21 | 22 | Body. 23 | 24 | """ 25 | 26 | def inner_func(): 27 | """Summary. 28 | 29 | Body. 30 | 31 | 32 | 33 | """ 34 | 35 | def inner_func(): 36 | """Summary. 37 | 38 | 39 | Body. 40 | 41 | """ 42 | -------------------------------------------------------------------------------- /tests/data/format/whitespace_stripper/function_docstrings.py.out: -------------------------------------------------------------------------------- 1 | def func(): 2 | """A docstring.""" 3 | 4 | def inner_func(): 5 | """A docstring.""" 6 | 7 | 8 | def func(): 9 | """A multi-line. 10 | 11 | docstring 12 | """ 13 | 14 | def inner_func(): 15 | """A multi-line. 16 | 17 | docstring 18 | """ 19 | 20 | 21 | def func(): 22 | """Summary. 23 | 24 | Body. 25 | """ 26 | 27 | def inner_func(): 28 | """Summary. 29 | 30 | Body. 31 | """ 32 | 33 | def inner_func(): 34 | """Summary. 35 | 36 | Body. 37 | """ 38 | -------------------------------------------------------------------------------- /tests/data/format/whitespace_stripper/module_docstring.py: -------------------------------------------------------------------------------- 1 | """ A multi-line 2 | docstring 3 | """ 4 | 5 | """ A multi-line 6 | docstring 7 | """ 8 | 9 | """ My docstring. 10 | 11 | My indented section 12 | """ 13 | -------------------------------------------------------------------------------- /tests/data/format/whitespace_stripper/module_docstring.py.out: -------------------------------------------------------------------------------- 1 | """A multi-line. 2 | 3 | docstring 4 | """ 5 | 6 | """A multi-line. 7 | 8 | docstring 9 | """ 10 | 11 | """My docstring. 12 | 13 | My indented section 14 | """ 15 | -------------------------------------------------------------------------------- /tests/data/format/whitespace_stripper/single_quote_docstring.args: -------------------------------------------------------------------------------- 1 | --no-quotes-type 2 | -------------------------------------------------------------------------------- /tests/data/format/whitespace_stripper/single_quote_docstring.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | " A docstring. " 3 | -------------------------------------------------------------------------------- /tests/data/format/whitespace_stripper/single_quote_docstring.py.out: -------------------------------------------------------------------------------- 1 | def func(): 2 | "A docstring." 3 | -------------------------------------------------------------------------------- /tests/data/format/whitespace_stripper/variable_docstring.py: -------------------------------------------------------------------------------- 1 | MYVAR = 1 2 | """ A multi-line 3 | docstring 4 | """ 5 | 6 | MYVAR = 1 7 | """ A multi-line 8 | docstring 9 | """ 10 | -------------------------------------------------------------------------------- /tests/data/format/whitespace_stripper/variable_docstring.py.out: -------------------------------------------------------------------------------- 1 | MYVAR = 1 2 | """A multi-line. 3 | 4 | docstring 5 | """ 6 | 7 | MYVAR = 1 8 | """A multi-line. 9 | 10 | docstring 11 | """ 12 | -------------------------------------------------------------------------------- /tests/data/incorrect_python_file.py: -------------------------------------------------------------------------------- 1 | var = """Unclosed string 2 | -------------------------------------------------------------------------------- /tests/data/utils/find_docstrings/dictionary.py: -------------------------------------------------------------------------------- 1 | MY_DICT = { 2 | "key": "value", 3 | } 4 | -------------------------------------------------------------------------------- /tests/data/utils/find_docstrings/function_docstrings.py: -------------------------------------------------------------------------------- 1 | def func(): 2 | """A docstring""" 3 | 4 | def inner_func(): 5 | """A docstring""" 6 | 7 | 8 | def func(): 9 | """A multi-line 10 | docstring""" 11 | 12 | def inner_func(): 13 | """A multi-line 14 | docstring""" 15 | 16 | 17 | def func(): 18 | """A multi-line 19 | docstring 20 | """ 21 | 22 | def inner_func(): 23 | """A multi-line 24 | docstring 25 | """ 26 | 27 | 28 | def func(): 29 | '''A docstring''' 30 | 31 | def inner_func(): 32 | '''A docstring''' 33 | 34 | 35 | def func(): 36 | '''A multi-line 37 | docstring''' 38 | 39 | def inner_func(): 40 | '''A multi-line 41 | docstring''' 42 | 43 | 44 | def func(): 45 | '''A multi-line 46 | docstring 47 | ''' 48 | 49 | def inner_func(): 50 | '''A multi-line 51 | docstring 52 | ''' 53 | -------------------------------------------------------------------------------- /tests/data/utils/find_docstrings/module_docstrings.py: -------------------------------------------------------------------------------- 1 | # Some comments 2 | 3 | """A multi-line docstring 4 | """ 5 | -------------------------------------------------------------------------------- /tests/data/utils/find_nothing/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanielNoord/pydocstringformatter/60e016d59f4b02cdd07a0f1a9492b807be76ed85/tests/data/utils/find_nothing/README.md -------------------------------------------------------------------------------- /tests/data/utils/find_recursive_files/file_one.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanielNoord/pydocstringformatter/60e016d59f4b02cdd07a0f1a9492b807be76ed85/tests/data/utils/find_recursive_files/file_one.py -------------------------------------------------------------------------------- /tests/data/utils/find_recursive_files/file_two.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanielNoord/pydocstringformatter/60e016d59f4b02cdd07a0f1a9492b807be76ed85/tests/data/utils/find_recursive_files/file_two.txt -------------------------------------------------------------------------------- /tests/data/utils/find_recursive_files/inner_directory/inner_file_one.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanielNoord/pydocstringformatter/60e016d59f4b02cdd07a0f1a9492b807be76ed85/tests/data/utils/find_recursive_files/inner_directory/inner_file_one.py -------------------------------------------------------------------------------- /tests/data/utils/find_recursive_files/inner_directory/inner_file_two.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanielNoord/pydocstringformatter/60e016d59f4b02cdd07a0f1a9492b807be76ed85/tests/data/utils/find_recursive_files/inner_directory/inner_file_two.txt -------------------------------------------------------------------------------- /tests/data/utils/find_recursive_files/inner_directory/inner_inner_directory/inner_inner_file_one.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanielNoord/pydocstringformatter/60e016d59f4b02cdd07a0f1a9492b807be76ed85/tests/data/utils/find_recursive_files/inner_directory/inner_inner_directory/inner_inner_file_one.py -------------------------------------------------------------------------------- /tests/data/utils/find_recursive_files/inner_directory/inner_inner_directory/inner_inner_file_one.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanielNoord/pydocstringformatter/60e016d59f4b02cdd07a0f1a9492b807be76ed85/tests/data/utils/find_recursive_files/inner_directory/inner_inner_directory/inner_inner_file_one.txt -------------------------------------------------------------------------------- /tests/data/utils/find_underscore_files/____file_five.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanielNoord/pydocstringformatter/60e016d59f4b02cdd07a0f1a9492b807be76ed85/tests/data/utils/find_underscore_files/____file_five.py -------------------------------------------------------------------------------- /tests/data/utils/find_underscore_files/___file_four.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanielNoord/pydocstringformatter/60e016d59f4b02cdd07a0f1a9492b807be76ed85/tests/data/utils/find_underscore_files/___file_four.txt -------------------------------------------------------------------------------- /tests/data/utils/find_underscore_files/__file_three.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanielNoord/pydocstringformatter/60e016d59f4b02cdd07a0f1a9492b807be76ed85/tests/data/utils/find_underscore_files/__file_three.py -------------------------------------------------------------------------------- /tests/data/utils/find_underscore_files/_file_two.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanielNoord/pydocstringformatter/60e016d59f4b02cdd07a0f1a9492b807be76ed85/tests/data/utils/find_underscore_files/_file_two.py -------------------------------------------------------------------------------- /tests/data/utils/find_underscore_files/file_one.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanielNoord/pydocstringformatter/60e016d59f4b02cdd07a0f1a9492b807be76ed85/tests/data/utils/find_underscore_files/file_one.py -------------------------------------------------------------------------------- /tests/test_conflicting_formatters.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Generator 4 | from contextlib import contextmanager 5 | from pathlib import Path 6 | 7 | import pytest 8 | 9 | from pydocstringformatter import _formatting 10 | from pydocstringformatter import _testutils as test_utils 11 | from pydocstringformatter._formatting import Formatter 12 | from pydocstringformatter._utils import UnstableResultError 13 | from pydocstringformatter.run import _Run 14 | 15 | 16 | @contextmanager 17 | def patched_run(formatters: list[Formatter]) -> Generator[type[_Run]]: 18 | """Patches formatters so Run only uses the provided formatters.""" 19 | old_formatters = _formatting.FORMATTERS 20 | _formatting.FORMATTERS = formatters 21 | yield _Run 22 | _formatting.FORMATTERS = old_formatters 23 | 24 | 25 | @pytest.mark.parametrize( 26 | "formatters,expected_errors", 27 | [ 28 | ( 29 | [test_utils.MakeAFormatter(), test_utils.MakeBFormatter()], 30 | ["Conflicting formatters"], 31 | ), 32 | ( 33 | [test_utils.MakeBFormatter(), test_utils.AddBFormatter()], 34 | ["not able to make stable changes"], 35 | ), 36 | ( 37 | [ 38 | test_utils.MakeBFormatter(), 39 | test_utils.AddBFormatter(), 40 | test_utils.MakeAFormatter(), 41 | ], 42 | ["Conflicting formatters:", "Diff too intricate to compute"], 43 | ), 44 | ], 45 | ) 46 | def test_conflicting_formatters( 47 | formatters: list[Formatter], 48 | expected_errors: list[str], 49 | tmp_path: Path, 50 | ) -> None: 51 | """Tests that conflicting formatters raise an error.""" 52 | tmp_file = tmp_path / "test.py" 53 | with open(tmp_file, "w", encoding="utf-8") as f: 54 | content = [ 55 | '"""AAA AA AAA"""', 56 | ] 57 | f.writelines(content) 58 | 59 | with patched_run(formatters) as run: 60 | with pytest.raises(UnstableResultError) as err: 61 | run([str(tmp_file)]) 62 | 63 | for expect_err in expected_errors: 64 | assert expect_err in str(err.value), str(err.value) 65 | -------------------------------------------------------------------------------- /tests/test_exceptions.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | 5 | from pydocstringformatter import _utils, run_docstring_formatter 6 | 7 | HERE = Path(__file__) 8 | DATA = HERE.parent / "data" 9 | 10 | 11 | def test_exception_on_incorrect_python() -> None: 12 | """Test that we raise the correct exception on unparsable python files.""" 13 | with pytest.raises(_utils.ParsingError): 14 | run_docstring_formatter([str(DATA / "incorrect_python_file.py")]) 15 | -------------------------------------------------------------------------------- /tests/test_formatter.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pydocstringformatter._formatting import FORMATTERS 4 | 5 | 6 | def test_formatter_names() -> None: 7 | """Test that each formatter name exists and is unique.""" 8 | formatter_names: set[str] = set() 9 | for formatter in FORMATTERS: 10 | assert formatter.name, "Each formatter should have a name set." 11 | assert ( 12 | formatter.name not in formatter_names 13 | ), "Each formatter should have an unique name." 14 | formatter_names.add(formatter.name) 15 | -------------------------------------------------------------------------------- /tests/test_formatting.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import pathlib 5 | from pathlib import Path 6 | 7 | import pytest 8 | 9 | import pydocstringformatter 10 | 11 | HERE = Path(__file__) 12 | TEST_DATA = HERE.parent / "data" / "format" 13 | 14 | # Get all the test files 15 | TESTS: list[str] = [] 16 | TEST_NAMES: list[str] = [] 17 | for dirname, _, files in os.walk(TEST_DATA): 18 | for file in files: 19 | if file.endswith(".py"): 20 | dirpath = Path(dirname) 21 | TESTS.append(str(dirpath / file)) 22 | TEST_NAMES.append(f"{dirpath.stem}-{file}") 23 | 24 | 25 | @pytest.mark.parametrize( 26 | "test_file", 27 | TESTS, 28 | ids=TEST_NAMES, 29 | ) 30 | def test_formatting( 31 | test_file: str, capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path 32 | ) -> None: 33 | """Test that we correctly format all files in the format directory. 34 | 35 | We create and write to a temporary file so that the original test file 36 | isn't overwritten and the 'py.out' file can represent an actual 37 | python file instead of a diff. 38 | 39 | Everything is tested in bytes as we want to preserve the type of the 40 | line endings of the original file. 41 | """ 42 | # Setup 43 | temp_file_name = str(tmp_path / "test_file.py") 44 | 45 | # Check if there is a specific output for the standard newline of this OS. 46 | # For example, this allows testing that in files without any initial newlines 47 | # we use the standard newline whenever we need one. 48 | test_name = f"{test_file}-{os.linesep.encode().hex()}.out" 49 | if not os.path.isfile(test_name): 50 | # This is the regular output file. 51 | test_name = test_file + ".out" 52 | 53 | with open(test_name, "rb") as f: 54 | expected_output = f.read() 55 | 56 | # Get original lines from test file and write to temporary file 57 | with open(test_file, "rb") as f: 58 | original_bytes = f.read() 59 | with open(temp_file_name, "wb") as f: 60 | f.write(original_bytes) 61 | 62 | # Get any additional args as specified by an .args file 63 | additional_args: list[str] = [] 64 | if os.path.exists(test_file.replace(".py", ".args")): 65 | with open(test_file.replace(".py", ".args"), encoding="utf-8") as f: 66 | additional_args = [i.rstrip("\n") for i in f.readlines()] 67 | 68 | # Get message on stderr 69 | if os.path.exists(test_file.replace(".py", ".err")): 70 | with open(test_file.replace(".py", ".err"), encoding="utf-8") as f: 71 | error_message = f.read() 72 | else: 73 | error_message = "" 74 | 75 | pydocstringformatter.run_docstring_formatter( 76 | [temp_file_name, "--write"] + additional_args 77 | ) 78 | 79 | output = capsys.readouterr() 80 | assert output.err == error_message.format(testfile=os.path.abspath(temp_file_name)) 81 | with open(temp_file_name, "rb") as f: 82 | assert f.read() == expected_output 83 | -------------------------------------------------------------------------------- /tests/test_run.py: -------------------------------------------------------------------------------- 1 | # pylint: disable = redefined-outer-name 2 | import os 3 | import sys 4 | from pathlib import Path 5 | 6 | import pytest 7 | 8 | import pydocstringformatter 9 | from pydocstringformatter._formatting import FORMATTERS 10 | from pydocstringformatter._formatting.base import StringFormatter 11 | from pydocstringformatter._formatting.formatters_pep257 import ( 12 | SplitSummaryAndDocstringFormatter, 13 | ) 14 | from pydocstringformatter._testutils import FormatterAsserter 15 | 16 | 17 | def test_no_arguments(capsys: pytest.CaptureFixture[str]) -> None: 18 | """Test that we display a help message when no arguments are provided.""" 19 | sys.argv = ["pydocstringformatter"] 20 | pydocstringformatter.run_docstring_formatter() 21 | out, err = capsys.readouterr() 22 | assert out.startswith("usage: pydocstringformatter [-h]") 23 | 24 | # Test that we print help messages for individual formatters as well 25 | # Default formatter 26 | assert "--strip-whitespaces" in out 27 | assert "Activate or deactivate strip-whitespaces" in out 28 | # Optional formatter 29 | assert "--split-summary-body" in out 30 | assert "Activate or deactivate split-summary-body" in out 31 | assert not err 32 | 33 | 34 | def test_formatter_help_categories(capsys: pytest.CaptureFixture[str]) -> None: 35 | """Test that formatter messages are in the correct group.""" 36 | 37 | class OptionalFormatter(StringFormatter): # pylint: disable=abstract-method 38 | """An optional formatter.""" 39 | 40 | name = "optional-formatter" 41 | optional = True 42 | 43 | class NonOptionalFormatter(StringFormatter): # pylint: disable=abstract-method 44 | """A non-optional formatter.""" 45 | 46 | name = "non-optional-formatter" 47 | 48 | FORMATTERS.append(OptionalFormatter()) # type: ignore[abstract] 49 | FORMATTERS.append(NonOptionalFormatter()) # type: ignore[abstract] 50 | sys.argv = ["pydocstringformatter"] 51 | pydocstringformatter.run_docstring_formatter() 52 | out, _ = capsys.readouterr() 53 | categories = out.replace("\n\n ", "\n").split("\n\n") 54 | for category in categories: 55 | if category.startswith("optional formatters"): 56 | assert "--optional-formatter" in category 57 | assert "--non-optional-formatter" not in category 58 | elif category.startswith("default formatters"): 59 | assert "--optional-formatter" not in category 60 | assert "--non-optional-formatter" in category 61 | FORMATTERS.pop() 62 | FORMATTERS.pop() 63 | 64 | 65 | def test_sys_agv_as_arguments( 66 | capsys: pytest.CaptureFixture[str], test_file: str 67 | ) -> None: 68 | """Test running with arguments in sys.argv.""" 69 | sys.argv = ["pydocstringformatter", test_file] 70 | pydocstringformatter.run_docstring_formatter() 71 | 72 | with open(test_file, encoding="utf-8") as file: 73 | assert "".join(file.readlines()) == '"""A multi-line\ndocstring."""' 74 | 75 | output = capsys.readouterr() 76 | assert output.out.endswith( 77 | ''' 78 | @@ -1,2 +1,4 @@ 79 | -"""A multi-line 80 | -docstring.""" 81 | +"""A multi-line. 82 | + 83 | +docstring. 84 | +""" 85 | ''' 86 | ) 87 | assert not output.err 88 | 89 | 90 | def test_output_message_nothing_done( 91 | capsys: pytest.CaptureFixture[str], test_file: str 92 | ) -> None: 93 | """Test that we emit the correct message when nothing was done.""" 94 | with open(test_file, "w", encoding="utf-8") as file: 95 | file.write('"""A multi-line.\n\ndocstring.\n"""') 96 | with open(test_file.replace(".py", "2.py"), "w", encoding="utf-8") as file: 97 | file.write('"""A multi-line.\n\ndocstring.\n"""') 98 | 99 | pydocstringformatter.run_docstring_formatter( 100 | [str(Path(test_file).parent), "--write"] 101 | ) 102 | 103 | output = capsys.readouterr() 104 | assert output.out == "Nothing to do! All docstrings in 2 files are correct 🎉\n" 105 | assert not output.err 106 | 107 | 108 | def test_output_message_one_file( 109 | capsys: pytest.CaptureFixture[str], test_file: str 110 | ) -> None: 111 | """Test that we emit the correct message when one out of two files was formatted.""" 112 | try: 113 | expected_path = os.path.relpath(test_file) 114 | except ValueError: 115 | expected_path = test_file 116 | 117 | with open(test_file.replace(".py", "2.py"), "w", encoding="utf-8") as file: 118 | file.write('"""A multi-line.\n\ndocstring.\n"""') 119 | 120 | pydocstringformatter.run_docstring_formatter( 121 | [str(Path(test_file).parent), "--write"] 122 | ) 123 | 124 | output = capsys.readouterr() 125 | assert output.out == f"Formatted {expected_path} 📖\n" 126 | assert not output.err 127 | 128 | 129 | def test_output_message_two_files( 130 | capsys: pytest.CaptureFixture[str], test_file: str 131 | ) -> None: 132 | """Test that we emit the correct message when two files were formatted.""" 133 | second_file = test_file.replace(".py", "2.py") 134 | 135 | try: 136 | expected_path = os.path.relpath(test_file) 137 | expected_second_path = os.path.relpath(second_file) 138 | except ValueError: 139 | expected_path = test_file 140 | expected_second_path = second_file 141 | 142 | with open(second_file, "w", encoding="utf-8") as file: 143 | file.write('"""A multi-line\ndocstring"""') 144 | 145 | pydocstringformatter.run_docstring_formatter( 146 | [str(Path(test_file).parent), "--write"] 147 | ) 148 | 149 | output = capsys.readouterr() 150 | assert ( 151 | output.out 152 | == f"""Formatted {expected_path} 📖 153 | Formatted {expected_second_path} 📖 154 | """ 155 | ) 156 | assert not output.err 157 | 158 | 159 | def test_begin_quote_formatters( 160 | capsys: pytest.CaptureFixture[str], tmp_path: Path 161 | ) -> None: 162 | """Test that (optional) formatters are activated or not depending on options.""" 163 | with FormatterAsserter( 164 | f'"""{"a" * 120}\n{"b" * 120}"""', FORMATTERS, capsys, tmp_path 165 | ) as asserter: 166 | asserter.assert_format_when_activated() 167 | asserter.assert_no_change_when_deactivated() 168 | 169 | 170 | def test_optional_formatters_argument( 171 | capsys: pytest.CaptureFixture[str], tmp_path: Path 172 | ) -> None: 173 | """Test that an optional formatter is correctly turned on and off with arguments.""" 174 | with FormatterAsserter( 175 | '"""Summary. Body."""', [SplitSummaryAndDocstringFormatter()], capsys, tmp_path 176 | ) as asserter: 177 | asserter.assert_format_when_activated() 178 | asserter.assert_no_change_when_deactivated() 179 | 180 | 181 | class TestExitCodes: 182 | """Tests for the --exit-code option.""" 183 | 184 | @staticmethod 185 | def test_exit_code_with_write(test_file: str) -> None: 186 | """Test that we emit the correct exit code in write mode.""" 187 | with pytest.raises(SystemExit) as exit_exec: 188 | pydocstringformatter.run_docstring_formatter( 189 | [str(Path(test_file)), "--write", "--exit-code"] 190 | ) 191 | 192 | assert exit_exec.value.code == 32 193 | 194 | # After first writing changes, now we expect no changes 195 | with pytest.raises(SystemExit) as exit_exec: 196 | pydocstringformatter.run_docstring_formatter( 197 | [str(Path(test_file)), "--write", "--exit-code"] 198 | ) 199 | 200 | assert not exit_exec.value.code 201 | 202 | @staticmethod 203 | def test_exit_code_without_write(test_file: str) -> None: 204 | """Test that we emit the correct exit code in write mode.""" 205 | with pytest.raises(SystemExit) as exit_exec: 206 | pydocstringformatter.run_docstring_formatter( 207 | [str(Path(test_file)), "--exit-code"] 208 | ) 209 | 210 | assert exit_exec.value.code == 32 211 | 212 | # We expect an exit code on both occassions 213 | with pytest.raises(SystemExit) as exit_exec: 214 | pydocstringformatter.run_docstring_formatter( 215 | [str(Path(test_file)), "--exit-code"] 216 | ) 217 | 218 | assert exit_exec.value.code == 32 219 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | import tokenize 5 | from pathlib import Path 6 | 7 | import pytest 8 | 9 | import pydocstringformatter 10 | from pydocstringformatter._testutils import MakeAFormatter, MakeBFormatter 11 | from pydocstringformatter._utils import ( 12 | compare_formatters, 13 | find_python_files, 14 | is_docstring, 15 | ) 16 | 17 | HERE = Path(__file__) 18 | UTILS_DATA = HERE.parent / "data" / "utils" 19 | 20 | 21 | class TestPythonFileFinder: 22 | """Test the python file finder.""" 23 | 24 | @staticmethod 25 | def test_underscores_files() -> None: 26 | """Test that we can find files with leading underscores.""" 27 | pathnames = find_python_files([str(UTILS_DATA / "find_underscore_files")], []) 28 | expected_paths = [ 29 | UTILS_DATA / "find_underscore_files" / "file_one.py", 30 | UTILS_DATA / "find_underscore_files" / "_file_two.py", 31 | UTILS_DATA / "find_underscore_files" / "__file_three.py", 32 | UTILS_DATA / "find_underscore_files" / "____file_five.py", 33 | ] 34 | assert sorted(expected_paths) == pathnames 35 | 36 | @staticmethod 37 | def test_recursive_files() -> None: 38 | """Test that we can find files recursively.""" 39 | pathnames = find_python_files( 40 | [str(UTILS_DATA / "find_recursive_files")], [], recursive=True 41 | ) 42 | expected_paths = [ 43 | UTILS_DATA / "find_recursive_files" / "file_one.py", 44 | UTILS_DATA 45 | / "find_recursive_files" 46 | / "inner_directory" 47 | / "inner_file_one.py", 48 | UTILS_DATA 49 | / "find_recursive_files" 50 | / "inner_directory" 51 | / "inner_inner_directory" 52 | / "inner_inner_file_one.py", 53 | ] 54 | assert sorted(expected_paths) == pathnames 55 | 56 | @staticmethod 57 | def test_recursive_files_standard() -> None: 58 | """Test that we can find files recursively even if argument is not supplied.""" 59 | pathnames = find_python_files([str(UTILS_DATA / "find_recursive_files")], []) 60 | expected_paths = [ 61 | UTILS_DATA / "find_recursive_files" / "file_one.py", 62 | UTILS_DATA 63 | / "find_recursive_files" 64 | / "inner_directory" 65 | / "inner_file_one.py", 66 | UTILS_DATA 67 | / "find_recursive_files" 68 | / "inner_directory" 69 | / "inner_inner_directory" 70 | / "inner_inner_file_one.py", 71 | ] 72 | assert sorted(expected_paths) == pathnames 73 | 74 | @staticmethod 75 | def test_ignore_recursive_files() -> None: 76 | """Test that we ignore inner directories if recusrive is False.""" 77 | pathnames = find_python_files( 78 | [str(UTILS_DATA / "find_recursive_files")], [], recursive=False 79 | ) 80 | expected_paths = [UTILS_DATA / "find_recursive_files" / "file_one.py"] 81 | assert sorted(expected_paths) == pathnames 82 | 83 | @staticmethod 84 | def test_ignore_non_python_file() -> None: 85 | """Test that we ignore a non Python file.""" 86 | pathnames = find_python_files( 87 | [str(UTILS_DATA / "find_nothing" / "README.md")], [] 88 | ) 89 | assert not pathnames 90 | 91 | 92 | class TestDocstringFinder: 93 | """Test the docstring finder.""" 94 | 95 | docstring_data = UTILS_DATA / "find_docstrings" 96 | 97 | def test_function_docstrings(self) -> None: 98 | """Test that we can find docstrings for function definitions.""" 99 | docstrings: list[tuple[tuple[int, int], tuple[int, int]]] = [] 100 | with open( 101 | self.docstring_data / "function_docstrings.py", encoding="utf-8" 102 | ) as file: 103 | tokens = list(tokenize.generate_tokens(file.readline)) 104 | for index, tokeninfo in enumerate(tokens): 105 | if is_docstring(tokeninfo, tokens[index - 1]): 106 | docstrings.append((tokeninfo.start, tokeninfo.end)) 107 | 108 | assert docstrings == [ 109 | ((2, 4), (2, 21)), 110 | ((5, 8), (5, 25)), 111 | ((9, 4), (10, 16)), 112 | ((13, 8), (14, 20)), 113 | ((18, 4), (20, 7)), 114 | ((23, 8), (25, 11)), 115 | ((29, 4), (29, 21)), 116 | ((32, 8), (32, 25)), 117 | ((36, 4), (37, 16)), 118 | ((40, 8), (41, 20)), 119 | ((45, 4), (47, 7)), 120 | ((50, 8), (52, 11)), 121 | ] 122 | 123 | def test_dictionary_key_value_line(self) -> None: 124 | """Test that string key-value pairs are not considered a docstring.""" 125 | with open(self.docstring_data / "dictionary.py", encoding="utf-8") as file: 126 | tokens = list(tokenize.generate_tokens(file.readline)) 127 | for index, tokeninfo in enumerate(tokens): 128 | assert not is_docstring(tokeninfo, tokens[index - 1]) 129 | 130 | def test_module_docstrings(self) -> None: 131 | """Test that we find the correct module docstring.""" 132 | docstrings: list[tuple[tuple[int, int], tuple[int, int]]] = [] 133 | with open( 134 | self.docstring_data / "module_docstrings.py", encoding="utf-8" 135 | ) as file: 136 | tokens = list(tokenize.generate_tokens(file.readline)) 137 | for index, tokeninfo in enumerate(tokens): 138 | if is_docstring(tokeninfo, tokens[index - 1]): 139 | docstrings.append((tokeninfo.start, tokeninfo.end)) 140 | 141 | assert docstrings == [((3, 0), (4, 3))] 142 | 143 | 144 | def test_encoding_of_console_messages( 145 | capsys: pytest.CaptureFixture[str], test_file: str 146 | ) -> None: 147 | """Test that we can print emoji's to non utf-8 consoles. 148 | 149 | Regression test for: 150 | https://github.com/DanielNoord/pydocstringformatter/issues/13 151 | """ 152 | sys.stdout.reconfigure(encoding="cp1252") # type: ignore[union-attr] 153 | with open(test_file, "w", encoding="utf-8") as file: 154 | file.write('"""A multi-line.\n\ndocstring.\n"""') 155 | 156 | pydocstringformatter.run_docstring_formatter([test_file, "--write"]) 157 | 158 | output = capsys.readouterr() 159 | assert output.out == "Nothing to do! All docstrings in 1 file are correct 🎉\n" 160 | assert not output.err 161 | 162 | 163 | def test_formatter_comparer() -> None: 164 | """Test the compare_formatters utility function.""" 165 | tokeninfo = tokenize.TokenInfo( 166 | 1, '"""AAA AA AAA"""', (1, 0), (1, 0), '"""AAA AA AAA"""' 167 | ) 168 | 169 | diff = compare_formatters(tokeninfo, MakeAFormatter(), MakeBFormatter(), "test") 170 | 171 | expected_sections = [ 172 | "--- make-a-formatter vs make-b-formatter test\n", 173 | '-"""AAA AA AAA"""\n', 174 | '+"""BBB BB BBB"""\n', 175 | ] 176 | 177 | for section in expected_sections: 178 | assert section in diff 179 | 180 | diff = compare_formatters(tokeninfo, MakeAFormatter(), MakeBFormatter()) 181 | 182 | expected_sections = [ 183 | "--- make-a-formatter vs make-b-formatter\n", 184 | '-"""AAA AA AAA"""\n', 185 | '+"""BBB BB BBB"""\n', 186 | ] 187 | 188 | for section in expected_sections: 189 | assert section in diff 190 | --------------------------------------------------------------------------------