├── tests ├── formatter │ ├── __init__.py │ └── test_do_format_code.py ├── patterns │ ├── __init__.py │ ├── test_field_patterns.py │ ├── test_list_patterns.py │ ├── test_rest_patterns.py │ ├── test_misc_patterns.py │ ├── test_url_patterns.py │ └── test_header_patterns.py ├── _data │ ├── tox.ini │ ├── setup.cfg │ ├── pyproject.toml │ └── string_files │ │ ├── misc_patterns.toml │ │ ├── encoding_functions.toml │ │ ├── field_patterns.toml │ │ ├── rest_patterns.toml │ │ ├── summary_wrappers.toml │ │ ├── utility_functions.toml │ │ ├── description_wrappers.toml │ │ ├── field_wrappers.toml │ │ ├── format_methods.toml │ │ ├── header_patterns.toml │ │ ├── list_patterns.toml │ │ ├── classify_functions.toml │ │ └── url_wrappers.toml ├── __init__.py ├── wrappers │ ├── test_url_wrapper.py │ ├── test_description_wrapper.py │ ├── test_summary_wrapper.py │ └── test_field_wrapper.py ├── test_utility_functions.py ├── test_encoding_functions.py └── test_classify_functions.py ├── docs ├── source │ ├── authors.rst │ ├── license.rst │ ├── index.rst │ ├── faq.rst │ ├── installation.rst │ ├── conf.py │ └── configuration.rst ├── images │ └── pycharm-file-watcher-configurations.png ├── Makefile └── make.bat ├── .pre-commit-hooks.yaml ├── .gitignore ├── .github ├── workflows │ ├── on-issue-open.yml │ ├── do-lint.yml │ ├── ci.yml │ ├── on-push-tag.yml │ ├── do-prioritize-issues.yml │ ├── do-update-authors.yml │ └── do-release.yml └── release-drafter.yml ├── LICENSE ├── src └── docformatter │ ├── __pkginfo__.py │ ├── wrappers │ ├── __init__.py │ ├── summary.py │ ├── url.py │ ├── description.py │ └── fields.py │ ├── patterns │ ├── __init__.py │ ├── rest.py │ ├── url.py │ ├── headers.py │ ├── misc.py │ ├── fields.py │ └── lists.py │ ├── __init__.py │ ├── encode.py │ ├── util.py │ └── __main__.py ├── .sourcery.yaml ├── .pre-commit-config.yaml ├── AUTHORS.rst ├── tox.ini └── pyproject.toml /tests/formatter/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/patterns/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/authors.rst: -------------------------------------------------------------------------------- 1 | Authors 2 | ======= 3 | 4 | .. include:: ../../AUTHORS.rst 5 | -------------------------------------------------------------------------------- /docs/source/license.rst: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | .. literalinclude:: ../../LICENSE 5 | -------------------------------------------------------------------------------- /tests/_data/tox.ini: -------------------------------------------------------------------------------- 1 | [docformatter] 2 | wrap-descriptions = 72 3 | wrap-summaries = 79 4 | blank = False 5 | -------------------------------------------------------------------------------- /tests/_data/setup.cfg: -------------------------------------------------------------------------------- 1 | [docformatter] 2 | blank = False 3 | wrap-summaries = 79 4 | wrap-descriptions = 72 5 | -------------------------------------------------------------------------------- /tests/_data/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.docformatter] 2 | wrap-summaries = 79 3 | wrap-descriptions = 72 4 | blank = false 5 | -------------------------------------------------------------------------------- /docs/images/pycharm-file-watcher-configurations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyCQA/docformatter/HEAD/docs/images/pycharm-file-watcher-configurations.png -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: docformatter 2 | name: docformatter 3 | description: 'Formats docstrings to follow PEP 257.' 4 | entry: docformatter 5 | args: [-i] 6 | language: python 7 | types: [python] 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.egg 3 | *.egg-info/ 4 | *.eggs/ 5 | *.pyc 6 | .*.swp 7 | .travis-solo/ 8 | MANIFEST 9 | README.html 10 | __pycache__/ 11 | build/ 12 | dist/ 13 | htmlcov/ 14 | *coverage* 15 | .python-version 16 | .idea/ 17 | .vscode/ 18 | .tox/ 19 | .venv/ 20 | -------------------------------------------------------------------------------- /.github/workflows/on-issue-open.yml: -------------------------------------------------------------------------------- 1 | # This workflow runs when a new issue is opened. 2 | # 3 | # - Apply the 'fresh' label. 4 | name: Issue Open Workflow 5 | 6 | on: 7 | issues: 8 | types: [opened] 9 | 10 | jobs: 11 | label_issue_backlog: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Add fresh new label 15 | uses: andymckay/labeler@master 16 | with: 17 | add-labels: "fresh" 18 | -------------------------------------------------------------------------------- /.github/workflows/do-lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: 3 | push: 4 | branches: 5 | - "*" 6 | pull_request: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | name: "Run linters on code base" 14 | steps: 15 | - name: Setup Python for linting 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: "3.12" 19 | - name: Install tox 20 | run: python -m pip install tox tox-gh-actions 21 | - uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 0 24 | - name: Lint code base 25 | run: tox -e pre-commit 26 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. docformatter documentation master file, created by 2 | sphinx-quickstart on Thu Aug 11 18:58:56 2022. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to docformatter! 7 | ======================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | installation 14 | usage 15 | configuration 16 | 17 | .. toctree:: 18 | :maxdepth: 2 19 | :caption: Miscellaneous: 20 | 21 | requirements 22 | faq 23 | authors 24 | license 25 | 26 | Indices and tables 27 | ================== 28 | 29 | * :ref:`genindex` 30 | * :ref:`modindex` 31 | * :ref:`search` 32 | -------------------------------------------------------------------------------- /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 = source 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/source/faq.rst: -------------------------------------------------------------------------------- 1 | 2 | Known Issues and Idiosyncrasies 3 | =============================== 4 | 5 | There are some know issues or idiosyncrasies when using ``docformatter``. 6 | These are stylistic issues and are in the process of being addressed. 7 | 8 | Wrapping Descriptions 9 | --------------------- 10 | 11 | ``docformatter`` will wrap descriptions, but only in simple cases. If there is 12 | text that seems like a bulleted/numbered list, ``docformatter`` will leave the 13 | description as is: 14 | 15 | .. code-block:: rest 16 | 17 | - Item one. 18 | - Item two. 19 | - Item three. 20 | 21 | This prevents the risk of the wrapping turning things into a mess. To force 22 | even these instances to get wrapped use ``--force-wrap``. This is being 23 | addressed by the constellation of issues related to the various syntaxes used 24 | in docstrings. 25 | -------------------------------------------------------------------------------- /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=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 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 | -------------------------------------------------------------------------------- /tests/_data/string_files/misc_patterns.toml: -------------------------------------------------------------------------------- 1 | [is_some_sort_of_code] 2 | instring = """ 3 | __________=__________(__________,__________,__________, 4 | __________[ 5 | '___'],__________,__________,__________, 6 | __________,______________=__________) 7 | """ 8 | expected = true 9 | 10 | [is_some_sort_of_code_python] 11 | instring = """ 12 | def is_some_sort_of_code(): 13 | x = 1 14 | y = 42 15 | return x + y 16 | """ 17 | expected = true 18 | 19 | [is_probably_beginning_of_sentence] 20 | instring = "- This is part of a list." 21 | expected = true 22 | 23 | [is_not_probably_beginning_of_sentence] 24 | instring = "(this just continues an existing sentence)." 25 | expected = "None" 26 | 27 | [is_probably_beginning_of_sentence_pydoc_ref] 28 | instring = ":see:MyClass This is not the start of a sentence." 29 | expected = "None" 30 | -------------------------------------------------------------------------------- /tests/_data/string_files/encoding_functions.toml: -------------------------------------------------------------------------------- 1 | [find_newline_only_cr] 2 | instring = ["print 1\r", "print 2\r", "print3\r"] 3 | expected = "\r" 4 | 5 | [find_newline_only_lf] 6 | instring = ["print 1\n", "print 2\n", "print3\n"] 7 | expected = "\n" 8 | 9 | [find_newline_only_crlf] 10 | instring = ["print 1\r\n", "print 2\r\n", "print3\r\n"] 11 | expected = "\r\n" 12 | 13 | [find_newline_cr1_and_lf2] 14 | instring = ["print 1\n", "print 2\r", "print3\n"] 15 | expected = "\n" 16 | 17 | [find_newline_cr1_and_crlf2] 18 | instring = ["print 1\r\n", "print 2\r", "print3\r\n"] 19 | expected = "\r\n" 20 | 21 | [find_newline_should_default_to_lf_empty] 22 | instring = [] 23 | expected = "\n" 24 | 25 | [find_newline_should_default_to_lf_blank] 26 | instring = ["", ""] 27 | expected = "\n" 28 | 29 | [find_dominant_newline] 30 | instring = ['def foo():\r\n', ' """\r\n', ' Hello\r\n', ' foo. This is a docstring.\r\n', ' """\r\n'] 31 | expected = "\n" 32 | -------------------------------------------------------------------------------- /tests/_data/string_files/field_patterns.toml: -------------------------------------------------------------------------------- 1 | [is_epytext_field_list] 2 | instring = """@param param1: Description of param1\n 3 | @return: Description of return value\n""" 4 | style = "epytext" 5 | expected = true 6 | 7 | [is_sphinx_field_list_epytext_style] 8 | instring = """:param param1: Description of param1\n 9 | :return: Description of return value\n""" 10 | style = "epytext" 11 | expected = false 12 | 13 | [is_sphinx_field_list] 14 | instring = """:param param1: Description of param1\n 15 | :return: Description of return value\n""" 16 | style = "sphinx" 17 | expected = true 18 | 19 | [is_epytext_field_list_sphinx_style] 20 | instring = """@param param1: Description of param1\n 21 | @return: Description of return value\n""" 22 | style = "sphinx" 23 | expected = false 24 | 25 | [is_numpy_field_list] 26 | instring = """Parameters\n 27 | ----------\n 28 | param1 : type\n 29 | Description of param1\n 30 | Returns\n 31 | -------\n 32 | type\n 33 | Description of return value\n""" 34 | style = "numpy" 35 | expected = false 36 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: '$RESOLVED_VERSION' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | categories: 4 | - title: 'Bug Fixes' 5 | labels: 6 | - 'P: bug' 7 | - 'V: patch' 8 | - title: '🚀Features' 9 | labels: 10 | - 'P: enhancement' 11 | - 'V: minor' 12 | - title: 'Maintenance' 13 | labels: 14 | - 'chore' 15 | change-template: '- [#$NUMBER] $TITLE (@$AUTHOR)' 16 | change-title-escapes: '\<*_&' 17 | version-resolver: 18 | major: 19 | labels: 20 | - 'V: major' 21 | minor: 22 | labels: 23 | - 'V: minor' 24 | patch: 25 | labels: 26 | - 'V: patch' 27 | default: patch 28 | exclude-labels: 29 | - 'dependencies' 30 | - 'fresh' 31 | - 'help wanted' 32 | - 'question' 33 | - 'release' 34 | - 'C: convention' 35 | - 'C: stakeholder' 36 | - 'C: style' 37 | - 'S: duplicate' 38 | - 'S: feedback' 39 | - 'S: invalid' 40 | - 'S: merged' 41 | - 'S: wontfix' 42 | include-pre-releases: false 43 | template: | 44 | ## What Changed 45 | 46 | $CHANGES 47 | -------------------------------------------------------------------------------- /docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | How to Install docformatter 2 | =========================== 3 | 4 | Install from PyPI 5 | ----------------- 6 | The latest released version of ``docformatter`` is available from PyPI. To 7 | install it using pip: 8 | 9 | .. code-block:: console 10 | 11 | $ pip install --upgrade docformatter 12 | 13 | Extras 14 | `````` 15 | If you want to use pyproject.toml to configure ``docformatter``, you'll need 16 | to install with TOML support: 17 | 18 | .. code-block:: console 19 | 20 | $ pip install --upgrade docformatter[tomli] 21 | 22 | This is only necessary if you are using Python < 3.11. Beginning with Python 3.11, 23 | docformatter will utilize ``tomllib`` from the standard library. 24 | 25 | Install from GitHub 26 | ------------------- 27 | 28 | If you'd like to use an unreleased version, you can also use pip to install 29 | ``docformatter`` from GitHub. 30 | 31 | .. code-block:: console 32 | 33 | $ python -m pip install git+https://github.com/PyCQA/docformatter.git@v1.5.0-rc1 34 | 35 | Replace the tag ``v1.5.0-rc1`` with a commit SHA to install an untagged 36 | version. 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012-2018 Steven Myint 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # type: ignore 2 | # Configuration file for the Sphinx documentation builder. 3 | # 4 | # For the full list of built-in configuration values, see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | """Configuration file for the Sphinx documentation builder.""" 9 | 10 | 11 | project = "docformatter" 12 | copyright = "2022-2023, Steven Myint" 13 | author = "Steven Myint" 14 | release = "1.7.7" 15 | 16 | # -- General configuration --------------------------------------------------- 17 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 18 | 19 | extensions = [] 20 | 21 | templates_path = ["_templates"] 22 | exclude_patterns = [] 23 | 24 | 25 | # -- Options for HTML output ------------------------------------------------- 26 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 27 | 28 | html_theme = "alabaster" 29 | html_static_path = ["_static"] 30 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Execute Test Suite 2 | on: 3 | push: 4 | branches: 5 | - "*" 6 | pull_request: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | test: 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | python-version: 16 | - "pypy3.9" 17 | - "3.13" 18 | - "3.12" 19 | - "3.11" 20 | - "3.10" 21 | - "3.9" 22 | os: [ubuntu-latest] 23 | runs-on: ${{ matrix.os }} 24 | name: "${{ matrix.os }} Python: ${{ matrix.python-version }}" 25 | steps: 26 | - name: Setup Python for tox 27 | uses: actions/setup-python@v4 28 | with: 29 | python-version: "3.12" 30 | - name: Install tox 31 | run: python -m pip install tox tox-gh-actions 32 | - uses: actions/checkout@v3 33 | with: 34 | fetch-depth: 0 35 | - name: Set up Python ${{ matrix.python-version }} for test 36 | uses: actions/setup-python@v4 37 | with: 38 | python-version: ${{ matrix.python-version }} 39 | - name: Setup test suite 40 | run: tox -vv --notest 41 | - name: Run tests with tox 42 | run: tox -e py --skip-pkg-install 43 | -------------------------------------------------------------------------------- /.github/workflows/on-push-tag.yml: -------------------------------------------------------------------------------- 1 | # This workflow runs when a version tag is pushed. 2 | # 3 | # - Get new tag. 4 | # - If release condidate tag: 5 | # - Cut GitHub pre-release. 6 | name: Prerelease Tag Workflow 7 | 8 | on: 9 | push: 10 | tags: 11 | - 'v*' 12 | 13 | jobs: 14 | cut_prerelease: 15 | permissions: 16 | contents: write 17 | pull-requests: write 18 | name: Cut Pre-Release 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v3 23 | with: 24 | fetch-depth: 0 25 | ref: master 26 | 27 | - name: Get new tag 28 | id: newversion 29 | run: | 30 | tag=${GITHUB_REF/refs\/tags\//} 31 | if [[ $tag == *"-rc"* ]]; then 32 | echo "do_prerelease=1" >> $GITHUB_ENV 33 | fi 34 | echo "tag=$(echo $tag)" >> $GITHUB_ENV 35 | echo "New tag is: $tag" 36 | echo "GitHub ref: ${{ github.ref }}" 37 | 38 | - name: Cut pre-release 39 | id: cutprerelease 40 | if: ${{ env.build_ok == 1 }} 41 | uses: release-drafter/release-drafter@v5 42 | with: 43 | name: ${{ env.tag }} 44 | tag: ${{ env.tag }} 45 | version: ${{ env.tag }} 46 | prerelease: true 47 | publish: true 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | -------------------------------------------------------------------------------- /src/docformatter/__pkginfo__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # docformatter.patterns.__pkginfo__.py is part of the docformatter project 4 | # 5 | # Copyright (C) 2012-2023 Steven Myint 6 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowlans 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining 9 | # a copy of this software and associated documentation files (the 10 | # "Software"), to deal in the Software without restriction, including 11 | # without limitation the rights to use, copy, modify, merge, publish, 12 | # distribute, sublicense, and/or sell copies of the Software, and to 13 | # permit persons to whom the Software is furnished to do so, subject to 14 | # the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | # SOFTWARE. 27 | """Package information for docformatter.""" 28 | 29 | 30 | __version__ = "1.7.7" 31 | -------------------------------------------------------------------------------- /src/docformatter/wrappers/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # docformatter.wrappers.__init__.py is part of the docformatter project 4 | # 5 | # Copyright (C) 2012-2023 Steven Myint 6 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining 9 | # a copy of this software and associated documentation files (the 10 | # "Software"), to deal in the Software without restriction, including 11 | # without limitation the rights to use, copy, modify, merge, publish, 12 | # distribute, sublicense, and/or sell copies of the Software, and to 13 | # permit persons to whom the Software is furnished to do so, subject to 14 | # the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | # SOFTWARE. 27 | """This is the docformatter wrappers package.""" 28 | 29 | 30 | # docformatter Local Imports 31 | from .description import * # noqa F403 32 | from .fields import * # noqa F403 33 | from .summary import * # noqa F403 34 | from .url import * # noqa F403 35 | -------------------------------------------------------------------------------- /src/docformatter/patterns/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # docformatter.patterns.__init__.py is part of the docformatter project 4 | # 5 | # Copyright (C) 2012-2023 Steven Myint 6 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining 9 | # a copy of this software and associated documentation files (the 10 | # "Software"), to deal in the Software without restriction, including 11 | # without limitation the rights to use, copy, modify, merge, publish, 12 | # distribute, sublicense, and/or sell copies of the Software, and to 13 | # permit persons to whom the Software is furnished to do so, subject to 14 | # the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | # SOFTWARE. 27 | """This is the docformatter patterns package.""" 28 | 29 | 30 | # docformatter Local Imports 31 | from .fields import * # noqa F403 32 | from .headers import * # noqa F403 33 | from .lists import * # noqa F403 34 | from .misc import * # noqa F403 35 | from .rest import * # noqa F403 36 | from .url import * # noqa F403 37 | -------------------------------------------------------------------------------- /.sourcery.yaml: -------------------------------------------------------------------------------- 1 | # 🪄 This is your project's Sourcery configuration file. 2 | 3 | # You can use it to get Sourcery working in the way you want, such as 4 | # ignoring specific refactorings, skipping directories in your project, 5 | # or writing custom rules. 6 | 7 | # 📚 For a complete reference to this file, see the documentation at 8 | # https://docs.sourcery.ai/Configuration/Project-Settings/ 9 | 10 | # This file was auto-generated by Sourcery on 2022-12-28 at 22:39. 11 | 12 | version: '1' # The schema version of this config file 13 | 14 | ignore: # A list of paths or files which Sourcery will ignore. 15 | - .git 16 | - venv 17 | - .venv 18 | - env 19 | - .env 20 | - .tox 21 | 22 | rule_settings: 23 | enable: 24 | - default 25 | disable: [] # A list of rule IDs Sourcery will never suggest. 26 | rule_types: 27 | - refactoring 28 | - suggestion 29 | - comment 30 | python_version: '3.7' 31 | 32 | # rules: # A list of custom rules Sourcery will include in its analysis. 33 | # - id: no-print-statements 34 | # description: Do not use print statements in the test directory. 35 | # pattern: print(...) 36 | # replacement: 37 | # condition: 38 | # explanation: 39 | # paths: 40 | # include: 41 | # - test 42 | # exclude: 43 | # - conftest.py 44 | # tests: [] 45 | # tags: [] 46 | 47 | # rule_tags: {} # Additional rule tags. 48 | 49 | # metrics: 50 | # quality_threshold: 25.0 51 | 52 | # github: 53 | # labels: [] 54 | # ignore_labels: 55 | # - sourcery-ignore 56 | # request_review: author 57 | # sourcery_branch: sourcery/{base_branch} 58 | 59 | # clone_detection: 60 | # min_lines: 3 61 | # min_duplicates: 2 62 | # identical_clones_only: false 63 | 64 | # proxy: 65 | # url: 66 | # ssl_certs_file: 67 | # no_ssl_verify: false 68 | -------------------------------------------------------------------------------- /src/docformatter/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # docformatter.__init__.py is part of the docformatter project 4 | # 5 | # Copyright (C) 2012-2023 Steven Myint 6 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining 9 | # a copy of this software and associated documentation files (the 10 | # "Software"), to deal in the Software without restriction, including 11 | # without limitation the rights to use, copy, modify, merge, publish, 12 | # distribute, sublicense, and/or sell copies of the Software, and to 13 | # permit persons to whom the Software is furnished to do so, subject to 14 | # the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | # SOFTWARE. 27 | """This is the docformatter package.""" 28 | 29 | 30 | __all__ = ["__version__"] 31 | 32 | # docformatter Local Imports 33 | from .__pkginfo__ import __version__ 34 | from .classify import * # noqa F403 35 | from .format import FormatResult # noqa F403 36 | from .format import Formatter # noqa F401 37 | from .patterns import * # noqa F403 38 | from .strings import * # noqa F403 39 | from .util import * # noqa F403 40 | from .wrappers import * # noqa F403 41 | 42 | # Have isort skip these they require the functions above. 43 | from .configuration import Configurater # isort: skip # noqa F401 44 | from .encode import Encoder # isort: skip # noqa F401 45 | -------------------------------------------------------------------------------- /.github/workflows/do-prioritize-issues.yml: -------------------------------------------------------------------------------- 1 | # This workflow runs when labels are applied to issues. 2 | # 3 | # - Get list of labels. 4 | # - Determine issue priority based on labels: 5 | # - C: convention && P: bug --> U: high 6 | # - C: style && P: bug --> U: medium 7 | # - C: stakeholder && P:bug --> U: medium 8 | # - C: convention && P: enhancement --> U: medium 9 | # - C: style && P: enhancement --> U: low 10 | # - C: stakeholder && P: enhancement --> U: low 11 | # - chore || P: docs --> U: low 12 | name: Prioritize Issues Workflow 13 | 14 | on: 15 | issues: 16 | types: ['labeled', 'unlabeled'] 17 | 18 | jobs: 19 | prioritize_issues: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Get Issue Labels 23 | id: getlabels 24 | uses: weibullguy/get-labels-action@main 25 | 26 | - name: Add High Urgency Labels 27 | if: "${{ (contains(steps.getlabels.outputs.labels, 'C: convention') && contains (steps.getlabels.outputs.labels, 'P: bug')) }}" 28 | uses: andymckay/labeler@master 29 | with: 30 | add-labels: "U: high" 31 | 32 | - name: Add Medium Urgency Labels 33 | if: "${{ (contains(steps.getlabels.outputs.labels, 'C: style') && contains(steps.getlabels.outputs.labels, 'P: bug')) || (contains(steps.getlabels.outputs.labels, 'C: stakeholder') && contains(steps.getlabels.outputs.labels, 'P: bug')) || (contains(steps.getlabels.outputs.labels, 'C: convention') && contains(steps.getlabels.outputs.labels, 'P: enhancement')) }}" 34 | uses: andymckay/labeler@master 35 | with: 36 | add-labels: "U: medium" 37 | 38 | - name: Add Low Urgency Labels 39 | if: "${{ (contains(steps.getlabels.outputs.labels, 'C: style') && contains(steps.getlabels.outputs.labels, 'P: enhancement')) || (contains(steps.getlabels.outputs.labels, 'C: stakeholder') && contains(steps.getlabels.outputs.labels, 'P: enhancement')) || contains(steps.getlabels.outputs.labels, 'doc') || contains(steps.getlabels.outputs.labels, 'chore') }}" 40 | uses: andymckay/labeler@master 41 | with: 42 | add-labels: "U: low" 43 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | # type: ignore 3 | # 4 | # tests._init__.py is part of the docformatter project 5 | # 6 | # Copyright (C) 2012-2023 Steven Myint 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining 9 | # a copy of this software and associated documentation files (the 10 | # "Software"), to deal in the Software without restriction, including 11 | # without limitation the rights to use, copy, modify, merge, publish, 12 | # distribute, sublicense, and/or sell copies of the Software, and to 13 | # permit persons to whom the Software is furnished to do so, subject to 14 | # the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | # SOFTWARE. 27 | 28 | # Standard Library Imports 29 | import random 30 | import string 31 | 32 | 33 | def generate_random_docstring( 34 | max_indentation_length=32, 35 | max_word_length=20, 36 | max_words=50, 37 | ): 38 | """Generate single-line docstring.""" 39 | if random.randint(0, 1): 40 | words = [] 41 | else: 42 | words = [ 43 | generate_random_word(random.randint(0, max_word_length)) 44 | for _ in range(random.randint(0, max_words)) 45 | ] 46 | 47 | indentation = random.randint(0, max_indentation_length) * " " 48 | quote = '"""' if random.randint(0, 1) else "'''" 49 | return quote + indentation + " ".join(words) + quote 50 | 51 | 52 | def generate_random_word(word_length): 53 | return "".join(random.sample(string.ascii_letters, word_length)) 54 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: 'tests/' 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v5.0.0 5 | hooks: 6 | - id: check-merge-conflict 7 | - id: check-toml 8 | - id: check-yaml 9 | - id: debug-statements 10 | - id: end-of-file-fixer 11 | - id: no-commit-to-branch 12 | - id: trailing-whitespace 13 | - repo: https://github.com/pre-commit/pre-commit 14 | rev: v4.2.0 15 | hooks: 16 | - id: validate_manifest 17 | - repo: https://github.com/psf/black 18 | rev: '25.1.0' 19 | hooks: 20 | - id: black 21 | types_or: [python, pyi] 22 | language_version: python3 23 | - repo: https://github.com/PyCQA/isort 24 | rev: 6.0.1 25 | hooks: 26 | - id: isort 27 | args: [--settings-file, ./pyproject.toml] 28 | - repo: https://github.com/PyCQA/docformatter 29 | rev: v1.7.7 30 | hooks: 31 | - id: docformatter 32 | additional_dependencies: [tomli] 33 | args: [--in-place, --config, ./pyproject.toml] 34 | - repo: https://github.com/charliermarsh/ruff-pre-commit 35 | rev: 'v0.12.4' 36 | hooks: 37 | - id: ruff 38 | args: [ --config, ./pyproject.toml ] 39 | - repo: https://github.com/pycqa/pydocstyle 40 | rev: 6.3.0 41 | hooks: 42 | - id: pydocstyle 43 | additional_dependencies: [toml] 44 | args: [--config, ./pyproject.toml] 45 | - repo: https://github.com/pre-commit/mirrors-mypy 46 | rev: v1.17.0 47 | hooks: 48 | - id: mypy 49 | additional_dependencies: [types-python-dateutil] 50 | args: [--config-file, ./pyproject.toml] 51 | - repo: https://github.com/myint/eradicate 52 | rev: '3.0.0' 53 | hooks: 54 | - id: eradicate 55 | args: [] 56 | - repo: https://github.com/rstcheck/rstcheck 57 | rev: 'v6.2.5' 58 | hooks: 59 | - id: rstcheck 60 | additional_dependencies: [tomli] 61 | args: [-r, --config, ./pyproject.toml] 62 | -------------------------------------------------------------------------------- /tests/_data/string_files/rest_patterns.toml: -------------------------------------------------------------------------------- 1 | [is_double_dot_directive] 2 | instring = """ 3 | This is a docstring that contains a reST directive. 4 | 5 | .. directive type:: directive 6 | :modifier: 7 | 8 | The directive type description. 9 | 10 | This is the part of the docstring that follows the reST directive. 11 | """ 12 | expected = [53, 136] 13 | 14 | [is_double_dot_directive_indented] 15 | instring = """ 16 | ``pattern`` is considered as an URL only if it is parseable as such and starts with 17 | ``http://`` or ``https://``. 18 | 19 | .. important:: 20 | 21 | This is a straight `copy of the functools.cache implementation 22 | `_, 23 | which is only `available in the standard library starting with Python v3.9 24 | `. 25 | """ 26 | expected = [114, 499] 27 | 28 | [is_inline_directive] 29 | instring = """ 30 | These are some reST directives that need to be retained even if it means not wrapping the line they are found on. 31 | Constructs and returns a :class:`QuadraticCurveTo `. 32 | Register ``..click:example::`` and ``.. click:run::`` directives, augmented with ANSI coloring. 33 | """ 34 | expected = [145, 183] 35 | 36 | [is_double_backtick_directive] 37 | instring = """ 38 | By default we choose to exclude: 39 | 40 | ``Cc`` 41 | Since ``mailman`` apparently `sometimes trims list members 42 | `_ 43 | from the ``Cc`` header to avoid sending duplicates. Which means that copies of mail 44 | reflected back from the list server will have a different ``Cc`` to the copy saved by 45 | the MUA at send-time. 46 | 47 | ``Bcc`` 48 | Because copies of the mail saved by the MUA at send-time will have ``Bcc``, but 49 | copies reflected back from the list server won't. 50 | 51 | ``Reply-To`` 52 | Since a mail could be ``Cc``'d to two lists with different ``Reply-To`` munging 53 | options set. 54 | """ 55 | expected = [38, 44] 56 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | .. This file is automatically generated/updated by a github actions workflow. 2 | .. Every manual change will be overwritten on push to master. 3 | .. You can find it here: ``.github/workflows/do-update-authors.yml`` 4 | 5 | Author 6 | ------ 7 | Steven Myint 8 | 9 | Additional contributions by (sorted by name) 10 | -------------------------------------------- 11 | - Alec Merdler 12 | - Alexander Biggs 13 | - Alexander Kapshuna 14 | - Alexander Puck Neuwirth 15 | - Alexandre Detiste 16 | - Andrew Howe 17 | - Andy Hayden 18 | - Anthony Sottile 19 | - Antoine Dechaume 20 | - Asher Foa 21 | - Benjamin Schubert 22 | - Björn Holtvogt 23 | - Casey Korver <84342833+korverdev@users.noreply.github.com> 24 | - Daniel Goldman 25 | - Doyle Rowland 26 | - Elliot Ford 27 | - Eric Hutton 28 | - Filip Kucharczyk 29 | - Jonas Haag 30 | - Josef Kemetmüller 31 | - Kapshuna Alexander 32 | - Kian-Meng Ang 33 | - KotlinIsland <65446343+KotlinIsland@users.noreply.github.com> 34 | - Lisha Li <65045844+lli-fincad@users.noreply.github.com> 35 | - Manuel Kaufmann 36 | - Oliver Sieweke 37 | - Paul Angerer <48882462+dabauxi@users.noreply.github.com> 38 | - Paul Angerer <48882462+etimoz@users.noreply.github.com> 39 | - Peter Boothe 40 | - Peter Cock 41 | - Sebastian Weigand 42 | - Sho Iwamoto 43 | - Swen Kooij 44 | - Thomas Denewiler 45 | - finswimmer 46 | - happlebao 47 | - icp 48 | - serhiy-yevtushenko 49 | -------------------------------------------------------------------------------- /tests/_data/string_files/summary_wrappers.toml: -------------------------------------------------------------------------------- 1 | [do_unwrap_summary] 2 | instring = """This is a summary that has been wrapped\n 3 | and needs to be unwrapped.""" 4 | expected = "This is a summary that has been wrapped and needs to be unwrapped." 5 | 6 | [do_unwrap_summary_empty] 7 | instring = "" 8 | expected = "" 9 | 10 | [do_unwrap_summary_only_newlines] 11 | instring = "\n\n\n" 12 | expected = " " 13 | 14 | [do_unwrap_summary_double_newlines] 15 | instring = "This is a summary that has been wrapped\n\nand needs to be unwrapped." 16 | expected = "This is a summary that has been wrapped and needs to be unwrapped." 17 | 18 | [do_unwrap_summary_leading_trailing] 19 | instring = "\nThis is a summary that has been wrapped\n and needs to be unwrapped.\n" 20 | expected = " This is a summary that has been wrapped and needs to be unwrapped. " 21 | 22 | [do_wrap_summary_no_wrap] 23 | instring = "This is a summary that should not be wrapped." 24 | expected = "This is a summary that should not be wrapped." 25 | 26 | [do_wrap_summary_disabled] 27 | instring = "This is a summary that should be wrapped because it is way too long to fit on a single line." 28 | expected = "This is a summary that should be wrapped because it is way too long to fit on a single line." 29 | 30 | [do_wrap_summary_with_wrap] 31 | instring = "This is a summary that should be wrapped because it is way too long to fit on a single line." 32 | expected = "This is a summary that should be wrapped\n because it is way too long to fit on a single\n line." 33 | 34 | [do_wrap_summary_with_indentation] 35 | instring = "This is a summary that should be wrapped because it is way too long to fit on a single line." 36 | expected = "This is a summary that should be wrapped\n because it is way too long to fit on a\n single line." 37 | 38 | [do_wrap_summary_long_word] 39 | instring = "supercalifragilisticexpialidocious" 40 | expected = "supercalifragilisticexpialidocious" 41 | 42 | [do_wrap_summary_empty] 43 | instring = "" 44 | expected = "" 45 | 46 | [do_wrap_summary_exact_length] 47 | instring = "This summary is exactly fifty characters long." 48 | expected = "This summary is exactly fifty characters long." 49 | 50 | [do_wrap_summary_tabs_spaces] 51 | instring = "word1 \t word2\nword3" 52 | expected = "word1 word2 word3" 53 | 54 | [do_wrap_summary_wrap_length_1] 55 | instring = "abc def" 56 | expected = ">a\n-b\n-c\n-d\n-e\n-f" 57 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | env_list = 3 | py39 4 | py310 5 | py311 6 | py312 7 | py313 8 | pypy3 9 | coverage 10 | pre-commit 11 | isolated_build = true 12 | skip_missing_interpreters = true 13 | skipsdist = true 14 | 15 | [gh-actions] 16 | python = 17 | 3.9: py39 18 | 3.10: py310 19 | 3.11: py311 20 | 3.12: py312 21 | 3.13: py313 22 | pypy-3.9: pypy3 23 | 24 | [testenv] 25 | description = Run the test suite using pytest under {basepython} 26 | deps = 27 | charset_normalizer 28 | coverage[toml] 29 | mock 30 | pytest 31 | pytest-cov 32 | pytest-order 33 | tomli 34 | setenv = 35 | COVERAGE_FILE = {toxworkdir}/.coverage.{envname} 36 | commands = 37 | pip install -U pip 38 | pip install --prefix={toxworkdir}/{envname} -e .[tomli] 39 | pytest -s -x -c {toxinidir}/pyproject.toml \ 40 | -m unit \ 41 | --cache-clear \ 42 | --cov=docformatter \ 43 | --cov-config={toxinidir}/pyproject.toml \ 44 | --cov-branch \ 45 | {toxinidir}/tests/ 46 | pytest -s -x -c {toxinidir}/pyproject.toml \ 47 | -m integration \ 48 | --cache-clear \ 49 | --cov=docformatter \ 50 | --cov-config={toxinidir}/pyproject.toml \ 51 | --cov-branch \ 52 | {toxinidir}/tests/ 53 | pytest -s -x -c {toxinidir}/pyproject.toml \ 54 | -m system \ 55 | --cache-clear \ 56 | --cov=docformatter \ 57 | --cov-config={toxinidir}/pyproject.toml \ 58 | --cov-branch \ 59 | --cov-append \ 60 | {toxinidir}/tests/ 61 | 62 | [testenv:coverage] 63 | description = combine coverage data and create report 64 | setenv = 65 | COVERAGE_FILE = {toxworkdir}/.coverage 66 | skip_install = true 67 | deps = 68 | coverage[toml] 69 | parallel_show_output = true 70 | commands = 71 | coverage combine 72 | coverage report -m 73 | coverage xml -o {toxworkdir}/coverage.xml 74 | depends = py39, py310, py311, py312, py313, pypy3 75 | 76 | [testenv:pre-commit] 77 | description = Run autoformatters and quality assurance tools under {basepython}. 78 | deps = 79 | pre-commit 80 | commands = 81 | {envpython} -m pre_commit run \ 82 | --color=always \ 83 | --show-diff-on-failure \ 84 | {posargs:--all-files} 85 | 86 | [testenv:docs] 87 | description = build docformatter documentation 88 | allowlist_externals = make 89 | changedir = docs 90 | commands = 91 | make html 92 | -------------------------------------------------------------------------------- /tests/patterns/test_field_patterns.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | # type: ignore 3 | # 4 | # tests.patterns.test_field_patterns.py is part of the docformatter project 5 | # 6 | # Copyright (C) 2012-2023 Steven Myint 7 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining 10 | # a copy of this software and associated documentation files (the 11 | # "Software"), to deal in the Software without restriction, including 12 | # without limitation the rights to use, copy, modify, merge, publish, 13 | # distribute, sublicense, and/or sell copies of the Software, and to 14 | # permit persons to whom the Software is furnished to do so, subject to 15 | # the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be 18 | # included in all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 24 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 25 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 26 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | # SOFTWARE. 28 | """Module for testing the field list pattern detection functions.""" 29 | 30 | # Standard Library Imports 31 | import contextlib 32 | import sys 33 | 34 | with contextlib.suppress(ImportError): 35 | if sys.version_info >= (3, 11): 36 | # Standard Library Imports 37 | import tomllib 38 | else: 39 | # Third Party Imports 40 | import tomli as tomllib 41 | 42 | # Third Party Imports 43 | import pytest 44 | 45 | # docformatter Package Imports 46 | from docformatter.patterns import is_field_list 47 | 48 | with open("tests/_data/string_files/field_patterns.toml", "rb") as f: 49 | TEST_STRINGS = tomllib.load(f) 50 | 51 | 52 | @pytest.mark.integration 53 | @pytest.mark.order(3) 54 | @pytest.mark.parametrize( 55 | "test_key", 56 | [ 57 | "is_epytext_field_list", 58 | "is_epytext_field_list_sphinx_style", 59 | "is_sphinx_field_list", 60 | "is_sphinx_field_list_epytext_style", 61 | "is_numpy_field_list", 62 | ], 63 | ) 64 | def test_is_field_list(test_key): 65 | """Test the is_field_list function.""" 66 | text = TEST_STRINGS[test_key]["instring"] 67 | style = TEST_STRINGS[test_key]["style"] 68 | expected = TEST_STRINGS[test_key]["expected"] 69 | 70 | result = is_field_list(text, style) 71 | assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" 72 | -------------------------------------------------------------------------------- /src/docformatter/patterns/rest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # docformatter.patterns.rest.py is part of the docformatter project 4 | # 5 | # Copyright (C) 2012-2023 Steven Myint 6 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining 9 | # a copy of this software and associated documentation files (the 10 | # "Software"), to deal in the Software without restriction, including 11 | # without limitation the rights to use, copy, modify, merge, publish, 12 | # distribute, sublicense, and/or sell copies of the Software, and to 13 | # permit persons to whom the Software is furnished to do so, subject to 14 | # the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | # SOFTWARE. 27 | """This module provides docformatter's reST directive pattern recognition functions.""" 28 | 29 | 30 | # Standard Library Imports 31 | import re 32 | 33 | # docformatter Package Imports 34 | from docformatter.constants import REST_DIRECTIVE_REGEX, REST_INLINE_REGEX 35 | 36 | 37 | def do_find_rest_directives( 38 | text: str, 39 | ) -> list[tuple[int, int]]: 40 | """Determine if docstring contains any reST directives. 41 | 42 | Parameters 43 | ---------- 44 | text : str 45 | The docstring text to test. 46 | indent : int 47 | The number of spaces the reST directive line is indented. 48 | 49 | Returns 50 | ------- 51 | bool 52 | True if the docstring is a reST directive, False otherwise. 53 | """ 54 | _rest_iter = re.finditer(REST_DIRECTIVE_REGEX, text, flags=re.MULTILINE) 55 | return [(_rest.start(0), _rest.end(0)) for _rest in _rest_iter] 56 | 57 | 58 | def do_find_inline_rest_markup(text: str) -> list[tuple[int, int]]: 59 | """Determine if docstring contains any inline reST markup. 60 | 61 | Parameters 62 | ---------- 63 | text : str 64 | The docstring text to test. 65 | 66 | Returns 67 | ------- 68 | bool 69 | True if the docstring is a reST directive, False otherwise. 70 | """ 71 | _rest_iter = re.finditer(REST_INLINE_REGEX, text, flags=re.MULTILINE) 72 | return [(_rest.start(0), _rest.end(0)) for _rest in _rest_iter] 73 | -------------------------------------------------------------------------------- /.github/workflows/do-update-authors.yml: -------------------------------------------------------------------------------- 1 | name: Update AUTHORS.rst 2 | 3 | # What this workflow does: 4 | # 1. Update the AUTHORS.rst file 5 | # 2. Git commit and push the file if there are changes. 6 | 7 | on: # yamllint disable-line rule:truthy 8 | workflow_dispatch: 9 | 10 | push: 11 | tags: 12 | - "!*" 13 | branches: 14 | - master 15 | 16 | jobs: 17 | update-authors: 18 | name: Update AUTHORS.rst file 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 0 24 | 25 | - uses: actions/setup-python@v3 26 | 27 | - name: Update AUTHORS.rst file 28 | shell: python 29 | run: | 30 | import subprocess 31 | 32 | git_authors = subprocess.run( 33 | ["git", "log", "--format=%aN <%aE>"], capture_output=True, check=True 34 | ).stdout.decode() 35 | 36 | skip_list = ( 37 | "Steven Myint", 38 | "dependabot", 39 | "pre-commit-ci", 40 | "github-action", 41 | "GitHub Actions", 42 | "Sourcery AI", 43 | ) 44 | authors = [ 45 | author 46 | for author in set(git_authors.strip().split("\n")) 47 | if not author.startswith(skip_list) 48 | ] 49 | authors.sort() 50 | 51 | file_head = ( 52 | ".. This file is automatically generated/updated by a github actions workflow.\n" 53 | ".. Every manual change will be overwritten on push to master.\n" 54 | ".. You can find it here: ``.github/workflows/do-update-authors.yml``\n\n" 55 | "Author\n" 56 | "------\n" 57 | "Steven Myint \n\n" 58 | "Additional contributions by (sorted by name)\n" 59 | "--------------------------------------------\n" 60 | ) 61 | 62 | with open("AUTHORS.rst", "w") as authors_file: 63 | authors_file.write(file_head) 64 | authors_file.write("- ") 65 | authors_file.write("\n- ".join(authors)) 66 | authors_file.write("\n") 67 | 68 | - name: Check if diff 69 | continue-on-error: true 70 | run: > 71 | git diff --exit-code AUTHORS.rst && 72 | (echo "### No update" && exit 1) || (echo "### Commit update") 73 | 74 | - uses: EndBug/add-and-commit@v9 75 | name: Commit and push if diff 76 | if: success() 77 | with: 78 | add: AUTHORS.rst 79 | message: 'chore: update AUTHORS.rst file with new author(s)' 80 | author_name: GitHub Actions 81 | author_email: action@github.com 82 | committer_name: GitHub Actions 83 | committer_email: actions@github.com 84 | push: true 85 | -------------------------------------------------------------------------------- /src/docformatter/wrappers/summary.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # docformatter.wrappers.summary.py is part of the docformatter project 4 | # 5 | # Copyright (C) 2012-2023 Steven Myint 6 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining 9 | # a copy of this software and associated documentation files (the 10 | # "Software"), to deal in the Software without restriction, including 11 | # without limitation the rights to use, copy, modify, merge, publish, 12 | # distribute, sublicense, and/or sell copies of the Software, and to 13 | # permit persons to whom the Software is furnished to do so, subject to 14 | # the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | # SOFTWARE. 27 | """This module provides docformatter's summary wrapper functions.""" 28 | 29 | 30 | # Standard Library Imports 31 | import re 32 | import textwrap 33 | 34 | 35 | def do_unwrap_summary(summary: str) -> str: 36 | r"""Return summary with newlines removed in preparation for wrapping. 37 | 38 | Parameters 39 | ---------- 40 | summary : str 41 | The summary text from the docstring. 42 | 43 | Returns 44 | ------- 45 | str 46 | The summary text with newline (\n) characters replaced by a single space. 47 | """ 48 | return re.sub(r"\s*\n\s*", " ", summary) 49 | 50 | 51 | def do_wrap_summary( 52 | summary: str, 53 | initial_indent: str, 54 | subsequent_indent: str, 55 | wrap_length: int, 56 | ) -> str: 57 | """Return line-wrapped summary text. 58 | 59 | If the wrap_length is any value less than or equal to zero, the raw, unwrapped 60 | summary text will be returned. 61 | 62 | Parameters 63 | ---------- 64 | summary : str 65 | The summary text from the docstring. 66 | initial_indent : str 67 | The indentation string for the first line of the summary. 68 | subsequent_indent : str 69 | The indentation string for all the other lines of the summary. 70 | wrap_length : int 71 | The column position to wrap the summary lines. 72 | 73 | Returns 74 | ------- 75 | str 76 | The summary text from the docstring wrapped at wrap_length columns. 77 | """ 78 | if wrap_length > 0: 79 | return textwrap.fill( 80 | do_unwrap_summary(summary), 81 | width=wrap_length, 82 | initial_indent=initial_indent, 83 | subsequent_indent=subsequent_indent, 84 | ).strip() 85 | else: 86 | return summary 87 | -------------------------------------------------------------------------------- /src/docformatter/patterns/url.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # docformatter.patterns.url.py is part of the docformatter project 4 | # 5 | # Copyright (C) 2012-2023 Steven Myint 6 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining 9 | # a copy of this software and associated documentation files (the 10 | # "Software"), to deal in the Software without restriction, including 11 | # without limitation the rights to use, copy, modify, merge, publish, 12 | # distribute, sublicense, and/or sell copies of the Software, and to 13 | # permit persons to whom the Software is furnished to do so, subject to 14 | # the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | # SOFTWARE. 27 | """This module provides docformatter's URL pattern recognition functions.""" 28 | 29 | 30 | # Standard Library Imports 31 | import contextlib 32 | import re 33 | from typing import List, Tuple 34 | 35 | # docformatter Package Imports 36 | from docformatter.constants import URL_REGEX, URL_SKIP_REGEX 37 | 38 | 39 | def do_find_links(text: str) -> List[Tuple[int, int]]: 40 | r"""Determine if docstring contains any links. 41 | 42 | Parameters 43 | ---------- 44 | text : str 45 | The docstring description to check for link patterns. 46 | 47 | Returns 48 | ------- 49 | list 50 | A list of tuples with each tuple containing the starting and ending 51 | position of each URL found in the description. 52 | """ 53 | _url_iter = re.finditer(URL_REGEX, text) 54 | return [(_url.start(0), _url.end(0)) for _url in _url_iter] 55 | 56 | 57 | def do_skip_link(text: str, index: Tuple[int, int]) -> bool: 58 | """Check if the identified URL is other than a complete link. 59 | 60 | Parameters 61 | ---------- 62 | text : str 63 | The description text containing the link. 64 | index : tuple 65 | The index in the text of the starting and ending position of the 66 | identified link. 67 | 68 | Returns 69 | ------- 70 | _do_skip : bool 71 | Whether to skip this link and simpley treat it as a standard text word. 72 | 73 | Notes 74 | ----- 75 | Is the identified link simply: 76 | 1. The URL scheme pattern such as 's3://' or 'file://' or 'dns:'. 77 | 2. The beginning of a URL link that has been wrapped by the user. 78 | """ 79 | _do_skip = re.search(URL_SKIP_REGEX, text[index[0] : index[1]]) is not None 80 | 81 | with contextlib.suppress(IndexError): 82 | _do_skip = _do_skip or (text[index[0]] == "<" and text[index[1]] != ">") 83 | 84 | return _do_skip 85 | -------------------------------------------------------------------------------- /tests/patterns/test_list_patterns.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | # type: ignore 3 | # 4 | # tests.patterns.test_list_patterns.py is part of the docformatter project 5 | # 6 | # Copyright (C) 2012-2023 Steven Myint 7 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining 10 | # a copy of this software and associated documentation files (the 11 | # "Software"), to deal in the Software without restriction, including 12 | # without limitation the rights to use, copy, modify, merge, publish, 13 | # distribute, sublicense, and/or sell copies of the Software, and to 14 | # permit persons to whom the Software is furnished to do so, subject to 15 | # the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be 18 | # included in all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 24 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 25 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 26 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | # SOFTWARE. 28 | """Module for testing the list pattern detection functions.""" 29 | 30 | # Standard Library Imports 31 | import contextlib 32 | import sys 33 | 34 | with contextlib.suppress(ImportError): 35 | if sys.version_info >= (3, 11): 36 | # Standard Library Imports 37 | import tomllib 38 | else: 39 | # Third Party Imports 40 | import tomli as tomllib 41 | 42 | # Third Party Imports 43 | import pytest 44 | 45 | # docformatter Package Imports 46 | from docformatter.patterns import is_type_of_list 47 | 48 | with open("tests/_data/string_files/list_patterns.toml", "rb") as f: 49 | TEST_STRINGS = tomllib.load(f) 50 | 51 | 52 | @pytest.mark.integration 53 | @pytest.mark.order(3) 54 | @pytest.mark.parametrize( 55 | "test_key", 56 | [ 57 | "is_bullet_list", 58 | "is_enum_list", 59 | "is_option_list", 60 | "is_option_list_indented", 61 | "is_list_with_single_hyphen", 62 | "is_list_with_double_hyphen", 63 | "is_list_with_at_sign", 64 | "is_heuristic_list", 65 | "is_not_list_sphinx_style", 66 | "is_sphinx_list_numpy_style", 67 | "is_numpy_list_sphinx_style", 68 | "is_google_list_numpy_style", 69 | "is_type_of_list_strict_wrap", 70 | "is_type_of_list_non_strict_wrap", 71 | "is_literal_block", 72 | "is_reST_header", 73 | "is_type_of_list_alembic_header", 74 | "is_epytext_field_list", 75 | "is_sphinx_field_list", 76 | ], 77 | ) 78 | def test_is_type_of_list(test_key): 79 | text = TEST_STRINGS[test_key]["instring"] 80 | strict = TEST_STRINGS[test_key]["strict"] 81 | style = TEST_STRINGS[test_key]["style"] 82 | expected = TEST_STRINGS[test_key]["expected"] 83 | 84 | result = is_type_of_list(text, strict, style) 85 | assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" 86 | -------------------------------------------------------------------------------- /tests/wrappers/test_url_wrapper.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | # type: ignore 3 | # 4 | # tests.wrappers.test_url_wrapper.py is part of the docformatter project 5 | # 6 | # Copyright (C) 2012-2023 Steven Myint 7 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining 10 | # a copy of this software and associated documentation files (the 11 | # "Software"), to deal in the Software without restriction, including 12 | # without limitation the rights to use, copy, modify, merge, publish, 13 | # distribute, sublicense, and/or sell copies of the Software, and to 14 | # permit persons to whom the Software is furnished to do so, subject to 15 | # the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be 18 | # included in all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 24 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 25 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 26 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | # SOFTWARE. 28 | """Module for testing functions that wrap URL text.""" 29 | 30 | # Standard Library Imports 31 | import contextlib 32 | import sys 33 | 34 | with contextlib.suppress(ImportError): 35 | if sys.version_info >= (3, 11): 36 | # Standard Library Imports 37 | import tomllib 38 | else: 39 | # Third Party Imports 40 | import tomli as tomllib 41 | 42 | # Third Party Imports 43 | import pytest 44 | 45 | # docformatter Package Imports 46 | from docformatter.wrappers import do_wrap_urls 47 | 48 | with open("tests/_data/string_files/url_wrappers.toml", "rb") as f: 49 | TEST_STRINGS = tomllib.load(f) 50 | 51 | 52 | @pytest.mark.unit 53 | @pytest.mark.parametrize( 54 | "test_key, url_idx, text_idx", 55 | [ 56 | ("elaborate_inline_url", [(134, 226)], 0), 57 | ("short_inline_url", [(8, 42)], 0), 58 | ("long_inline_url", [(44, 168)], 0), 59 | ("simple_url", [(4, 100)], 0), 60 | ("short_url", [(8, 32)], 0), 61 | ("inline_url_retain_space", [(47, 171)], 0), 62 | ("keep_inline_url_together", [(20, 133)], 0), 63 | ("inline_url_two_paragraphs", [(26, 153)], 0), 64 | ("url_no_delete_words", [(36, 92)], 0), 65 | ("no_newline_after_url", [(113, 167), (229, 280)], 0), 66 | ("only_url_in_description", [(4, 99)], 0), 67 | ("no_indent_string_on_newline", [(43, 91)], 0), 68 | ("short_anonymous_url", [(137, 190)], 0), 69 | ("quoted_url", [(59, 80)], 0), 70 | ], 71 | ) 72 | def test_do_wrap_urls(test_key, url_idx, text_idx): 73 | source = TEST_STRINGS[test_key]["instring"] 74 | expected = TEST_STRINGS[test_key]["expected"] 75 | 76 | # We convert the returned tuple to a list because we can't store a tuple in a 77 | # TOML file. 78 | result = list(do_wrap_urls(source, url_idx, text_idx, " ", 72)) 79 | assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" 80 | -------------------------------------------------------------------------------- /tests/wrappers/test_description_wrapper.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | # type: ignore 3 | # 4 | # tests.wrappers.test_description_wrapper.py is part of the docformatter project 5 | # 6 | # Copyright (C) 2012-2023 Steven Myint 7 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining 10 | # a copy of this software and associated documentation files (the 11 | # "Software"), to deal in the Software without restriction, including 12 | # without limitation the rights to use, copy, modify, merge, publish, 13 | # distribute, sublicense, and/or sell copies of the Software, and to 14 | # permit persons to whom the Software is furnished to do so, subject to 15 | # the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be 18 | # included in all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 24 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 25 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 26 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | # SOFTWARE. 28 | """Module for testing the description wrapper functions.""" 29 | 30 | # Standard Library Imports 31 | import contextlib 32 | import sys 33 | 34 | with contextlib.suppress(ImportError): 35 | if sys.version_info >= (3, 11): 36 | # Standard Library Imports 37 | import tomllib 38 | else: 39 | # Third Party Imports 40 | import tomli as tomllib 41 | 42 | # Third Party Imports 43 | import pytest 44 | 45 | # docformatter Package Imports 46 | from docformatter.wrappers import do_close_description, do_wrap_description 47 | 48 | with open("tests/_data/string_files/description_wrappers.toml", "rb") as f: 49 | TEST_STRINGS = tomllib.load(f) 50 | 51 | 52 | @pytest.mark.unit 53 | @pytest.mark.parametrize( 54 | "test_key, text_index", 55 | [ 56 | ("do_close_description", 24), 57 | ], 58 | ) 59 | def test_do_close_description(test_key, text_index): 60 | source = TEST_STRINGS[test_key]["instring"] 61 | expected = TEST_STRINGS[test_key]["expected"] 62 | 63 | result = do_close_description(source, text_index, " ") 64 | assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" 65 | 66 | 67 | @pytest.mark.integration 68 | @pytest.mark.order(2) 69 | @pytest.mark.parametrize( 70 | "test_key, force_wrap", 71 | [ 72 | ("do_wrap_description", False), 73 | ("do_wrap_description_with_doctest", False), 74 | ("do_wrap_description_with_list", False), 75 | ("do_wrap_description_with_heuristic_list", False), 76 | ("do_wrap_description_with_heuristic_list_force_wrap", True), 77 | ("do_wrap_description_with_directive", False), 78 | ], 79 | ) 80 | def test_do_wrap_description(test_key, force_wrap): 81 | source = TEST_STRINGS[test_key]["instring"] 82 | expected = TEST_STRINGS[test_key]["expected"] 83 | 84 | result = do_wrap_description(source, " ", 72, force_wrap, False, "", "sphinx") 85 | assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" 86 | -------------------------------------------------------------------------------- /tests/_data/string_files/utility_functions.toml: -------------------------------------------------------------------------------- 1 | [has_correct_length_none] 2 | length_range = "None" 3 | start = 1 4 | end = 9 5 | expected = true 6 | 7 | [has_correct_length_start_in_range] 8 | length_range = [1, 3] 9 | start = 3 10 | end = 5 11 | expected = true 12 | 13 | [has_correct_length_end_in_range] 14 | length_range = [1, 10] 15 | start = 5 16 | end = 10 17 | expected = true 18 | 19 | [has_correct_length_both_in_range] 20 | length_range = [1, 10] 21 | start = 3 22 | end = 7 23 | expected = true 24 | 25 | [has_correct_length_start_out_of_range] 26 | length_range = [5, 16] 27 | start = 3 28 | end = 5 29 | expected = false 30 | 31 | [has_correct_length_end_out_of_range] 32 | length_range = [1, 10] 33 | start = 5 34 | end = 20 35 | expected = false 36 | 37 | [has_correct_length_both_out_of_range] 38 | length_range = [1, 10] 39 | start = 11 40 | end = 27 41 | expected = false 42 | 43 | [is_in_range_none] 44 | line_range = "None" 45 | start = 1 46 | end = 9 47 | expected = true 48 | 49 | [is_in_range_start_in_range] 50 | line_range = [1, 4] 51 | start = 3 52 | end = 5 53 | expected = true 54 | 55 | [is_in_range_end_in_range] 56 | line_range = [1, 4] 57 | start = 4 58 | end = 10 59 | expected = true 60 | 61 | [is_in_range_both_in_range] 62 | line_range = [2, 10] 63 | start = 1 64 | end = 2 65 | expected = true 66 | 67 | [is_in_range_out_of_range] 68 | line_range = [10, 20] 69 | start = 1 70 | end = 9 71 | expected = false 72 | 73 | [find_py_file] 74 | sources = ["test_python_file.py"] 75 | exclude = [] 76 | expected = ["test_python_file.py"] 77 | 78 | [find_py_file_recursive] 79 | sources = [ 80 | "/root/folder_one/one.py", 81 | "/root/folder_one/folder_three/three.py", 82 | "/root/folder_two/two.py", 83 | ] 84 | exclude = [] 85 | expected = [ 86 | "/root/folder_one/folder_three/three.py", 87 | "/root/folder_one/one.py", 88 | "/root/folder_two/two.py", 89 | ] 90 | 91 | [skip_hidden_py_file] 92 | sources = ["not_hidden.py", ".hidden_file.py"] 93 | exclude = [".hidden_file.py"] 94 | expected = ["not_hidden.py"] 95 | 96 | [skip_hidden_py_file_recursive] 97 | sources = ["/root/not_hidden.py", "/root/.hidden_file.py"] 98 | exclude = [".hidden_file.py"] 99 | expected = ["/root/not_hidden.py"] 100 | 101 | [ignore_non_py_file] 102 | sources = ["one.py", "two.py", "three.toml"] 103 | exclude = [] 104 | expected = ["one.py", "two.py"] 105 | 106 | [ignore_non_py_file_recursive] 107 | sources = ["one.py", "two.py", "three.toml", "subdir/four.py", "subdir/five.txt"] 108 | exclude = [] 109 | expected = ["one.py", "subdir/four.py", "two.py"] 110 | 111 | [exclude_py_file] 112 | sources = ["one.py", "two.py", "three.py", "four.py"] 113 | exclude = ["three.py"] 114 | expected = ["four.py", "one.py", "two.py"] 115 | 116 | [exclude_py_file_recursive] 117 | sources = ["/root/one.py", "/root/two.py", "/root/folder_three/three.py", "/root/four.py"] 118 | exclude = ["three.py"] 119 | expected = ["/root/four.py", "/root/one.py", "/root/two.py"] 120 | 121 | [exclude_multiple_files] 122 | sources = ["one.py", "two.py", "three.py", "four.py"] 123 | exclude = ["three.py", "four.py"] 124 | expected = ["one.py", "two.py"] 125 | 126 | [exclude_multiple_files_recursive] 127 | sources = ["/root/one.py", "/root/two.py", "/root/folder_three/three.py", "/root/four.py"] 128 | exclude = ["three.py", "four.py"] 129 | expected = ["/root/one.py", "/root/two.py"] 130 | -------------------------------------------------------------------------------- /tests/patterns/test_rest_patterns.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | # type: ignore 3 | # 4 | # tests.patterns.test_rest_patterns.py is part of the docformatter project 5 | # 6 | # Copyright (C) 2012-2023 Steven Myint 7 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining 10 | # a copy of this software and associated documentation files (the 11 | # "Software"), to deal in the Software without restriction, including 12 | # without limitation the rights to use, copy, modify, merge, publish, 13 | # distribute, sublicense, and/or sell copies of the Software, and to 14 | # permit persons to whom the Software is furnished to do so, subject to 15 | # the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be 18 | # included in all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 24 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 25 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 26 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | # SOFTWARE. 28 | """Module for testing the reST directive pattern detection functions.""" 29 | 30 | # Standard Library Imports 31 | import contextlib 32 | import sys 33 | 34 | with contextlib.suppress(ImportError): 35 | if sys.version_info >= (3, 11): 36 | # Standard Library Imports 37 | import tomllib 38 | else: 39 | # Third Party Imports 40 | import tomli as tomllib 41 | 42 | # Third Party Imports 43 | import pytest 44 | 45 | # docformatter Package Imports 46 | from docformatter.patterns import do_find_rest_directives, do_find_inline_rest_markup 47 | 48 | with open("tests/_data/string_files/rest_patterns.toml", "rb") as f: 49 | TEST_STRINGS = tomllib.load(f) 50 | 51 | 52 | @pytest.mark.unit 53 | @pytest.mark.parametrize( 54 | "test_key", 55 | [ 56 | "is_double_dot_directive", 57 | "is_double_dot_directive_indented", 58 | ], 59 | ) 60 | def test_do_find_rest_directives(test_key): 61 | source = TEST_STRINGS[test_key]["instring"] 62 | expected = TEST_STRINGS[test_key]["expected"] 63 | 64 | result = do_find_rest_directives(source) 65 | assert ( 66 | result[0][0] == expected[0] 67 | ), f"\nFailed {test_key}\nExpected {expected[0]}\nGot {result[0][0]}" 68 | assert ( 69 | result[0][1] == expected[1] 70 | ), f"\nFailed {test_key}\nExpected {expected[0]}\nGot {result[0][1]}" 71 | 72 | 73 | @pytest.mark.unit 74 | @pytest.mark.parametrize( 75 | "test_key", 76 | [ 77 | "is_inline_directive", 78 | "is_double_backtick_directive", 79 | ], 80 | ) 81 | def test_do_find_inline_rest_markup(test_key): 82 | source = TEST_STRINGS[test_key]["instring"] 83 | expected = TEST_STRINGS[test_key]["expected"] 84 | 85 | result = do_find_inline_rest_markup(source) 86 | print(result) 87 | assert ( 88 | result[0][0] == expected[0] 89 | ), f"\nFailed {test_key}\nExpected {expected[0]}\nGot {result[0][0]}" 90 | assert ( 91 | result[0][1] == expected[1] 92 | ), f"\nFailed {test_key}\nExpected {expected[0]}\nGot {result[0][1]}" 93 | -------------------------------------------------------------------------------- /tests/_data/string_files/description_wrappers.toml: -------------------------------------------------------------------------------- 1 | [do_close_description] 2 | instring = """ 3 | http://www.google.com. This is the part of the long description that follows a URL or 4 | something like that. This stuff should get wrapped according to the rules in place 5 | at the time.""" 6 | expected = [' This is the part of the long description that follows a URL or', 7 | ' something like that. This stuff should get wrapped according to the rules in place', 8 | ' at the time.'] 9 | 10 | [do_wrap_description] 11 | instring = """ 12 | \nThis is the long description from a docstring. It follows the summary and will be wrapped at X characters, where X is the wrap_length argument to docformatter. 13 | """ 14 | expected = """ 15 | This is the long description from a docstring. It follows the 16 | summary and will be wrapped at X characters, where X is the 17 | wrap_length argument to docformatter.""" 18 | 19 | [do_wrap_description_with_doctest] 20 | instring = """ 21 | This is a long description from a docstring that contains a simple little doctest. The description shouldn't get wrapped at all. 22 | 23 | >>> print(x) 24 | >>> 42 25 | """ 26 | expected = """ 27 | This is a long description from a docstring that contains a simple little doctest. The description shouldn't get wrapped at all. 28 | 29 | >>> print(x) 30 | >>> 42""" 31 | 32 | [do_wrap_description_with_list] 33 | instring = """ 34 | This is a long description from a docstring that will contain a list. The description shouldn't get wrapped at all. 35 | 36 | 1. Item one 37 | 2. Item two 38 | 3. Item 3 39 | """ 40 | expected = """ 41 | This is a long description from a docstring that will contain a list. The description shouldn't get wrapped at all. 42 | 43 | 1. Item one 44 | 2. Item two 45 | 3. Item 3""" 46 | 47 | [do_wrap_description_with_heuristic_list] 48 | instring = """ 49 | This is a long description from a docstring that will contain an heuristic list. The description shouldn't get wrapped at all. 50 | 51 | Example: 52 | Item one 53 | Item two 54 | Item 3 55 | """ 56 | expected = """ 57 | This is a long description from a docstring that will contain an heuristic list. The description shouldn't get wrapped at all. 58 | 59 | Example: 60 | Item one 61 | Item two 62 | Item 3""" 63 | 64 | [do_wrap_description_with_heuristic_list_force_wrap] 65 | instring = """ 66 | This is a long description from a docstring that will contain an heuristic list and is passed force_wrap = True. The list portion of the description should also get wrapped. 67 | 68 | Example: 69 | Item one 70 | Item two 71 | Item 3 72 | Item 4 73 | Item 5 74 | Item 6 75 | """ 76 | expected = """ 77 | This is a long description from a docstring that will contain an 78 | heuristic list and is passed force_wrap = True. The list portion of 79 | the description should also get wrapped. 80 | 81 | Example: Item one Item two Item 3 Item 4 Item 5 82 | Item 6""" 83 | 84 | [do_wrap_description_with_directive] 85 | instring = """ 86 | This is a long docstring containing some reST directives. 87 | 88 | .. note:: 89 | This is a note in the reST dialog. 90 | """ 91 | expected = """ 92 | This is a long docstring containing some reST directives. 93 | 94 | .. note:: 95 | This is a note in the reST dialog.""" 96 | -------------------------------------------------------------------------------- /tests/patterns/test_misc_patterns.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | # type: ignore 3 | # 4 | # tests.patterns.test_misc_patterns.py is part of the docformatter project 5 | # 6 | # Copyright (C) 2012-2023 Steven Myint 7 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining 10 | # a copy of this software and associated documentation files (the 11 | # "Software"), to deal in the Software without restriction, including 12 | # without limitation the rights to use, copy, modify, merge, publish, 13 | # distribute, sublicense, and/or sell copies of the Software, and to 14 | # permit persons to whom the Software is furnished to do so, subject to 15 | # the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be 18 | # included in all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 24 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 25 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 26 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | # SOFTWARE. 28 | """Module for testing the miscellaneous pattern detection functions.""" 29 | 30 | # Standard Library Imports 31 | import contextlib 32 | import sys 33 | 34 | with contextlib.suppress(ImportError): 35 | if sys.version_info >= (3, 11): 36 | # Standard Library Imports 37 | import tomllib 38 | else: 39 | # Third Party Imports 40 | import tomli as tomllib 41 | 42 | # Third Party Imports 43 | import pytest 44 | 45 | # docformatter Package Imports 46 | from docformatter.patterns import ( 47 | is_probably_beginning_of_sentence, 48 | is_some_sort_of_code, 49 | ) 50 | 51 | with open("tests/_data/string_files/misc_patterns.toml", "rb") as f: 52 | TEST_STRINGS = tomllib.load(f) 53 | 54 | 55 | @pytest.mark.unit 56 | @pytest.mark.parametrize( 57 | "test_key, patternizer", 58 | [ 59 | ("is_some_sort_of_code", is_some_sort_of_code), 60 | pytest.param( 61 | "is_some_sort_of_code_python", 62 | is_some_sort_of_code, 63 | marks=pytest.mark.skip( 64 | reason="The is_some_sort_of_code function is simply looking for long " 65 | "words. This function needs to be re-written to look for actual code " 66 | "patterns." 67 | ), 68 | ), 69 | ("is_probably_beginning_of_sentence", is_probably_beginning_of_sentence), 70 | ("is_not_probably_beginning_of_sentence", is_probably_beginning_of_sentence), 71 | ( 72 | "is_probably_beginning_of_sentence_pydoc_ref", 73 | is_probably_beginning_of_sentence, 74 | ), 75 | ], 76 | ) 77 | def test_miscellaneous_patterns(test_key, patternizer): 78 | source = TEST_STRINGS[test_key]["instring"] 79 | expected = TEST_STRINGS[test_key]["expected"] 80 | 81 | result = patternizer(source) 82 | if result: 83 | assert ( 84 | result == expected 85 | ), f"\nFailed {test_key}\nExpected {expected}\nGot {result}" 86 | else: 87 | result = "None" if result is None else result 88 | assert ( 89 | result == expected 90 | ), f"\nFailed {test_key}\nExpected {expected}\nGot {result}" 91 | -------------------------------------------------------------------------------- /tests/wrappers/test_summary_wrapper.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | # type: ignore 3 | # 4 | # tests.wrappers.test_summary_wrapper.py is part of the docformatter project 5 | # 6 | # Copyright (C) 2012-2023 Steven Myint 7 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining 10 | # a copy of this software and associated documentation files (the 11 | # "Software"), to deal in the Software without restriction, including 12 | # without limitation the rights to use, copy, modify, merge, publish, 13 | # distribute, sublicense, and/or sell copies of the Software, and to 14 | # permit persons to whom the Software is furnished to do so, subject to 15 | # the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be 18 | # included in all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 24 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 25 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 26 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | # SOFTWARE. 28 | """Module for testing functions that wrap summary text.""" 29 | 30 | # Standard Library Imports 31 | import contextlib 32 | import sys 33 | 34 | with contextlib.suppress(ImportError): 35 | if sys.version_info >= (3, 11): 36 | # Standard Library Imports 37 | import tomllib 38 | else: 39 | # Third Party Imports 40 | import tomli as tomllib 41 | 42 | # Third Party Imports 43 | import pytest 44 | 45 | # docformatter Package Imports 46 | from docformatter.wrappers import do_unwrap_summary, do_wrap_summary 47 | 48 | with open("tests/_data/string_files/summary_wrappers.toml", "rb") as f: 49 | TEST_STRINGS = tomllib.load(f) 50 | 51 | 52 | @pytest.mark.unit 53 | @pytest.mark.parametrize( 54 | "test_key", 55 | [ 56 | "do_unwrap_summary", 57 | "do_unwrap_summary_empty", 58 | "do_unwrap_summary_only_newlines", 59 | "do_unwrap_summary_double_newlines", 60 | "do_unwrap_summary_leading_trailing", 61 | ], 62 | ) 63 | def test_do_unwrap_summary(test_key): 64 | source = TEST_STRINGS[test_key]["instring"] 65 | expected = TEST_STRINGS[test_key]["expected"] 66 | 67 | result = do_unwrap_summary(source) 68 | 69 | assert ( 70 | result == expected 71 | ), f"Failed {test_key}:\nExpected:\n{expected!r}\nGot:\n{result!r}" 72 | 73 | 74 | @pytest.mark.integration 75 | @pytest.mark.order(2) 76 | @pytest.mark.parametrize( 77 | "test_key, initial_indent, subsequent_indent, wrap_length", 78 | [ 79 | ("do_wrap_summary_no_wrap", " ", " ", 88), 80 | ("do_wrap_summary_disabled", " ", " ", 0), 81 | ("do_wrap_summary_with_wrap", " ", " ", 50), 82 | ("do_wrap_summary_empty", "", "", 50), 83 | ("do_wrap_summary_long_word", "", "", 50), 84 | ("do_wrap_summary_exact_length", "", "", 50), 85 | ("do_wrap_summary_tabs_spaces", " ", " ", 40), 86 | ("do_wrap_summary_wrap_length_1", ">", "-", 1), 87 | ], 88 | ) 89 | def test_do_wrap_summary( 90 | test_key, 91 | initial_indent, 92 | subsequent_indent, 93 | wrap_length, 94 | ): 95 | source = TEST_STRINGS[test_key]["instring"] 96 | expected = TEST_STRINGS[test_key]["expected"] 97 | 98 | result = do_wrap_summary( 99 | source, 100 | initial_indent=initial_indent, 101 | subsequent_indent=subsequent_indent, 102 | wrap_length=wrap_length, 103 | ) 104 | assert ( 105 | result == expected 106 | ), f"Failed {test_key}:\nExpected:\n{expected!r}\nGot:\n{result!r}" 107 | -------------------------------------------------------------------------------- /src/docformatter/wrappers/url.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # docformatter.wrappers.url.py is part of the docformatter project 4 | # 5 | # Copyright (C) 2012-2023 Steven Myint 6 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining 9 | # a copy of this software and associated documentation files (the 10 | # "Software"), to deal in the Software without restriction, including 11 | # without limitation the rights to use, copy, modify, merge, publish, 12 | # distribute, sublicense, and/or sell copies of the Software, and to 13 | # permit persons to whom the Software is furnished to do so, subject to 14 | # the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | # SOFTWARE. 27 | """This module provides docformatter's URL wrapper functions.""" 28 | 29 | 30 | # Standard Library Imports 31 | import contextlib 32 | from typing import Iterable, List, Tuple 33 | 34 | # docformatter Package Imports 35 | import docformatter.patterns as _patterns 36 | import docformatter.strings as _strings 37 | 38 | 39 | def do_wrap_urls( 40 | text: str, 41 | url_idx: Iterable, 42 | text_idx: int, 43 | indentation: str, 44 | wrap_length: int, 45 | ) -> Tuple[List[str], int]: 46 | """Wrap URLs in the long description. 47 | 48 | Parameters 49 | ---------- 50 | text : str 51 | The long description text. 52 | url_idx : list 53 | The list of URL indices found in the description text. 54 | text_idx : int 55 | The index in the description of the end of the last URL. 56 | indentation : str 57 | The string to use to indent each line in the long description. 58 | wrap_length : int 59 | The line length at which to wrap long lines in the description. 60 | 61 | Returns 62 | ------- 63 | _lines, _text_idx : tuple 64 | A list of the long description lines and the index in the long 65 | description where the last URL ended. 66 | """ 67 | _lines = [] 68 | for _url in url_idx: 69 | # Skip URL if it is simply a quoted pattern. 70 | if _patterns.do_skip_link(text, _url): 71 | continue 72 | 73 | # If the text including the URL is longer than the wrap length, 74 | # we need to split the description before the URL, wrap the pre-URL 75 | # text, and add the URL as a separate line. 76 | if len(text[text_idx : _url[1]]) > (wrap_length - len(indentation)): 77 | # Wrap everything in the description before the first URL. 78 | _lines.extend( 79 | _strings.description_to_list( 80 | text[text_idx : _url[0]], 81 | indentation, 82 | wrap_length, 83 | ) 84 | ) 85 | 86 | with contextlib.suppress(IndexError): 87 | if text[_url[0] - len(indentation) - 2] != "\n" and not _lines[-1]: 88 | _lines.pop(-1) 89 | 90 | # Add the URL making sure that the leading quote is kept with a quoted URL. 91 | _text = f"{text[_url[0]: _url[1]]}" 92 | with contextlib.suppress(IndexError): 93 | if _lines[0][-1] == '"': 94 | _lines[0] = _lines[0][:-2] 95 | _text = f'"{text[_url[0] : _url[1]]}' 96 | 97 | _lines.append(f"{_strings.do_clean_excess_whitespace(_text, indentation)}") 98 | 99 | text_idx = _url[1] 100 | 101 | return _lines, text_idx 102 | -------------------------------------------------------------------------------- /tests/wrappers/test_field_wrapper.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | # type: ignore 3 | # 4 | # tests.wrappers.test_field_wrapper.py is part of the docformatter project 5 | # 6 | # Copyright (C) 2012-2023 Steven Myint 7 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining 10 | # a copy of this software and associated documentation files (the 11 | # "Software"), to deal in the Software without restriction, including 12 | # without limitation the rights to use, copy, modify, merge, publish, 13 | # distribute, sublicense, and/or sell copies of the Software, and to 14 | # permit persons to whom the Software is furnished to do so, subject to 15 | # the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be 18 | # included in all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 24 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 25 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 26 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | # SOFTWARE. 28 | """Module for testing the description wrapper functions.""" 29 | 30 | # Standard Library Imports 31 | import contextlib 32 | import sys 33 | 34 | with contextlib.suppress(ImportError): 35 | if sys.version_info >= (3, 11): 36 | # Standard Library Imports 37 | import tomllib 38 | else: 39 | # Third Party Imports 40 | import tomli as tomllib 41 | 42 | # Third Party Imports 43 | import pytest 44 | 45 | # docformatter Package Imports 46 | from docformatter.wrappers import fields 47 | 48 | with open("tests/_data/string_files/field_wrappers.toml", "rb") as f: 49 | TEST_STRINGS = tomllib.load(f) 50 | 51 | 52 | @pytest.mark.unit 53 | @pytest.mark.parametrize( 54 | "test_key, field_idx, idx", 55 | [ 56 | ( 57 | "do_join_field_body", 58 | [(146, 161), (185, 208), (319, 342), (372, 395), (425, 433), (598, 605)], 59 | 0, 60 | ), 61 | ( 62 | "do_join_field_body_2", 63 | [(146, 161), (185, 208), (319, 342), (372, 395), (425, 433), (598, 605)], 64 | 1, 65 | ), 66 | ( 67 | "do_join_field_body_3", 68 | [(146, 161), (185, 208), (319, 342), (372, 395), (425, 433), (598, 605)], 69 | 2, 70 | ), 71 | ], 72 | ) 73 | def test_do_join_field_body(test_key, field_idx, idx): 74 | source = TEST_STRINGS[test_key]["instring"] 75 | expected = TEST_STRINGS[test_key]["expected"] 76 | 77 | result = fields._do_join_field_body(source, field_idx, idx) 78 | assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" 79 | 80 | 81 | @pytest.mark.unit 82 | @pytest.mark.parametrize( 83 | "test_key", 84 | [ 85 | "do_wrap_field", 86 | "do_wrap_long_field", 87 | ], 88 | ) 89 | def test_do_wrap_field(test_key): 90 | source = TEST_STRINGS[test_key]["instring"] 91 | expected = TEST_STRINGS[test_key]["expected"] 92 | 93 | result = fields._do_wrap_field(source[0], source[1], " ", 72) 94 | assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" 95 | 96 | 97 | @pytest.mark.integration 98 | @pytest.mark.order(0) 99 | @pytest.mark.parametrize( 100 | "test_key, field_idx, text_idx", 101 | [ 102 | ( 103 | "do_wrap_field_list", 104 | [(146, 162), (186, 209), (320, 343), (373, 396), (426, 434), (599, 606)], 105 | 140, 106 | ), 107 | ], 108 | ) 109 | def test_do_wrap_field_lists(test_key, field_idx, text_idx): 110 | source = TEST_STRINGS[test_key]["instring"] 111 | lines = TEST_STRINGS[test_key]["lines"] 112 | expected = TEST_STRINGS[test_key]["expected"] 113 | 114 | # We convert the returned tuple to a list because we can't store tuple in a TOML 115 | # file. 116 | result = list( 117 | fields.do_wrap_field_lists(source, field_idx, lines, text_idx, " ", 72) 118 | ) 119 | assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" 120 | -------------------------------------------------------------------------------- /tests/_data/string_files/field_wrappers.toml: -------------------------------------------------------------------------------- 1 | [do_join_field_body] 2 | instring = """ 3 | We only wrap simple descriptions. We leave doctests, multi-paragraph text, and bulleted lists alone. See http://www.docformatter.com/. 4 | 5 | :param str text: the text argument. 6 | :param str indentation: the super long description for the indentation argument that will require docformatter to wrap this line. 7 | :param int wrap_length: the wrap_length argument. 8 | :param bool force_wrap: the force_warp argument. 9 | :return: really long description text wrapped at n characters and a very long description of the return value so we can wrap this line abcd efgh ijkl mnop qrst uvwx yz. 10 | :rtype: str 11 | """ 12 | expected = " the text argument." 13 | 14 | [do_join_field_body_2] 15 | instring = """ 16 | We only wrap simple descriptions. We leave doctests, multi-paragraph text, and bulleted lists alone. See http://www.docformatter.com/. 17 | 18 | :param str text: the text argument. 19 | :param str indentation: the super long description for the indentation argument that will require docformatter to wrap this line. 20 | :param int wrap_length: the wrap_length argument. 21 | :param bool force_wrap: the force_warp argument. 22 | :return: really long description text wrapped at n characters and a very long description of the return value so we can wrap this line abcd efgh ijkl mnop qrst uvwx yz. 23 | :rtype: str 24 | """ 25 | expected = " the super long description for the indentation argument that will require docformatter to wrap this line." 26 | 27 | [do_join_field_body_3] 28 | instring = """ 29 | We only wrap simple descriptions. We leave doctests, multi-paragraph text, and bulleted lists alone. See http://www.docformatter.com/. 30 | 31 | :param str text: the text argument. 32 | :param str indentation: the super long description for the indentation argument that will require docformatter to wrap this line. 33 | :param int wrap_length: the wrap_length argument. 34 | :param bool force_wrap: the force_warp argument. 35 | :return: really long description text wrapped at n characters and a very long description of the return value so we can wrap this line abcd efgh ijkl mnop qrst uvwx yz. 36 | :rtype: str 37 | """ 38 | expected = " the wrap_length argument." 39 | 40 | [do_wrap_field] 41 | instring = [":param bundle:", " The bundle identifier."] 42 | expected = [" :param bundle: The bundle identifier."] 43 | 44 | [do_wrap_long_field] 45 | instring = [":param long:", " A very long description of a parameter that is going to need to be wrapped at 72 characters or else."] 46 | expected = [" :param long: A very long description of a parameter that is going to", 47 | " need to be wrapped at 72 characters or else."] 48 | 49 | [do_wrap_field_list] 50 | instring = """ 51 | We only wrap simple descriptions. We leave doctests, multi-paragraph text, and bulleted lists alone. See http://www.docformatter.com/. 52 | 53 | :param str text: the text argument. 54 | :param str indentation: the super long description for the indentation argument that will require docformatter to wrap this line. 55 | :param int wrap_length: the wrap_length argument 56 | :param bool force_wrap: the force_warp argument. 57 | :return: really long description text wrapped at n characters and a very long description of the return value so we can wrap this line abcd efgh ijkl mnop qrst uvwx yz. 58 | :rtype: str 59 | """ 60 | lines = [" We only wrap simple descriptions. We leave doctests, multi-paragraph text, and", 61 | " bulleted lists alone. See", 62 | " http://www.docformatter.com/."] 63 | expected = [ 64 | [" We only wrap simple descriptions. We leave doctests, multi-paragraph text, and", 65 | " bulleted lists alone. See", 66 | " http://www.docformatter.com/.", 67 | "", 68 | " :param str text: the text argument.", 69 | " :param str indentation: the super long description for the", 70 | " indentation argument that will require docformatter to wrap this", 71 | " line.", 72 | " :param int wrap_length: the wrap_length argument", 73 | " :param bool force_wrap: the force_warp argument.", 74 | " :return: really long description text wrapped at n characters and a", 75 | " very long description of the return value so we can wrap this", 76 | " line abcd efgh ijkl mnop qrst uvwx yz.", 77 | " :rtype: str"], 606] 78 | -------------------------------------------------------------------------------- /tests/_data/string_files/format_methods.toml: -------------------------------------------------------------------------------- 1 | # In this file, token lists have the following information: 2 | # [type, string, start, end, line] 3 | # for creating a TokenInfo() object. 4 | [do_add_unformatted_docstring] 5 | token = [ 6 | 3, '''"""This is a docstring.\n\n\n That should be on less lines\n"""''', 7 | [3, 4], [6, 7], 8 | ''' """This is a docstring.\n\n\n That should be on less lines\n """'''] 9 | 10 | [do_add_formatted_docstring] 11 | token = [ 12 | 3, '''"""This is a docstring.\n"""''', 13 | [3, 4], [6, 7], 14 | ''' """This is a docstring.\n """'''] 15 | next_token = [5, "\b", [2, 0], [2, 4], ''' """This is a docstring.\n'''] 16 | 17 | [do_format_oneline_docstring] 18 | source = "This is a one-line docstring." 19 | expected = '"""This is a one-line docstring."""' 20 | 21 | [do_format_oneline_docstring_that_ends_in_quote] 22 | source ='"Hello"' 23 | expected = '''""""Hello"."""''' 24 | 25 | [do_format_oneline_docstring_with_wrap] 26 | source = "This is a long one-line summary that will need to be wrapped because we're going to pass the --wrap-summaries argument." 27 | expected = ''' 28 | """This is a long one-line summary that will need to be wrapped 29 | because we're going to pass the --wrap-summaries argument."""''' 30 | 31 | [do_format_oneline_docstring_with_quotes_newline] 32 | source = "This is a long one-line summary that will have the closing quotes on a separate line because we're going to pass the --close-quotes-on-newline argument." 33 | expected = ''' 34 | """This is a long one-line summary that will have the closing quotes on a 35 | separate line because we're going to pass the --close-quotes-on-newline 36 | argument. 37 | """''' 38 | 39 | [do_format_oneline_docstring_make_multiline] 40 | source = "This is one-line docstring and we're going to pass the --make-summary-multi-line argument to see what happens." 41 | expected = ''' 42 | """ 43 | This is one-line docstring and we're going to pass the --make-summary- 44 | multi-line argument to see what happens. 45 | """''' 46 | 47 | [do_format_multiline_docstring] 48 | source = [ 49 | "This is the summary of a multiline docstring.", 50 | "This is the long description part of the same multiline docstring."] 51 | expected = '''"""This is the summary of a multiline docstring. 52 | 53 | This is the long description part of the same multiline docstring. 54 | """''' 55 | 56 | [do_format_multiline_docstring_pre_summary_newline] 57 | source = [ 58 | "This is the summary of a multiline docstring.", 59 | "This is the long description part of the same multiline docstring."] 60 | expected = '''""" 61 | This is the summary of a multiline docstring. 62 | 63 | This is the long description part of the same multiline docstring. 64 | """''' 65 | 66 | [do_format_multiline_docstring_post_description_blank] 67 | source = [ 68 | "This is the summary of a multiline docstring.", 69 | "This is the long description part of the same multiline docstring."] 70 | expected = '''"""This is the summary of a multiline docstring. 71 | 72 | This is the long description part of the same multiline docstring. 73 | 74 | """''' 75 | 76 | [do_rewrite_docstring_blocks] 77 | tokens = [ 78 | [1, "def", [1, 0], [1, 3], "def foo():\n"], 79 | [1, "foo", [1, 4], [1, 7], "def foo():\n"], 80 | [55, "(", [1, 7], [1, 8], "def foo():\n"], 81 | [55, ")", [1, 8], [1, 9], "def foo():\n"], 82 | [55, ":", [1, 9], [1, 10], "def foo():\n"], 83 | [4, "\n", [1, 10], [1, 11], "def foo():\n"], 84 | [5, " ", [3, 0], [3, 4], ''' """Hello foo."""\n'''], 85 | [3, '"""Hello foo."""', [3, 4], [5, 7], ''' """Hello foo."""\n'''], 86 | [4, "\n", [5, 7], [5, 8], ''' """Hello foo."""\n'''], 87 | [6, "", [6, 0], [6, 0], ""], 88 | [0, "", [6, 0], [6, 0], ""] 89 | ] 90 | expected = [ 91 | [1, "def", [1, 0], [1, 3], "def foo():\n"], 92 | [1, "foo", [1, 4], [1, 7], "def foo():\n"], 93 | [55, "(", [1, 7], [1, 8], "def foo():\n"], 94 | [55, ")", [1, 8], [1, 9], "def foo():\n"], 95 | [55, ":", [1, 9], [1, 10], "def foo():\n"], 96 | [4, "\n", [1, 10], [1, 11], "def foo():\n"], 97 | [5, " ", [2, 0], [2, 4], ''' """Hello foo.""" 98 | '''], 99 | [3, '"""Hello foo."""', [2, 4], [2, 7], ''' """Hello foo.""" 100 | '''], 101 | [4, "\n", [2, 7], [2, 8], ''' """Hello foo.""" 102 | '''], 103 | [6, "", [3, 0], [3, 0], ""], 104 | [0, "", [3, 0], [3, 0], ""] 105 | ] 106 | -------------------------------------------------------------------------------- /tests/_data/string_files/header_patterns.toml: -------------------------------------------------------------------------------- 1 | [is_alembic_header] 2 | instring = "Revision ID: >" 3 | expected = "Revision ID: " 4 | 5 | [is_not_alembic_header_epytext] 6 | instring = "@not alembic header: some non-alembic stuff" 7 | expected = "None" 8 | 9 | [is_not_alembic_header_google] 10 | instring = "not alembic header: some non-alembic stuff" 11 | expected = "None" 12 | 13 | [is_not_alembic_header_numpy] 14 | instring = "not alembic header : some non-alembic stuff" 15 | expected = "None" 16 | 17 | [is_numpy_section_header_parameters] 18 | instring = "Parameters\n----------\nsome parameters" 19 | expected = "Parameters\n----------" 20 | 21 | [is_numpy_section_header_returns] 22 | instring = "Returns\n----------\nsome return values" 23 | expected = "Returns\n----------" 24 | 25 | [is_numpy_section_header_yields] 26 | instring = "Yields\n----------\nsome yield value" 27 | expected = "Yields\n----------" 28 | 29 | [is_numpy_section_header_raises] 30 | instring = "Raises\n----------\nsome errors raised" 31 | expected = "Raises\n----------" 32 | 33 | [is_numpy_section_header_receives] 34 | instring = "Receives\n----------\nsome values to receive" 35 | expected = "Receives\n----------" 36 | 37 | [is_numpy_section_header_other_parameters] 38 | instring = "Other Parameters\n----------\nsome other parameters" 39 | expected = "Other Parameters\n----------" 40 | 41 | [is_numpy_section_header_warns] 42 | instring = "Warns\n----------\nthe little used Warns section" 43 | expected = "Warns\n----------" 44 | 45 | [is_numpy_section_header_warnings] 46 | instring = "Warnings\n----------\nthe little used Warnings section" 47 | expected = "Warnings\n----------" 48 | 49 | [is_numpy_section_header_see_also] 50 | instring = "See Also\n----------\nother stuff you should look at" 51 | expected = "See Also\n----------" 52 | 53 | [is_numpy_section_header_examples] 54 | instring = "Examples\n----------\nsome examples" 55 | expected = "Examples\n----------" 56 | 57 | [is_numpy_section_header_notes] 58 | instring = "Notes\n----------\nsome notes" 59 | expected = "Notes\n----------" 60 | 61 | [is_not_numpy_section_header] 62 | instring = "Section\n----------\na section that is not standard" 63 | expected = "None" 64 | 65 | [is_not_numpy_section_header_wrong_dashes] 66 | instring = "Parameters\n**********\na section with standard name but wrong dashes" 67 | expected = "None" 68 | 69 | [is_rest_section_header_pound] 70 | instring = "######\nPart 1\n######\nsome part" 71 | expected = "######\nPart 1\n######" 72 | 73 | [is_rest_section_header_star] 74 | instring = "*********\nChapter 1\n*********\nsome chapter" 75 | expected = "*********\nChapter 1\n*********" 76 | 77 | [is_rest_section_header_equal] 78 | instring = "Section 1\n=========\nsome section" 79 | expected = "Section 1\n=========" 80 | 81 | [is_rest_section_header_dash] 82 | instring = "Subsection 1\n------------\nsome subsection" 83 | expected = "Subsection 1\n------------" 84 | 85 | [is_rest_section_header_circumflex] 86 | instring = "Subsubsection 1\n^^^^^^^^^^^^^^^\nsome subsubsection" 87 | expected = "Subsubsection 1\n^^^^^^^^^^^^^^^" 88 | 89 | [is_rest_section_header_single_quote] 90 | instring = "Section 2\n'''''''''\nanother section" 91 | expected = "Section 2\n'''''''''" 92 | 93 | [is_rest_section_header_double_quote] 94 | instring = '''Subsection 2 95 | """""""""""" 96 | another subsection 97 | ''' 98 | expected = '''Subsection 2 99 | """"""""""""''' 100 | 101 | [is_rest_section_header_plus] 102 | instring = "Subsubsection 2\n+++++++++++++++\nanother subsubsection" 103 | expected = "Subsubsection 2\n+++++++++++++++" 104 | 105 | [is_rest_section_header_underscore] 106 | instring = "______\nPart 3\n______\nyet another part" 107 | expected = "______\nPart 3\n______" 108 | 109 | [is_rest_section_header_tilde] 110 | instring = "Section 3\n~~~~~~~~~\nyet another section" 111 | expected = "Section 3\n~~~~~~~~~" 112 | 113 | [is_rest_section_header_colon] 114 | instring = "Subsection 3\n::::::::::::\nyet another subsection" 115 | expected = "Subsection 3\n::::::::::::" 116 | 117 | [is_rest_section_header_backtick] 118 | instring = "Subsubsection 3\n```````````````\nyet another subsubsection" 119 | expected = "Subsubsection 3\n```````````````" 120 | 121 | [is_rest_section_header_period] 122 | instring = "Part 4\n......\nthe fourth part" 123 | expected = "Part 4\n......" 124 | 125 | [is_not_rest_section_header_unknown_adornments] 126 | instring = "??????\nPart 5\n??????\na part with unknown adornments" 127 | expected = "None" 128 | -------------------------------------------------------------------------------- /tests/patterns/test_url_patterns.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | # type: ignore 3 | # 4 | # tests.patterns.test_url_patterns.py is part of the docformatter project 5 | # 6 | # Copyright (C) 2012-2023 Steven Myint 7 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining 10 | # a copy of this software and associated documentation files (the 11 | # "Software"), to deal in the Software without restriction, including 12 | # without limitation the rights to use, copy, modify, merge, publish, 13 | # distribute, sublicense, and/or sell copies of the Software, and to 14 | # permit persons to whom the Software is furnished to do so, subject to 15 | # the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be 18 | # included in all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 24 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 25 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 26 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | # SOFTWARE. 28 | """Module for testing the URL pattern detection functions.""" 29 | 30 | # Standard Library Imports 31 | import contextlib 32 | import sys 33 | 34 | with contextlib.suppress(ImportError): 35 | if sys.version_info >= (3, 11): 36 | # Standard Library Imports 37 | import tomllib 38 | else: 39 | # Third Party Imports 40 | import tomli as tomllib 41 | 42 | # Third Party Imports 43 | import pytest 44 | 45 | # docformatter Package Imports 46 | from docformatter.patterns import do_find_links, do_skip_link 47 | 48 | with open("tests/_data/string_files/url_patterns.toml", "rb") as f: 49 | TEST_STRINGS = tomllib.load(f) 50 | 51 | 52 | @pytest.mark.unit 53 | @pytest.mark.parametrize( 54 | "test_key", 55 | [ 56 | "apple_filing_protocol", 57 | "network_filing_system", 58 | "samba_filing_system", 59 | "apt", 60 | "bitcoin", 61 | "chrome", 62 | "java_compressed_archive", 63 | "concurrent_version_system", 64 | "git", 65 | "subversion", 66 | "domain_name_system", 67 | "file_transfer_protocol", 68 | "secure_file_transfer_protocol", 69 | "ssh_file_transfer_protocol", 70 | "finger", 71 | "rsync", 72 | "telnet", 73 | "virtual_network_computing", 74 | "extensible_resource_identifier", 75 | "fish", 76 | "ssh", 77 | "webdav_transfer_protocol", 78 | "hypertext_transfer_protocol", 79 | "secure_hypertext_transfer_protocol", 80 | "obsolete_secure_hypertext_transfer_protocol", 81 | "imap", 82 | "smtp", 83 | "pop", 84 | "internet_printing_protocol", 85 | "secure_internet_printing_protocol", 86 | "internet_relay_chat", 87 | "internet_relay_chat_v6", 88 | "secure_internet_relay_chat", 89 | "short_message_service", 90 | "extensible_messaging_and_presence_protocol", 91 | "ldap", 92 | "secure_ldap", 93 | "amazon_s3", 94 | "usenet", 95 | "nntp", 96 | "session_initiation_protocol", 97 | "secure_session_initiation_protocol", 98 | "simple_network_management_protocol", 99 | ], 100 | ) 101 | def test_do_find_links(test_key): 102 | source = TEST_STRINGS[test_key]["instring"] 103 | expected = TEST_STRINGS[test_key]["expected"] 104 | 105 | result = do_find_links(source) 106 | assert ( 107 | result[0][0] == expected[0] 108 | ), f"\nFailed {test_key}\nExpected {expected[0]}\nGot {result[0][0]}" 109 | assert ( 110 | result[0][1] == expected[1] 111 | ), f"\nFailed {test_key}\nExpected {expected[0]}\nGot {result[0][1]}" 112 | 113 | 114 | @pytest.mark.unit 115 | @pytest.mark.parametrize( 116 | "test_key, index", 117 | [ 118 | ("only_link_patterns", (70, 76)), 119 | ("only_link_patterns", (137, 145)), 120 | ("already_wrapped_url", (70, 117)), 121 | ], 122 | ) 123 | def test_do_skip_link(test_key, index): 124 | source = TEST_STRINGS[test_key]["instring"] 125 | expected = TEST_STRINGS[test_key]["expected"] 126 | 127 | result = do_skip_link(source, index) 128 | assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" 129 | -------------------------------------------------------------------------------- /src/docformatter/patterns/headers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # docformatter.patterns.headers.py is part of the docformatter project 4 | # 5 | # Copyright (C) 2012-2023 Steven Myint 6 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining 9 | # a copy of this software and associated documentation files (the 10 | # "Software"), to deal in the Software without restriction, including 11 | # without limitation the rights to use, copy, modify, merge, publish, 12 | # distribute, sublicense, and/or sell copies of the Software, and to 13 | # permit persons to whom the Software is furnished to do so, subject to 14 | # the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | # SOFTWARE. 27 | """This module provides docformatter's header pattern recognition functions.""" 28 | 29 | 30 | # Standard Library Imports 31 | import re 32 | from re import Match 33 | from typing import Union 34 | 35 | # docformatter Package Imports 36 | from docformatter.constants import ( 37 | ALEMBIC_REGEX, 38 | NUMPY_SECTION_REGEX, 39 | REST_SECTION_REGEX, 40 | ) 41 | 42 | 43 | def is_alembic_header(line: str) -> Union[Match[str], None]: 44 | """Check if the line is an Alembic header. 45 | 46 | Parameters 47 | ---------- 48 | line : str 49 | The line to check for Alembic header patterns. 50 | 51 | Notes 52 | ----- 53 | Alembic headers have the following pattern: 54 | Revision ID: > 55 | Revises: 56 | Create Date: 2023-01-06 10:13:28.156709 57 | 58 | Returns 59 | ------- 60 | bool 61 | True if the line matches an Alembic header pattern, False otherwise. 62 | """ 63 | return re.match(ALEMBIC_REGEX, line) 64 | 65 | 66 | def is_numpy_section_header(line: str) -> Union[Match[str], None]: 67 | r"""Check if the line is a NumPy section header. 68 | 69 | Parameters 70 | ---------- 71 | line : str 72 | The line to check for NumPy section header patterns. 73 | 74 | Notes 75 | ----- 76 | NumPy section headers have the following pattern: 77 | header\n---- 78 | 79 | The following NumPy section headers are recognized: 80 | 81 | * Parameters 82 | * Other Parameters 83 | * Receives 84 | * Returns 85 | * Yields 86 | * Raises 87 | * Warns 88 | * Warnings 89 | * Notes 90 | * See Also 91 | * Examples 92 | 93 | Returns 94 | ------- 95 | Match[str] | None 96 | A match object if the line matches a NumPy section header pattern, None 97 | otherwise. 98 | """ 99 | return re.match(NUMPY_SECTION_REGEX, line) 100 | 101 | 102 | def is_rest_section_header(line: str) -> Union[Match[str], None]: 103 | r"""Check if the line is a reST section header. 104 | 105 | Parameters 106 | ---------- 107 | line : str 108 | The line to check for reST section header patterns. 109 | 110 | Notes 111 | ----- 112 | reST section headers have the following patterns: 113 | ====\ndescription\n==== 114 | ----\ndescription\n---- 115 | description\n---- 116 | 117 | The following adornments used in Python documentation are supported (see 118 | https://devguide.python.org/documentation/markup/#sections): 119 | 120 | #, for parts 121 | *, for chapters 122 | =, for sections 123 | -, for subsections 124 | ^, for subsubsections 125 | 126 | The following additional docutils recommended adornments are supported (see 127 | https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#sections): 128 | 129 | ', single quote 130 | ", double quote 131 | +, plus sign 132 | _, underscore 133 | ~, tilde 134 | `, backtick 135 | ., period 136 | :, colon 137 | 138 | Returns 139 | ------- 140 | bool 141 | True if the line matches a reST section header pattern, False otherwise. 142 | """ 143 | return re.match(REST_SECTION_REGEX, line) 144 | -------------------------------------------------------------------------------- /tests/_data/string_files/list_patterns.toml: -------------------------------------------------------------------------------- 1 | [is_bullet_list] 2 | instring = """* parameter\n 3 | - parameter\n 4 | + parameter\n""" 5 | strict = false 6 | style = "sphinx" 7 | expected = true 8 | 9 | [is_enum_list] 10 | instring = """1. parameter\n 11 | 2. parameter\n 12 | 3. parameter\n""" 13 | strict = false 14 | style = "sphinx" 15 | expected = true 16 | 17 | [is_option_list] 18 | instring = """ 19 | -a include all the stuff\n 20 | --config the path to the configuration file\n 21 | -h, --help show this help\n""" 22 | strict = false 23 | style = "sphinx" 24 | expected = true 25 | 26 | [is_option_list_indented] 27 | instring = """ 28 | -a include all the stuff\n 29 | --config the path to the configuration file\n 30 | -h, --help show this help\n""" 31 | strict = false 32 | style = "sphinx" 33 | expected = true 34 | 35 | [is_list_with_single_hyphen] 36 | instring = """\ 37 | Keyword arguments: 38 | real - the real part (default 0.0) 39 | imag - the imaginary part (default 0.0) 40 | """ 41 | strict = false 42 | style = "sphinx" 43 | expected = true 44 | 45 | [is_list_with_double_hyphen] 46 | instring = """\ 47 | Keyword arguments: 48 | real -- the real part (default 0.0) 49 | imag -- the imaginary part (default 0.0) 50 | """ 51 | strict = false 52 | style = "sphinx" 53 | expected = true 54 | 55 | [is_list_with_at_sign] 56 | instring = """\ 57 | Keyword arguments: 58 | @real the real part (default 0.0) 59 | @imag the imaginary part (default 0.0) 60 | """ 61 | strict = false 62 | style = "sphinx" 63 | expected = true 64 | 65 | [is_heuristic_list] 66 | instring = "Example:\nrelease-1.1/\nrelease-1.2/\nrelease-1.3/\nrelease-1.4/\nrelease-1.4.1/\nrelease-1.5/\n" 67 | strict = false 68 | style = "sphinx" 69 | expected = true 70 | 71 | [is_type_of_list_strict_wrap] 72 | instring = "Launch\nthe\nrocket." 73 | strict = true 74 | style = "numpy" 75 | expected = false 76 | 77 | [is_type_of_list_non_strict_wrap] # See issue #67. 78 | instring = "Launch\nthe\nrocket." 79 | strict = false 80 | style = "numpy" 81 | expected = true 82 | 83 | [is_type_of_list_alembic_header] # See issue #242. 84 | instring = """Add some column. 85 | 86 | Revision ID: > 87 | Revises: 88 | Create Date: 2023-01-06 10:13:28.156709 89 | """ 90 | strict = false 91 | style = "numpy" 92 | expected = true 93 | 94 | [is_not_list_sphinx_style] # See requirement docformatter_10.4 95 | instring = """\ 96 | Using Sphinx parameter list 97 | 98 | :param str arg1: the first argument. 99 | :param int arg2: the second argument. 100 | """ 101 | strict = false 102 | style = "sphinx" 103 | expected = false 104 | 105 | [is_sphinx_list_numpy_style] # See requirements docformatter_10.2.1 and docformatter_10.3.1 106 | instring = """\ 107 | Using Sphinx parameter list 108 | 109 | :param str arg1: the first argument. 110 | :param int arg2: the second argument. 111 | """ 112 | strict = false 113 | style = "numpy" 114 | expected = true 115 | 116 | [is_numpy_list_sphinx_style] # See requirement docformatter_10.4.1 117 | instring = """\ 118 | Using Numpy parameter list 119 | 120 | Parameters 121 | ---------- 122 | arg1 : str 123 | The first argument. 124 | arg2 : int 125 | The second argument. 126 | """ 127 | strict = false 128 | style = "sphinx" 129 | expected = true 130 | 131 | [is_google_list_numpy_style] 132 | instring = """\ 133 | Args: 134 | stream (BinaryIO): Binary stream (usually a file object). 135 | """ 136 | strict = true 137 | style = "numpy" 138 | expected = true 139 | 140 | [is_literal_block] 141 | instring = """\ 142 | This is a description. 143 | 144 | Example code:: 145 | 146 | config(par=value) 147 | 148 | Example code2:: 149 | 150 | with config(par=value) as f: 151 | pass 152 | """ 153 | strict = false 154 | style = "numpy" 155 | expected = true 156 | 157 | [is_reST_header] 158 | instring = """\ 159 | =============================== 160 | Example of creating an example. 161 | =============================== 162 | 163 | .. currentmodule:: my_project 164 | 165 | In this example, we illustrate how to create 166 | an example. 167 | """ 168 | strict = false 169 | style = "numpy" 170 | expected = true 171 | 172 | [is_sphinx_field_list] 173 | instring = """\ 174 | This is a description. 175 | 176 | :parameter arg1: the first argument. 177 | :parameter arg2: the second argument. 178 | """ 179 | strict = false 180 | style = "sphinx" 181 | expected = false 182 | 183 | [is_epytext_field_list] 184 | instring = """\ 185 | This is a description. 186 | 187 | @param arg1: the first argument. 188 | @param arg2: the second argument. 189 | """ 190 | strict = false 191 | style = "epytext" 192 | expected = false 193 | -------------------------------------------------------------------------------- /src/docformatter/patterns/misc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # docformatter.patterns.misc.py is part of the docformatter project 4 | # 5 | # Copyright (C) 2012-2023 Steven Myint 6 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining 9 | # a copy of this software and associated documentation files (the 10 | # "Software"), to deal in the Software without restriction, including 11 | # without limitation the rights to use, copy, modify, merge, publish, 12 | # distribute, sublicense, and/or sell copies of the Software, and to 13 | # permit persons to whom the Software is furnished to do so, subject to 14 | # the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | # SOFTWARE. 27 | """This module provides docformatter's miscellaneous pattern recognition functions.""" 28 | 29 | 30 | # Standard Library Imports 31 | import re 32 | import tokenize 33 | from re import Match 34 | from typing import Union 35 | 36 | # docformatter Package Imports 37 | from docformatter.constants import LITERAL_REGEX, URL_REGEX 38 | 39 | 40 | # TODO: Create INLINE_MATH_REGEX in constants.py and use it here. 41 | def is_inline_math(line: str) -> Union[Match[str], None]: 42 | """Check if the line is an inline math expression. 43 | 44 | Parameters 45 | ---------- 46 | line : str 47 | The line to check for inline math patterns. 48 | 49 | Returns 50 | ------- 51 | Match[str] | None 52 | A match object if the line matches an inline math pattern, None otherwise. 53 | 54 | Notes 55 | ----- 56 | Inline math expressions have the following pattern: 57 | c :math:`[0, `]` 58 | """ 59 | return re.match(r" *\w *:[a-zA-Z0-9_\- ]*:", line) 60 | 61 | 62 | def is_literal_block(line: str) -> Union[Match[str], None]: 63 | """Check if the line is a literal block. 64 | 65 | Parameters 66 | ---------- 67 | line : str 68 | The line to check for literal block patterns. 69 | 70 | Returns 71 | ------- 72 | Match[str] | None 73 | A match object if the line matches a literal block pattern, None otherwise. 74 | 75 | Notes 76 | ----- 77 | Literal blocks have the following pattern: 78 | :: 79 | code 80 | """ 81 | return re.match(LITERAL_REGEX, line) 82 | 83 | 84 | def is_probably_beginning_of_sentence(line: str) -> Union[Match[str], None, bool]: 85 | """Determine if the line begins a sentence. 86 | 87 | Parameters 88 | ---------- 89 | line : str 90 | The line to be tested. 91 | 92 | Returns 93 | ------- 94 | bool 95 | True if this token is the beginning of a sentence, False otherwise. 96 | """ 97 | # Check heuristically for a parameter list. 98 | for token in ["@", "-", r"\*"]: 99 | if re.search(rf"\s{token}\s", line): 100 | return True 101 | 102 | stripped_line = line.strip() 103 | is_beginning_of_sentence = re.match(r"^[-@\)]", stripped_line) 104 | is_pydoc_ref = re.match(r"^:\w+:", stripped_line) 105 | 106 | return is_beginning_of_sentence and not is_pydoc_ref 107 | 108 | 109 | def is_some_sort_of_code(text: str) -> bool: 110 | """Return True if the text looks like code. 111 | 112 | Parameters 113 | ---------- 114 | text : str 115 | The text to check for code patterns. 116 | 117 | Returns 118 | ------- 119 | bool 120 | True if the text contains and code patterns, False otherwise. 121 | """ 122 | return any( 123 | len(word) > 50 and not re.match(URL_REGEX, word) # noqa: PLR2004 124 | for word in text.split() 125 | ) 126 | 127 | 128 | def is_string_constant(token: tokenize.TokenInfo) -> bool: 129 | """Determine if docstring token is actually a string constant. 130 | 131 | Parameters 132 | ---------- 133 | token : TokenInfo 134 | The token immediately preceding the docstring token. 135 | 136 | Returns 137 | ------- 138 | bool 139 | True if the doctring token is actually string constant, False otherwise. 140 | """ 141 | if token.type == tokenize.OP and token.string == "=": 142 | return True 143 | 144 | return False 145 | -------------------------------------------------------------------------------- /src/docformatter/wrappers/description.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # docformatter.wrappers.description.py is part of the docformatter project 4 | # 5 | # Copyright (C) 2012-2023 Steven Myint 6 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining 9 | # a copy of this software and associated documentation files (the 10 | # "Software"), to deal in the Software without restriction, including 11 | # without limitation the rights to use, copy, modify, merge, publish, 12 | # distribute, sublicense, and/or sell copies of the Software, and to 13 | # permit persons to whom the Software is furnished to do so, subject to 14 | # the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | # SOFTWARE. 27 | """This module provides docformatter's description wrapper functions.""" 28 | 29 | 30 | # Standard Library Imports 31 | import contextlib 32 | from typing import List 33 | 34 | # docformatter Package Imports 35 | import docformatter.patterns as _patterns 36 | import docformatter.strings as _strings 37 | 38 | 39 | def do_wrap_description( # noqa: PLR0913 40 | text, 41 | indentation, 42 | wrap_length, 43 | force_wrap, 44 | strict, 45 | rest_sections, 46 | style: str = "sphinx", 47 | ): 48 | """Return line-wrapped description text. 49 | 50 | We only wrap simple descriptions. We leave doctests, multi-paragraph text, and 51 | bulleted lists alone. 52 | 53 | Parameters 54 | ---------- 55 | text : str 56 | The unwrapped description text. 57 | indentation : str 58 | The indentation string. 59 | wrap_length : int 60 | The line length at which to wrap long lines. 61 | force_wrap : bool 62 | Whether to force docformatter to wrap long lines when normally they 63 | would remain untouched. 64 | strict : bool 65 | Whether to strictly follow reST syntax to identify lists. 66 | rest_sections : str 67 | A regular expression used to find reST section header adornments. 68 | style : str 69 | The name of the docstring style to use when dealing with parameter 70 | lists (default is sphinx). 71 | 72 | Returns 73 | ------- 74 | str 75 | The description wrapped at wrap_length characters. 76 | """ 77 | text = _strings.do_strip_leading_blank_lines(text) 78 | 79 | # TODO: Don't wrap the doctests, but wrap the remainder of the docstring. 80 | # Do not modify docstrings with doctests at all. 81 | if ">>>" in text: 82 | return text 83 | 84 | text = _strings.do_reindent(text, indentation).rstrip() 85 | 86 | # TODO: Don't wrap the code section or the lists, but wrap everything else. 87 | # Ignore possibly complicated cases. 88 | if wrap_length <= 0 or ( 89 | not force_wrap 90 | and ( 91 | _patterns.is_some_sort_of_code(text) 92 | or _patterns.do_find_rest_directives(text) 93 | or _patterns.is_type_of_list(text, strict, style) 94 | ) 95 | ): 96 | return text 97 | 98 | lines = _strings.do_split_description(text, indentation, wrap_length, style) 99 | 100 | return indentation + "\n".join(lines).strip() 101 | 102 | 103 | def do_close_description( 104 | text: str, 105 | text_idx: int, 106 | indentation: str, 107 | ) -> List[str]: 108 | """Wrap any description following the last URL or field list. 109 | 110 | Parameters 111 | ---------- 112 | text : str 113 | The docstring text. 114 | text_idx : int 115 | The index of the last URL or field list match. 116 | indentation : str 117 | The indentation string to use with docstrings. 118 | 119 | Returns 120 | ------- 121 | _split_lines : list 122 | The text input split into individual lines. 123 | """ 124 | _split_lines = [] 125 | with contextlib.suppress(IndexError): 126 | _split_lines = ( 127 | text[text_idx + 1 :] if text[text_idx] == "\n" else text[text_idx:] 128 | ).splitlines() 129 | for _idx, _line in enumerate(_split_lines): 130 | if _line not in ["", "\n", f"{indentation}"]: 131 | _split_lines[_idx] = f"{indentation}{_line.strip()}" 132 | 133 | return _split_lines 134 | -------------------------------------------------------------------------------- /tests/test_utility_functions.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | # type: ignore 3 | # 4 | # tests.test_utility_functions.py is part of the docformatter project 5 | # 6 | # Copyright (C) 2012-2023 Steven Myint 7 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining 10 | # a copy of this software and associated documentation files (the 11 | # "Software"), to deal in the Software without restriction, including 12 | # without limitation the rights to use, copy, modify, merge, publish, 13 | # distribute, sublicense, and/or sell copies of the Software, and to 14 | # permit persons to whom the Software is furnished to do so, subject to 15 | # the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be 18 | # included in all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 24 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 25 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 26 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | # SOFTWARE. 28 | """Module for testing utility functions used when processing docstrings.""" 29 | 30 | 31 | # Standard Library Imports 32 | import contextlib 33 | import sys 34 | 35 | with contextlib.suppress(ImportError): 36 | if sys.version_info >= (3, 11): 37 | # Standard Library Imports 38 | import tomllib 39 | else: 40 | # Third Party Imports 41 | import tomli as tomllib 42 | 43 | # Third Party Imports 44 | import pytest 45 | 46 | # docformatter Package Imports 47 | from docformatter.util import find_py_files, has_correct_length, is_in_range 48 | 49 | with open("tests/_data/string_files/utility_functions.toml", "rb") as f: 50 | TEST_STRINGS = tomllib.load(f) 51 | 52 | 53 | @pytest.mark.unit 54 | @pytest.mark.parametrize( 55 | "test_key", 56 | [ 57 | "has_correct_length_none", 58 | "has_correct_length_start_in_range", 59 | "has_correct_length_end_in_range", 60 | "has_correct_length_both_in_range", 61 | "has_correct_length_start_out_of_range", 62 | "has_correct_length_end_out_of_range", 63 | "has_correct_length_both_out_of_range", 64 | ], 65 | ) 66 | def test_has_correct_length(test_key): 67 | """Test has_correct_length() function.""" 68 | length_range = TEST_STRINGS[test_key]["length_range"] 69 | start = TEST_STRINGS[test_key]["start"] 70 | end = TEST_STRINGS[test_key]["end"] 71 | expected = TEST_STRINGS[test_key]["expected"] 72 | 73 | if length_range == "None": 74 | length_range = None 75 | 76 | result = has_correct_length(length_range, start, end) 77 | assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" 78 | 79 | 80 | @pytest.mark.unit 81 | @pytest.mark.parametrize( 82 | "test_key", 83 | [ 84 | "is_in_range_none", 85 | "is_in_range_start_in_range", 86 | "is_in_range_end_in_range", 87 | "is_in_range_both_in_range", 88 | "is_in_range_out_of_range", 89 | ], 90 | ) 91 | def test_is_in_range(test_key): 92 | """Test is_in_range() function.""" 93 | line_range = TEST_STRINGS[test_key]["line_range"] 94 | start = TEST_STRINGS[test_key]["start"] 95 | end = TEST_STRINGS[test_key]["end"] 96 | expected = TEST_STRINGS[test_key]["expected"] 97 | 98 | if line_range == "None": 99 | line_range = None 100 | 101 | result = is_in_range(line_range, start, end) 102 | assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" 103 | 104 | 105 | @pytest.mark.unit 106 | @pytest.mark.parametrize( 107 | "test_key, recursive", 108 | [ 109 | ("find_py_file", False), 110 | ("find_py_file_recursive", True), 111 | ("skip_hidden_py_file", False), 112 | ("skip_hidden_py_file_recursive", True), 113 | ("ignore_non_py_file", False), 114 | ("ignore_non_py_file_recursive", True), 115 | ("exclude_py_file", False), 116 | ("exclude_py_file_recursive", True), 117 | ("exclude_multiple_files", False), 118 | ("exclude_multiple_files_recursive", True), 119 | ], 120 | ) 121 | def test_find_py_files(test_key, recursive): 122 | """Test find_py_files() function.""" 123 | sources = TEST_STRINGS[test_key]["sources"] 124 | exclude = TEST_STRINGS[test_key]["exclude"] 125 | expected = TEST_STRINGS[test_key]["expected"] 126 | 127 | result = list(find_py_files(sources, recursive, exclude)) 128 | assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" 129 | -------------------------------------------------------------------------------- /src/docformatter/encode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # docformatter.encode.py is part of the docformatter project 4 | # 5 | # Copyright (C) 2012-2023 Steven Myint 6 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining 9 | # a copy of this software and associated documentation files (the 10 | # "Software"), to deal in the Software without restriction, including 11 | # without limitation the rights to use, copy, modify, merge, publish, 12 | # distribute, sublicense, and/or sell copies of the Software, and to 13 | # permit persons to whom the Software is furnished to do so, subject to 14 | # the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | # SOFTWARE. 27 | """This module provides docformatter's Encoder class.""" 28 | 29 | 30 | # Standard Library Imports 31 | import collections 32 | import locale 33 | import sys 34 | from typing import Dict, List 35 | 36 | # Third Party Imports 37 | from charset_normalizer import from_path # pylint: disable=import-error 38 | 39 | unicode = str 40 | 41 | 42 | class Encoder: 43 | """Encoding and decoding of files.""" 44 | 45 | CR = "\r" 46 | LF = "\n" 47 | CRLF = "\r\n" 48 | 49 | # Default encoding to use if the file encoding cannot be detected 50 | DEFAULT_ENCODING = sys.getdefaultencoding() 51 | 52 | def __init__(self): 53 | """Initialize an Encoder instance.""" 54 | self.encoding = self.DEFAULT_ENCODING 55 | self.system_encoding = locale.getpreferredencoding() or sys.getdefaultencoding() 56 | 57 | def do_detect_encoding(self, filename) -> None: 58 | """Return the detected file encoding. 59 | 60 | Parameters 61 | ---------- 62 | filename : str 63 | The full path name of the file whose encoding is to be detected. 64 | """ 65 | try: 66 | detection_result = from_path(filename).best() 67 | if detection_result and detection_result.encoding in ["utf_16", "utf_32"]: 68 | # Treat undetectable/binary encodings as failure 69 | self.encoding = self.DEFAULT_ENCODING 70 | else: 71 | self.encoding = ( 72 | detection_result.encoding 73 | if detection_result 74 | else self.DEFAULT_ENCODING 75 | ) 76 | 77 | # Check for correctness of encoding. 78 | with self.do_open_with_encoding(filename) as check_file: 79 | check_file.read() 80 | except (SyntaxError, LookupError, UnicodeDecodeError): 81 | self.encoding = self.DEFAULT_ENCODING 82 | 83 | def do_find_newline(self, source: List[str]) -> str: 84 | """Return type of newline used in source. 85 | 86 | Parameters 87 | ---------- 88 | source : list 89 | A list of lines. 90 | 91 | Returns 92 | ------- 93 | newline : str 94 | The most prevalent new line type found. 95 | """ 96 | assert not isinstance(source, unicode) 97 | 98 | counter: Dict[str, int] = collections.defaultdict(int) 99 | for line in source: 100 | if line.endswith(self.CRLF): 101 | counter[self.CRLF] += 1 102 | elif line.endswith(self.CR): 103 | counter[self.CR] += 1 104 | elif line.endswith(self.LF): 105 | counter[self.LF] += 1 106 | 107 | return ( 108 | sorted( 109 | counter, 110 | key=counter.get, # type: ignore 111 | reverse=True, 112 | ) 113 | or [self.LF] 114 | )[0] 115 | 116 | def do_open_with_encoding(self, filename, mode: str = "r"): 117 | """Return opened file with a specific encoding. 118 | 119 | Parameters 120 | ---------- 121 | filename : str 122 | The full path name of the file to open. 123 | mode : str 124 | The mode to open the file in. Defaults to read-only. 125 | 126 | Returns 127 | ------- 128 | contents : TextIO 129 | The contents of the file. 130 | """ 131 | return open( 132 | filename, mode=mode, encoding=self.encoding, newline="" 133 | ) # Preserve line endings 134 | -------------------------------------------------------------------------------- /docs/source/configuration.rst: -------------------------------------------------------------------------------- 1 | How to Configure docformatter 2 | ============================= 3 | 4 | The command line options for ``docformatter`` can also be stored in a 5 | configuration file. Currently only ``pyproject.toml``, ``setup.cfg``, and 6 | ``tox.ini`` are supported. The configuration file can be passed with a full 7 | path. For example: 8 | 9 | .. code-block:: console 10 | 11 | $ docformatter --config ~/.secret/path/to/pyproject.toml 12 | 13 | If no configuration file is explicitly passed, ``docformatter`` will search 14 | the current directory for the supported files and use the first one found. 15 | The order of precedence is ``pyproject.toml``, ``setup.cfg``, then ``tox.ini``. 16 | 17 | In ``pyproject.toml``, add a section ``[tool.docformatter]`` with 18 | options listed using the same name as command line argument. For example: 19 | 20 | .. code-block:: yaml 21 | 22 | [tool.docformatter] 23 | recursive = true 24 | wrap-summaries = 82 25 | blank = true 26 | 27 | In ``setup.cfg`` or ``tox.ini``, add a ``[docformatter]`` section. 28 | 29 | .. code-block:: yaml 30 | 31 | [docformatter] 32 | recursive = true 33 | wrap-summaries = 82 34 | blank = true 35 | 36 | Command line arguments will take precedence over configuration file settings. 37 | For example, if the following is in your ``pyproject.toml`` 38 | 39 | .. code-block:: yaml 40 | 41 | [tool.docformatter] 42 | recursive = true 43 | wrap-summaries = 82 44 | wrap-descriptions = 81 45 | blank = true 46 | 47 | And you invoke docformatter as follows: 48 | 49 | .. code-block:: console 50 | 51 | $ docformatter --config ~/.secret/path/to/pyproject.toml --wrap-summaries 68 52 | 53 | Summaries will be wrapped at 68, not 82. 54 | 55 | A Note on Options to Control Styles 56 | ----------------------------------- 57 | There are various ``docformatter`` options that can be used to control the 58 | style of the docstring. These options can be passed on the command line or 59 | set in a configuration file. Currently, the style options are: 60 | 61 | * ``--black`` 62 | * ``-s`` or ``--style`` 63 | 64 | When passing the ``--black`` option, the following arguments are set 65 | automatically: 66 | 67 | * ``--pre-summary-space`` is set to True 68 | * ``--wrap-descriptions`` is set to 88 69 | * ``--wrap-summaries`` is set to 88 70 | 71 | All of these options can be overridden from the command line or in the configuration 72 | file. Further, the ``--pre-summary-space`` option only inserts a space before the 73 | summary when the summary begins with a double quote ("). For example: 74 | 75 | ``"""This summary gets no space."""`` becomes ``"""This summary gets no space."""`` 76 | 77 | and 78 | 79 | ``""""This" summary does get a space."""`` becomes ``""" "This" summary does get a space."""`` 80 | 81 | The ``--style`` argument takes a string which is the name of the field list style you 82 | are using. Currently, only ``sphinx`` and ``epytext`` are recognized, but ``numpy`` 83 | and ``google`` are future styles. For the selected style, each line in the field lists 84 | will be wrapped at the ``--wrap-descriptions`` length as well as any portion of the 85 | elaborate description preceding the parameter list. Field lists that don't follow the 86 | passed style will cause the entire elaborate description to be ignored and remain 87 | unwrapped. 88 | 89 | A Note on reST Header Adornments Regex 90 | -------------------------------------- 91 | ``docformatter-1.7.2`` added a new option ``--rest-section-adorns``. This allows for 92 | setting the characters used as overline and underline adornments for reST section 93 | headers. Per the `ReStructuredText Markup Specification `_, 94 | the following are all valid adornment characters, 95 | 96 | .. code-block:: 97 | 98 | ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ~ 99 | 100 | Thus, the default regular expression ``[!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~]{4,}`` 101 | looks for any of these characters appearing at least four times in a row. Note that the 102 | list of valid adornment characters includes the double quote (") and the greater-than 103 | sign (>). Four repetitions was selected because: 104 | 105 | * Docstrings open and close with triple double quotes. 106 | * Doctests begin with >>>. 107 | * It would be rare for a section header to consist of fewer than four characters. 108 | 109 | The user can override this default list of characters by passing a regex from the 110 | command line or setting the ``rest-section-adorns`` option in the configuration file. 111 | It may be usefule to set this regex to only include the subset of characters you 112 | actually use in your docstrings. For example, to only recognize the recommended list 113 | in the ReStructuredText Markup Specification, the following regular expression would 114 | be used: 115 | 116 | .. code-block:: 117 | 118 | [=-`:.'"~^_*+#]{4,} 119 | -------------------------------------------------------------------------------- /tests/patterns/test_header_patterns.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | # type: ignore 3 | # 4 | # tests.patterns.test_header_patterns.py is part of the docformatter project 5 | # 6 | # Copyright (C) 2012-2023 Steven Myint 7 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining 10 | # a copy of this software and associated documentation files (the 11 | # "Software"), to deal in the Software without restriction, including 12 | # without limitation the rights to use, copy, modify, merge, publish, 13 | # distribute, sublicense, and/or sell copies of the Software, and to 14 | # permit persons to whom the Software is furnished to do so, subject to 15 | # the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be 18 | # included in all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 24 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 25 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 26 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | # SOFTWARE. 28 | """Module for testing the field list pattern detection functions.""" 29 | 30 | # Standard Library Imports 31 | import contextlib 32 | import sys 33 | 34 | with contextlib.suppress(ImportError): 35 | if sys.version_info >= (3, 11): 36 | # Standard Library Imports 37 | import tomllib 38 | else: 39 | # Third Party Imports 40 | import tomli as tomllib 41 | 42 | # Third Party Imports 43 | import pytest 44 | 45 | # docformatter Package Imports 46 | from docformatter.patterns import ( 47 | is_alembic_header, 48 | is_numpy_section_header, 49 | is_rest_section_header, 50 | ) 51 | 52 | with open("tests/_data/string_files/header_patterns.toml", "rb") as f: 53 | TEST_STRINGS = tomllib.load(f) 54 | 55 | 56 | @pytest.mark.unit 57 | @pytest.mark.parametrize( 58 | "test_key, patternizer", 59 | [ 60 | ("is_alembic_header", is_alembic_header), 61 | ("is_not_alembic_header_epytext", is_alembic_header), 62 | ("is_not_alembic_header_numpy", is_alembic_header), 63 | ("is_not_alembic_header_google", is_alembic_header), 64 | ("is_numpy_section_header_parameters", is_numpy_section_header), 65 | ("is_numpy_section_header_returns", is_numpy_section_header), 66 | ("is_numpy_section_header_yields", is_numpy_section_header), 67 | ("is_numpy_section_header_raises", is_numpy_section_header), 68 | ("is_numpy_section_header_receives", is_numpy_section_header), 69 | ("is_numpy_section_header_other_parameters", is_numpy_section_header), 70 | ("is_numpy_section_header_warns", is_numpy_section_header), 71 | ("is_numpy_section_header_warnings", is_numpy_section_header), 72 | ("is_numpy_section_header_see_also", is_numpy_section_header), 73 | ("is_numpy_section_header_examples", is_numpy_section_header), 74 | ("is_numpy_section_header_notes", is_numpy_section_header), 75 | ("is_not_numpy_section_header", is_numpy_section_header), 76 | ("is_not_numpy_section_header_wrong_dashes", is_numpy_section_header), 77 | ("is_rest_section_header_pound", is_rest_section_header), 78 | ("is_rest_section_header_star", is_rest_section_header), 79 | ("is_rest_section_header_equal", is_rest_section_header), 80 | ("is_rest_section_header_dash", is_rest_section_header), 81 | ("is_rest_section_header_circumflex", is_rest_section_header), 82 | ("is_rest_section_header_single_quote", is_rest_section_header), 83 | ("is_rest_section_header_double_quote", is_rest_section_header), 84 | ("is_rest_section_header_plus", is_rest_section_header), 85 | ("is_rest_section_header_underscore", is_rest_section_header), 86 | ("is_rest_section_header_tilde", is_rest_section_header), 87 | ("is_rest_section_header_backtick", is_rest_section_header), 88 | ("is_rest_section_header_period", is_rest_section_header), 89 | ("is_rest_section_header_colon", is_rest_section_header), 90 | ("is_not_rest_section_header_unknown_adornments", is_rest_section_header), 91 | ], 92 | ) 93 | def test_is_header(test_key, patternizer): 94 | source = TEST_STRINGS[test_key]["instring"] 95 | expected = TEST_STRINGS[test_key]["expected"] 96 | 97 | result = patternizer(source) 98 | if result: 99 | assert ( 100 | result.group(0) == expected 101 | ), f"\nFailed {test_key}\nExpected {expected}\nGot {result.group(0)}" 102 | else: 103 | result = "None" if result is None else result 104 | assert ( 105 | result == expected 106 | ), f"\nFailed {test_key}\nExpected {expected}\nGot {result}" 107 | -------------------------------------------------------------------------------- /tests/_data/string_files/classify_functions.toml: -------------------------------------------------------------------------------- 1 | [is_module_docstring] 2 | instring = ''' 3 | """This is a module docstring.""" 4 | ''' 5 | expected = true 6 | 7 | [is_class_docstring] 8 | instring = ''' 9 | class A: 10 | """Class-level docstring.""" 11 | ''' 12 | expected = true 13 | 14 | [is_method_docstring] 15 | instring = ''' 16 | def foo(): 17 | """Method docstring.""" 18 | ''' 19 | expected = true 20 | 21 | [is_function_docstring] 22 | instring = ''' 23 | def foo(): 24 | """Function docstring.""" 25 | ''' 26 | expected = true 27 | 28 | [is_attribute_docstring] 29 | instring = ''' 30 | class A: 31 | x = 1 32 | """Attribute docstring.""" 33 | ''' 34 | expected = true 35 | 36 | [is_not_attribute_docstring] 37 | instring = ''' 38 | #!/usr/bin/env python 39 | 40 | import os 41 | from typing import Iterator 42 | 43 | """Don't remove this comment, it's cool.""" 44 | IMPORTANT_CONSTANT = "potato" 45 | ''' 46 | expected = false 47 | 48 | [is_code_line] 49 | type=1 50 | string='x = 42\n' 51 | start='(3,10)' 52 | end='(3,20)' 53 | line='x = 42\n' 54 | expected = true 55 | 56 | [is_closing_quotes] 57 | prev_type=5 58 | prev_string=' ' 59 | prev_start='(3,10)' 60 | prev_end='(3,40)' 61 | prev_line=' """\n' 62 | type=4 63 | string='"""\n' 64 | start='(3,10)' 65 | end='(3,40)' 66 | line=' """\n' 67 | expected = true 68 | 69 | [is_definition_line_class] 70 | type = 1 71 | string = 'class foo():\n' 72 | start = "(3,10)" 73 | end = "(3,25)" 74 | line = 'class foo():\n' 75 | expected = true 76 | 77 | [is_definition_line_function] 78 | type = 1 79 | string = 'def foo():\n' 80 | start = "(3,10)" 81 | end = "(3,25)" 82 | line = 'def foo():\n' 83 | expected = true 84 | 85 | [is_definition_line_async_function] 86 | type = 1 87 | string = 'async def foo():\n' 88 | start = "(3,10)" 89 | end = "(3,25)" 90 | line = 'async def foo():\n' 91 | expected = true 92 | 93 | [is_not_definition_line_function] 94 | type = 1 95 | string = 'definitely\n' 96 | start = "(3,10)" 97 | end = "(3,20)" 98 | line = 'definitely\n' 99 | expected = false 100 | 101 | [is_nested_definition_line_class] 102 | type = 1 103 | string = ' class foo():\n' 104 | start = "(3,10)" 105 | end = "(3,25)" 106 | line = ' class foo():\n' 107 | expected = true 108 | 109 | [is_nested_definition_line_function] 110 | type = 1 111 | string = ' def foo():\n' 112 | start = "(3,10)" 113 | end = "(3,25)" 114 | line = ' def foo():\n' 115 | expected = true 116 | 117 | [is_nested_definition_line_async_function] 118 | type = 1 119 | string = ' async def foo():\n' 120 | start = "(3,10)" 121 | end = "(3,25)" 122 | line = ' async def foo():\n' 123 | expected = true 124 | 125 | [is_not_nested_definition_line_function] 126 | type = 1 127 | string = ' definitely\n' 128 | start = "(3,10)" 129 | end = "(3,20)" 130 | line = ' definitely\n' 131 | expected = false 132 | 133 | [is_inline_comment] 134 | type = 64 135 | string = "# This is an inline comment" 136 | start = "(3,10)" 137 | end = "(3,30)" 138 | line = '"""This is a docstring with an inline comment""" # This is an inline comment' 139 | expected = true 140 | 141 | [is_string_variable] 142 | prev_type = 55 143 | prev_string = 'x = """This is a string variable"""' 144 | prev_start = "(3,10)" 145 | prev_end = "(3,40)" 146 | prev_line = 'x = """This is a string variable"""' 147 | type = 3 148 | string = 'x = """This is a string variable"""' 149 | start = "(3,10)" 150 | end = "(3,40)" 151 | line = 'x = """This is a string variable"""' 152 | expected = true 153 | 154 | [is_newline_continuation] 155 | prev_type = 3 156 | prev_string = '"""\n' 157 | prev_start = "(3,10)" 158 | prev_end = "(3,40)" 159 | prev_line = '"""\n' 160 | type = 4 161 | string = '\n' 162 | start = "(3,10)" 163 | end = "(3,40)" 164 | line = '"""\n' 165 | expected = true 166 | 167 | [is_line_following_indent] 168 | prev_type = 5 169 | prev_string = ' ' 170 | prev_start = "(3,10)" 171 | prev_end = "(3,40)" 172 | prev_line = ' """\n' 173 | type = 4 174 | string = '"""\n' 175 | start = "(3,10)" 176 | end = "(3,40)" 177 | line = ' """\n' 178 | expected = true 179 | 180 | [is_f_string] 181 | prev_type = 61 182 | prev_string = 'f"""' 183 | prev_start = "(3,4)" 184 | prev_end = "(3,7)" 185 | prev_line = 'f"""This is an f-string with a {variable} inside."""' 186 | type = 62 187 | string = 'This is an f-string with a {variable} inside."""' 188 | start = "(3,7)" 189 | end = "(3,55)" 190 | line = 'f"""This is an f-string with a {variable} inside."""' 191 | expected = true 192 | expected313 = false 193 | 194 | [find_module_docstring] 195 | instring = '''"""Module docstring.""" 196 | import os''' 197 | 198 | [find_class_docstring] 199 | instring = ''' 200 | class A: 201 | """Class docstring.""" 202 | pass 203 | ''' 204 | 205 | [find_function_docstring] 206 | instring = ''' 207 | def foo(): 208 | """Function docstring.""" 209 | pass 210 | ''' 211 | 212 | [find_function_docstring_with_decorator] 213 | instring = ''' 214 | @decorator 215 | def f(): 216 | """Docstring.""" 217 | pass 218 | ''' 219 | 220 | [find_attribute_docstring] 221 | instring = ''' 222 | x = 1 223 | """Doc for x.""" 224 | ''' 225 | 226 | [find_multiple_docstrings] 227 | instring = ''' 228 | """Module.""" 229 | 230 | class A: 231 | """Class.""" 232 | 233 | def f(self): 234 | """Method.""" 235 | pass 236 | ''' 237 | -------------------------------------------------------------------------------- /tests/_data/string_files/url_wrappers.toml: -------------------------------------------------------------------------------- 1 | [elaborate_inline_url] 2 | instring = """ 3 | Here is an elaborate description containing a link. 4 | `Area Under the Receiver Operating Characteristic Curve (ROC AUC) 5 | `_.""" 6 | expected = [ 7 | [" Here is an elaborate description containing a link. `Area Under the", 8 | " Receiver Operating Characteristic Curve (ROC AUC)", 9 | " `_"], 10 | 226] 11 | 12 | [short_inline_url] 13 | instring = "See `the link `_ for more details." 14 | expected = [[], 0] 15 | 16 | [long_inline_url] 17 | instring = """ 18 | A larger description that starts here. https://github.com/apache/kafka/blob/2.5/clients/src/main/java/org/apache/kafka/common/requests/DescribeConfigsResponse.java 19 | A larger description that ends here. 20 | """ 21 | expected = [ 22 | [" A larger description that starts here.", 23 | " https://github.com/apache/kafka/blob/2.5/clients/src/main/java/org/apache/kafka/common/requests/DescribeConfigsResponse.java"], 24 | 168] 25 | 26 | [simple_url] 27 | instring = "See http://www.reliqual.com/wiki/how_to_use_ramstk/verification_and_validation_module/index.html for additional information." 28 | expected = [ 29 | [" See", 30 | " http://www.reliqual.com/wiki/how_to_use_ramstk/verification_and_validation_module/index.html for"], 31 | 100] 32 | 33 | [short_url] 34 | instring = "See http://www.reliaqual.com for examples." 35 | expected = [[], 0] 36 | 37 | [inline_url_retain_space] 38 | instring = """ 39 | A larger description that starts here. 40 | https://github.com/apache/kafka/blob/2.5/clients/src/main/java/org/apache/kafka/common/requests/DescribeConfigsResponse.java 41 | A larger description that ends here.""" 42 | expected = [ 43 | [" A larger description that starts here.", 44 | " https://github.com/apache/kafka/blob/2.5/clients/src/main/java/org/apache/kafka/common/requests/DescribeConfigsResponse.java"], 45 | 171] 46 | 47 | [keep_inline_url_together] 48 | instring = """ 49 | See the list of `custom types provided by Click 50 | `_.""" 51 | expected = [ 52 | [" See the list of", 53 | " `custom types provided by Click `_."], 54 | 133] 55 | 56 | [inline_url_two_paragraphs] 57 | instring = """ 58 | User configuration is `merged to the context default_map as Click does 59 | `_. 60 | 61 | This allow user's config to only overrides defaults. Values sets from direct 62 | command line parameters, environment variables or interactive prompts, takes 63 | precedence over any values from the config file.""" 64 | expected = [ 65 | [" User configuration is", 66 | " `merged to the context default_map as Click does `_"], 67 | 153] 68 | 69 | [url_no_delete_words] 70 | instring = """ 71 | This will normally be used with https://aaaaaaaa.bbb.ccccccccc.com/xxxxx/xxx_xxxxxxxxxxx to generate the xxx""" 72 | expected = [ 73 | [" This will normally be used with", 74 | " https://aaaaaaaa.bbb.ccccccccc.com/xxxxx/xxx_xxxxxxxxxx"], 75 | 92] 76 | 77 | [no_newline_after_url] 78 | instring = """ 79 | Generated by 'django-admin startproject' using Django 4.1.1. 80 | 81 | For more information on this file, see 82 | https://docs.djangoproject.com/en/4.1/topics/settings/ 83 | 84 | For the full list of settings and their values, see 85 | https://docs.djangoproject.com/en/4.1/ref/settings/""" 86 | expected = [ 87 | [" Generated by 'django-admin startproject' using Django 4.1.1.", 88 | "", 89 | " For more information on this file, see", 90 | " https://docs.djangoproject.com/en/4.1/topics/settings/", 91 | "", 92 | " For the full list of settings and their values, see", 93 | " https://docs.djangoproject.com/en/4.1/ref/settings/"], 94 | 280] 95 | 96 | [only_url_in_description] 97 | instring = " https://example.com/this-is-just-a-long-url/designed-to-trigger/the-wrapping-of-the-description" 98 | expected = [ 99 | [" https://example.com/this-is-just-a-long-url/designed-to-trigger/the-wrapping-of-the-description"], 100 | 99] 101 | 102 | [no_indent_string_on_newline] 103 | instring = """ 104 | Here is a link to the github issue 105 | https://github.com/PyCQA/docformatter/issues/199 106 | 107 | This is a long description.""" 108 | expected = [ 109 | [" Here is a link to the github issue", 110 | " https://github.com/PyCQA/docformatter/issues/19"], 111 | 91] 112 | 113 | [short_anonymous_url] 114 | instring = """ 115 | This graphics format generates terminal escape codes that transfer 116 | PNG data to a TTY using the `kitty graphics protocol`__. 117 | 118 | __ https://sw.kovidgoyal.net/kitty/graphics-protocol/""" 119 | expected = [ 120 | [" This graphics format generates terminal escape codes that transfer", 121 | " PNG data to a TTY using the `kitty graphics protocol`__.", 122 | "", 123 | " __ https://sw.kovidgoyal.net/kitty/graphics-protocol/"], 124 | 190] 125 | 126 | [quoted_url] 127 | instring = """ 128 | It's not a perfect guess, but it's better than having "https://example.com". 129 | 130 | :param bundle: The bundle identifier. 131 | :param app_name: The app name. 132 | :returns: The candidate project URL""" 133 | expected = [ 134 | [" It's not a perfect guess, but it's better than having", 135 | ' "https://example.com"'], 136 | 80] 137 | -------------------------------------------------------------------------------- /tests/test_encoding_functions.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | # type: ignore 3 | # 4 | # tests.test.encoding_functions.py is part of the docformatter project 5 | # 6 | # Copyright (C) 2012-2023 Steven Myint 7 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining 10 | # a copy of this software and associated documentation files (the 11 | # "Software"), to deal in the Software without restriction, including 12 | # without limitation the rights to use, copy, modify, merge, publish, 13 | # distribute, sublicense, and/or sell copies of the Software, and to 14 | # permit persons to whom the Software is furnished to do so, subject to 15 | # the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be 18 | # included in all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 24 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 25 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 26 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | # SOFTWARE. 28 | """Module for testing functions used to determine file encodings.""" 29 | 30 | # Standard Library Imports 31 | import contextlib 32 | import io 33 | import sys 34 | 35 | with contextlib.suppress(ImportError): 36 | if sys.version_info >= (3, 11): 37 | # Standard Library Imports 38 | import tomllib 39 | else: 40 | # Third Party Imports 41 | import tomli as tomllib 42 | 43 | # Third Party Imports 44 | import pytest 45 | 46 | # docformatter Package Imports 47 | from docformatter import Encoder 48 | 49 | with open("tests/_data/string_files/encoding_functions.toml", "rb") as f: 50 | TEST_STRINGS = tomllib.load(f) 51 | 52 | 53 | @pytest.mark.unit 54 | @pytest.mark.parametrize( 55 | "test_key", 56 | [ 57 | "find_newline_only_cr", 58 | "find_newline_only_lf", 59 | "find_newline_only_crlf", 60 | "find_newline_cr1_and_lf2", 61 | "find_newline_cr1_and_crlf2", 62 | "find_newline_should_default_to_lf_empty", 63 | "find_newline_should_default_to_lf_blank", 64 | "find_dominant_newline", 65 | ], 66 | ) 67 | def test_do_find_newline(test_key): 68 | uut = Encoder() 69 | 70 | source = TEST_STRINGS[test_key]["instring"] 71 | expected = TEST_STRINGS[test_key]["expected"] 72 | 73 | result = uut.do_find_newline(source) 74 | assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" 75 | 76 | 77 | @pytest.mark.usefixtures("temporary_file") 78 | class TestDoOpenWithEncoding: 79 | """Class for testing the do_open_with_encoding function.""" 80 | 81 | @pytest.mark.unit 82 | @pytest.mark.parametrize("contents", ["# -*- coding: utf-8 -*-\n"]) 83 | def test_do_open_with_utf_8_encoding(self, temporary_file, contents): 84 | """Return TextIOWrapper object when opening file with encoding.""" 85 | uut = Encoder() 86 | 87 | assert isinstance( 88 | uut.do_open_with_encoding(temporary_file), 89 | io.TextIOWrapper, 90 | ) 91 | 92 | @pytest.mark.unit 93 | @pytest.mark.parametrize("contents", ["# -*- coding: utf-8 -*-\n"]) 94 | def test_do_open_with_wrong_encoding(self, temporary_file, contents): 95 | """Raise LookupError when passed unknown encoding.""" 96 | uut = Encoder() 97 | uut.encoding = "cr1252" 98 | 99 | with pytest.raises(LookupError): 100 | uut.do_open_with_encoding(temporary_file) 101 | 102 | 103 | @pytest.mark.usefixtures("temporary_file") 104 | class TestDoDetectEncoding: 105 | """Class for testing the detect_encoding() function.""" 106 | 107 | @pytest.mark.integration 108 | @pytest.mark.parametrize("contents", ["# -*- coding: utf-8 -*-\n"]) 109 | def test_do_detect_encoding_with_explicit_utf_8(self, temporary_file, contents): 110 | """Return utf-8 when explicitly set in the file.""" 111 | uut = Encoder() 112 | uut.do_detect_encoding(temporary_file) 113 | 114 | assert "utf_8" == uut.encoding 115 | 116 | @pytest.mark.integration 117 | @pytest.mark.parametrize("contents", ["# Wow! docformatter is super-cool.\n"]) 118 | def test_do_detect_encoding_with_non_explicit_setting( 119 | self, temporary_file, contents 120 | ): 121 | """Return default system encoding when encoding not explicitly set.""" 122 | uut = Encoder() 123 | uut.do_detect_encoding(temporary_file) 124 | 125 | assert "ascii" == uut.encoding 126 | 127 | @pytest.mark.integration 128 | @pytest.mark.parametrize("contents", ["# -*- coding: blah -*-"]) 129 | def test_do_detect_encoding_with_bad_encoding(self, temporary_file, contents): 130 | """Default to latin-1 when unknown encoding detected.""" 131 | uut = Encoder() 132 | uut.do_detect_encoding(temporary_file) 133 | 134 | assert "ascii" == uut.encoding 135 | 136 | @pytest.mark.integration 137 | @pytest.mark.parametrize("contents", [""]) 138 | def test_do_detect_encoding_with_undetectable_encoding(self, temporary_file): 139 | """Default to latin-1 when encoding detection fails.""" 140 | uut = Encoder() 141 | 142 | # Simulate a file with undetectable encoding 143 | with open(temporary_file, "wb") as file: 144 | # Binary content unlikely to have a detectable encoding 145 | file.write(b"\xff\xfe\xfd\xfc\x00\x00\x00\x00") 146 | 147 | uut.do_detect_encoding(temporary_file) 148 | 149 | assert uut.encoding == uut.DEFAULT_ENCODING 150 | -------------------------------------------------------------------------------- /tests/formatter/test_do_format_code.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | # type: ignore 3 | # 4 | # tests.formatter.test_do_format_code.py is part of the docformatter project 5 | # 6 | # Copyright (C) 2012-2023 Steven Myint 7 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining 10 | # a copy of this software and associated documentation files (the 11 | # "Software"), to deal in the Software without restriction, including 12 | # without limitation the rights to use, copy, modify, merge, publish, 13 | # distribute, sublicense, and/or sell copies of the Software, and to 14 | # permit persons to whom the Software is furnished to do so, subject to 15 | # the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be 18 | # included in all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 24 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 25 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 26 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | # SOFTWARE. 28 | """Module for testing the Formattor _do_format_code method.""" 29 | 30 | # Standard Library Imports 31 | import contextlib 32 | import sys 33 | 34 | with contextlib.suppress(ImportError): 35 | if sys.version_info >= (3, 11): 36 | # Standard Library Imports 37 | import tomllib 38 | else: 39 | # Third Party Imports 40 | import tomli as tomllib 41 | 42 | # Third Party Imports 43 | import pytest 44 | 45 | # docformatter Package Imports 46 | from docformatter import Formatter 47 | 48 | NO_ARGS = [""] 49 | 50 | with open("tests/_data/string_files/do_format_code.toml", "rb") as f: 51 | TEST_STRINGS = tomllib.load(f) 52 | 53 | 54 | @pytest.mark.integration 55 | @pytest.mark.order(7) 56 | @pytest.mark.parametrize( 57 | "test_key, args", 58 | [ 59 | ("one_line", NO_ARGS), 60 | ("module_docstring", NO_ARGS), 61 | ("newline_module_variable", NO_ARGS), 62 | ("class_docstring", NO_ARGS), 63 | ("newline_class_variable", NO_ARGS), 64 | ("newline_outside_docstring", NO_ARGS), 65 | pytest.param( 66 | "preserve_line_ending", 67 | NO_ARGS, 68 | marks=pytest.mark.skipif( 69 | sys.platform != "win32", reason="Not running on Windows" 70 | ), 71 | ), 72 | ("non_docstring", NO_ARGS), 73 | ("tabbed_indentation", NO_ARGS), 74 | ("mixed_indentation", NO_ARGS), 75 | ("escaped_newlines", NO_ARGS), 76 | ("code_comments", NO_ARGS), 77 | ("inline_comment", NO_ARGS), 78 | ("raw_lowercase", NO_ARGS), 79 | ("raw_uppercase", NO_ARGS), 80 | ("raw_lowercase_single", NO_ARGS), 81 | ("raw_uppercase_single", NO_ARGS), 82 | ("unicode_lowercase", NO_ARGS), 83 | ("unicode_uppercase", NO_ARGS), 84 | ("unicode_lowercase_single", NO_ARGS), 85 | ("unicode_uppercase_single", NO_ARGS), 86 | ("nested_triple", NO_ARGS), 87 | ("multiple_sentences", NO_ARGS), 88 | ("multiple_sentences_same_line", NO_ARGS), 89 | ("multiline_summary", NO_ARGS), 90 | ("empty_lines", NO_ARGS), 91 | ("class_empty_lines", NO_ARGS), 92 | ("class_empty_lines_2", NO_ARGS), 93 | ("method_empty_lines", NO_ARGS), 94 | ("trailing_whitespace", NO_ARGS), 95 | ("parameter_list", NO_ARGS), 96 | ("single_quote", NO_ARGS), 97 | ("double_quote", NO_ARGS), 98 | ("nested_triple_quote", NO_ARGS), 99 | ("first_line_assignment", NO_ARGS), 100 | ("regular_strings", NO_ARGS), 101 | ("syntax_error", NO_ARGS), 102 | ("slash_r", NO_ARGS), 103 | ("slash_r_slash_n", NO_ARGS), 104 | ("strip_blank_lines", ["--black", ""]), 105 | ("range_miss", ["--range", "1", "1", ""]), 106 | ("range_hit", ["--range", "1", "2", ""]), 107 | ("length_ignore", ["--docstring-length", "1", "1", ""]), 108 | ("class_attribute_wrap", NO_ARGS), 109 | ("issue_51", NO_ARGS), 110 | ("issue_51_2", NO_ARGS), 111 | ( 112 | "issue_79", 113 | NO_ARGS 114 | + [ 115 | "--wrap-summaries", 116 | "100", 117 | "--wrap-descriptions", 118 | "100", 119 | ], 120 | ), 121 | ("issue_97", NO_ARGS), 122 | ("issue_97_2", NO_ARGS), 123 | ("issue_130", NO_ARGS), 124 | ("issue_139", NO_ARGS), 125 | ("issue_139_2", NO_ARGS), 126 | ("issue_156", NO_ARGS), 127 | ("issue_156_2", NO_ARGS), 128 | ("issue_156_173", NO_ARGS), 129 | ("issue_157_7", ["--wrap-descriptions", "88", ""]), 130 | ("issue_157_8", ["--wrap-descriptions", "88", ""]), 131 | ("issue_157_9", ["--wrap-descriptions", "88", ""]), 132 | ("issue_157_10", ["--wrap-descriptions", "88", ""]), 133 | ("issue_176", NO_ARGS), 134 | ("issue_176_black", NO_ARGS), 135 | ("issue_187", NO_ARGS), 136 | ("issue_203", NO_ARGS), 137 | ("issue_243", NO_ARGS), 138 | ], 139 | ) 140 | def test_do_format_code(test_key, test_args, args): 141 | uut = Formatter( 142 | test_args, 143 | sys.stderr, 144 | sys.stdin, 145 | sys.stdout, 146 | ) 147 | 148 | source = TEST_STRINGS[test_key]["source"] 149 | expected = TEST_STRINGS[test_key]["expected"] 150 | 151 | result = uut._do_format_code(source) 152 | assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" 153 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "docformatter" 3 | version = "1.7.7" 4 | description = "Formats docstrings to follow PEP 257" 5 | authors = ["Steven Myint"] 6 | maintainers = [ 7 | "Doyle Rowland ", 8 | ] 9 | license = "Expat" 10 | readme = "README.rst" 11 | homepage = "https://github.com/PyCQA/docformatter" 12 | repository = "https://github.com/PyCQA/docformatter" 13 | documentation = "https://docformatter.readthedocs.io/en/latest/" 14 | keywords = [ 15 | "PEP 257", "pep257", "style", "formatter", "docstrings", 16 | ] 17 | classifiers=[ 18 | 'Intended Audience :: Developers', 19 | 'Environment :: Console', 20 | 'Programming Language :: Python :: 3', 21 | 'Programming Language :: Python :: 3.9', 22 | 'Programming Language :: Python :: 3.10', 23 | 'Programming Language :: Python :: 3.11', 24 | 'Programming Language :: Python :: 3.12', 25 | 'Programming Language :: Python :: Implementation', 26 | 'Programming Language :: Python :: Implementation :: PyPy', 27 | 'Programming Language :: Python :: Implementation :: CPython', 28 | 'License :: OSI Approved :: MIT License', 29 | ] 30 | packages = [{include = "docformatter", from = "src"}] 31 | 32 | [tool.poetry.dependencies] 33 | python = "^3.9" 34 | charset_normalizer = "^3.0.0" 35 | tomli = {version = "^2.0.0", python = "<3.11", optional = true} 36 | 37 | [tool.poetry.group.dev.dependencies] 38 | Sphinx = "^6.0.0" 39 | tox = "^4.0.0" 40 | twine = "^6.1.0" 41 | 42 | [tool.poetry.group.testing.dependencies] 43 | coverage = {extras = ["toml"], version = "^7.5.0"} 44 | mock = "^5.2.0" 45 | pytest = "^8.4.0" 46 | pytest-cov = "^6.2.0" 47 | pytest-order = "^1.3.0" 48 | 49 | [tool.poetry.group.linting.dependencies] 50 | autopep8 = "^2.0.0" 51 | black = ">=25" 52 | isort = "^6.0.0" 53 | mypy = "^1.17.0" 54 | pycodestyle = "^2.8.0" 55 | pydocstyle = "^6.1.1" 56 | pylint = "^3.3.0" 57 | rstcheck = "^6.1.0" 58 | ruff = "^0.12.0" 59 | 60 | [tool.poetry.extras] 61 | tomli = ["tomli"] 62 | 63 | [tool.poetry.scripts] 64 | docformatter = "docformatter.__main__:main" 65 | 66 | [build-system] 67 | requires = ["poetry-core>=1.0.0"] 68 | build-backend = "poetry.core.masonry.api" 69 | 70 | [tool.pylint.master] 71 | ignore-paths = [ 72 | "tests*", 73 | ] 74 | 75 | [tool.pylint.messages_control] 76 | disable = [ 77 | "fixme", 78 | "import-outside-toplevel", 79 | "inconsistent-return-statements", 80 | "invalid-name", 81 | "no-else-return", 82 | "no-member", 83 | "too-few-public-methods", 84 | "too-many-arguments", 85 | "too-many-boolean-expressions", 86 | "too-many-locals", 87 | "too-many-return-statements", 88 | "useless-object-inheritance", 89 | ] 90 | 91 | [tool.docformatter] 92 | black = true 93 | non-strict = false 94 | non-cap = [ 95 | "docformatter", 96 | ] 97 | 98 | [tool.mypy] 99 | allow_subclassing_any = true 100 | follow_imports = "skip" 101 | implicit_reexport = true 102 | ignore_missing_imports = true 103 | 104 | [tool.pydocstyle] 105 | convention = "pep257" 106 | 107 | [tool.pytest.ini_options] 108 | markers = [ 109 | "unit: mark the test as a unit test.", 110 | "integration: mark the test as an integration test.", 111 | "system: mark the test as a system test.", 112 | ] 113 | 114 | [tool.coverage.run] 115 | branch = true 116 | cover_pylib = false 117 | omit = [ 118 | '*/site-packages/*', 119 | '*/*pypy/*', 120 | '*/tests/*', 121 | '__init__.py', 122 | 'setup.py', 123 | ] 124 | relative_files = true 125 | 126 | [tool.coverage.report] 127 | omit = [ 128 | '*/site-packages/*', 129 | '*/*pypy/*', 130 | '*/tests/*', 131 | '__init__.py', 132 | 'setup.py', 133 | ] 134 | exclude_lines = [ 135 | 'pragma: no cover', 136 | 'import', 137 | ] 138 | show_missing = true 139 | 140 | [tool.coverage.xml] 141 | output = 'coverage.xml' 142 | 143 | [tool.black] 144 | line-length = 88 145 | target-version = ['py39', 'py310', 'py311', 'py312', 'py313'] 146 | include = '\.pyi?$' 147 | exclude = ''' 148 | /( 149 | \.eggs 150 | | \.git 151 | | \.hg 152 | | \.mypy_cache 153 | | \.tox 154 | | \.venv 155 | | _build 156 | | buck-out 157 | | build 158 | | dist 159 | )/ 160 | ''' 161 | 162 | [tool.isort] 163 | known_first_party = 'docformatter' 164 | known_third_party = ['toml'] 165 | import_heading_firstparty = 'docformatter Package Imports' 166 | import_heading_localfolder = 'docformatter Local Imports' 167 | import_heading_stdlib = 'Standard Library Imports' 168 | import_heading_thirdparty = 'Third Party Imports' 169 | multi_line_output = 3 170 | include_trailing_comma = true 171 | force_grid_wrap = 0 172 | use_parentheses = true 173 | ensure_newline_before_comments = true 174 | line_length = 88 175 | 176 | [tool.rstcheck] 177 | report = "warning" 178 | ignore_directives = [ 179 | "automodule", 180 | "literalinclude", 181 | "tabularcolumns", 182 | "toctree", 183 | ] 184 | ignore_messages = [ 185 | "Possible title underline" 186 | ] 187 | ignore_roles = [ 188 | "numref", 189 | "ref", 190 | ] 191 | 192 | [tool.ruff] 193 | exclude = [ 194 | ".bzr", 195 | ".direnv", 196 | ".eggs", 197 | ".git", 198 | ".git-rewrite", 199 | ".hg", 200 | ".ipynb_checkpoints", 201 | ".mypy_cache", 202 | ".nox", 203 | ".pants.d", 204 | ".pyenv", 205 | ".pytest_cache", 206 | ".pytype", 207 | ".ruff_cache", 208 | ".svn", 209 | ".tox", 210 | ".venv", 211 | ".vscode", 212 | "__pypackages__", 213 | "_build", 214 | "buck-out", 215 | "build", 216 | "dist", 217 | "node_modules", 218 | "site-packages", 219 | "venv", 220 | "tests/", 221 | ] 222 | line-length = 88 223 | indent-width = 4 224 | target-version = "py39" 225 | 226 | [tool.ruff.lint] 227 | select = ["E", "F", "PL"] 228 | ignore = [] 229 | 230 | [tool.ruff.format] 231 | quote-style = "double" 232 | indent-style = "space" 233 | skip-magic-trailing-comma = false 234 | line-ending = "auto" 235 | -------------------------------------------------------------------------------- /src/docformatter/util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # docformatter.util.py is part of the docformatter project 4 | # 5 | # Copyright (C) 2012-2023 Steven Myint 6 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining 9 | # a copy of this software and associated documentation files (the 10 | # "Software"), to deal in the Software without restriction, including 11 | # without limitation the rights to use, copy, modify, merge, publish, 12 | # distribute, sublicense, and/or sell copies of the Software, and to 13 | # permit persons to whom the Software is furnished to do so, subject to 14 | # the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | # SOFTWARE. 27 | """This module provides docformatter utility functions.""" 28 | 29 | 30 | # Standard Library Imports 31 | import os 32 | import re 33 | import sysconfig 34 | from typing import List, Tuple 35 | 36 | unicode = str 37 | 38 | _PYTHON_LIBS = set(sysconfig.get_paths().values()) 39 | 40 | 41 | def find_py_files(sources, recursive, exclude=None): 42 | """Find Python source files. 43 | 44 | Parameters 45 | ---------- 46 | sources : list 47 | Paths to files and/or directories to search. 48 | recursive : bool 49 | Drill down directories if True. 50 | exclude : list 51 | Which directories and files are excluded. 52 | 53 | Returns 54 | ------- 55 | list of files found. 56 | """ 57 | 58 | def is_hidden(name): 59 | """Return True if file 'name' is .hidden.""" 60 | return os.path.basename(os.path.abspath(name)).startswith(".") 61 | 62 | def is_excluded(name, excluded): 63 | """Return True if file 'name' is excluded.""" 64 | return ( 65 | any(re.search(re.escape(str(e)), name, re.IGNORECASE) for e in excluded) 66 | if excluded 67 | else False 68 | ) 69 | 70 | for _name in sorted(sources): 71 | if recursive and os.path.isdir(_name): 72 | for root, dirs, children in os.walk(unicode(_name)): 73 | if is_excluded(root, exclude): 74 | break 75 | 76 | files = sorted( 77 | [ 78 | _file 79 | for _file in children 80 | if not is_hidden(_file) 81 | and not is_excluded(_file, exclude) 82 | and _file.endswith(".py") 83 | ] 84 | ) 85 | for filename in files: 86 | yield os.path.join(root, filename) 87 | elif ( 88 | _name.endswith(".py") 89 | and not is_hidden(_name) 90 | and not is_excluded(_name, exclude) 91 | ): 92 | yield _name 93 | 94 | 95 | def has_correct_length(length_range, start, end): 96 | """Determine if the line under test is within the desired docstring length. 97 | 98 | This function is used with the --docstring-length min_rows max_rows 99 | argument. 100 | 101 | Parameters 102 | ---------- 103 | length_range: list 104 | The file row range passed to the --docstring-length argument. 105 | start: int 106 | The row number where the line under test begins in the source file. 107 | end: int 108 | The row number where the line under tests ends in the source file. 109 | 110 | Returns 111 | ------- 112 | correct_length: bool 113 | True if the docstring has the correct length or length range is None, 114 | otherwise False 115 | """ 116 | if length_range is None: 117 | return True 118 | min_length, max_length = length_range 119 | 120 | docstring_length = end + 1 - start 121 | return min_length <= docstring_length <= max_length 122 | 123 | 124 | def is_in_range(line_range, start, end): 125 | """Determine if ??? is within the desired range. 126 | 127 | This function is used with the --range start_row end_row argument. 128 | 129 | Parameters 130 | ---------- 131 | line_range: list 132 | The line number range passed to the --range argument. 133 | start: int 134 | The row number where the line under test begins in the source file. 135 | end: int 136 | The row number where the line under tests ends in the source file. 137 | 138 | Returns 139 | ------- 140 | in_range : bool 141 | True if in range or range is None, else False 142 | """ 143 | if line_range is None: 144 | return True 145 | return any( 146 | line_range[0] <= line_no <= line_range[1] for line_no in range(start, end + 1) 147 | ) 148 | 149 | 150 | def prefer_field_over_url( 151 | field_idx: List[Tuple[int, int]], 152 | url_idx: List[Tuple[int, int]], 153 | ): 154 | """Remove URL indices that overlap with field list indices. 155 | 156 | Parameters 157 | ---------- 158 | field_idx : list 159 | The list of field list index tuples. 160 | url_idx : list 161 | The list of URL index tuples. 162 | 163 | Returns 164 | ------- 165 | url_idx : list 166 | The url_idx list with any tuples that have indices overlapping with field 167 | list indices removed. 168 | """ 169 | if not field_idx: 170 | return url_idx 171 | 172 | nonoverlapping_urls = [] 173 | 174 | any_param_start = min(e[0] for e in field_idx) 175 | for _key, _value in enumerate(url_idx): 176 | if _value[1] < any_param_start: 177 | nonoverlapping_urls.append(_value) 178 | return nonoverlapping_urls 179 | -------------------------------------------------------------------------------- /src/docformatter/wrappers/fields.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # docformatter.wrappers.fields.py is part of the docformatter project 4 | # 5 | # Copyright (C) 2012-2023 Steven Myint 6 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining 9 | # a copy of this software and associated documentation files (the 10 | # "Software"), to deal in the Software without restriction, including 11 | # without limitation the rights to use, copy, modify, merge, publish, 12 | # distribute, sublicense, and/or sell copies of the Software, and to 13 | # permit persons to whom the Software is furnished to do so, subject to 14 | # the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | # SOFTWARE. 27 | """This module provides docformatter's field list wrapper functions.""" 28 | 29 | 30 | # Standard Library Imports 31 | import re 32 | import textwrap 33 | from typing import List, Tuple 34 | 35 | # docformatter Package Imports 36 | import docformatter.strings as _strings 37 | from docformatter.constants import DEFAULT_INDENT 38 | 39 | 40 | def do_wrap_field_lists( # noqa: PLR0913 41 | text: str, 42 | field_idx: List[Tuple[int, int]], 43 | lines: List[str], 44 | text_idx: int, 45 | indentation: str, 46 | wrap_length: int, 47 | ) -> Tuple[List[str], int]: 48 | """Wrap field lists in the long description. 49 | 50 | Parameters 51 | ---------- 52 | text : str 53 | The long description text. 54 | field_idx : list 55 | The list of field list indices found in the description text. 56 | lines : list 57 | The list of formatted lines in the description that come before the 58 | first parameter list item. 59 | text_idx : int 60 | The index in the description of the end of the last parameter list 61 | item. 62 | indentation : str 63 | The string to use to indent each line in the long description. 64 | wrap_length : int 65 | The line length at which to wrap long lines in the description. 66 | 67 | Returns 68 | ------- 69 | lines, text_idx : tuple 70 | A list of the long description lines and the index in the long 71 | description where the last parameter list item ended. 72 | """ 73 | lines.extend( 74 | _strings.description_to_list( 75 | text[text_idx : field_idx[0][0]], 76 | indentation, 77 | wrap_length, 78 | ) 79 | ) 80 | 81 | for _idx, _field_idx in enumerate(field_idx): 82 | _field_name = text[_field_idx[0] : _field_idx[1]] 83 | _field_body = _do_join_field_body( 84 | text, 85 | field_idx, 86 | _idx, 87 | ) 88 | 89 | if len(f"{_field_name}{_field_body}") <= (wrap_length - len(indentation)): 90 | _field = f"{_field_name}{_field_body}" 91 | lines.append(f"{indentation}{_field}") 92 | else: 93 | lines.extend( 94 | _do_wrap_field(_field_name, _field_body, indentation, wrap_length) 95 | ) 96 | 97 | text_idx = _field_idx[1] 98 | 99 | return lines, text_idx 100 | 101 | 102 | def _do_join_field_body(text: str, field_idx: list[tuple[int, int]], idx: int) -> str: 103 | """Join the filed body lines into a single line that can be wrapped. 104 | 105 | Parameters 106 | ---------- 107 | text : str 108 | The docstring long description text that contains field lists. 109 | field_idx : list 110 | The list of tuples containing the found field list start and end position. 111 | idx : int 112 | The index of the tuple in the field_idx list to extract the field body. 113 | 114 | Returns 115 | ------- 116 | _field_body : str 117 | The field body collapsed into a single line. 118 | """ 119 | try: 120 | _field_body = text[field_idx[idx][1] : field_idx[idx + 1][0]].strip() 121 | except IndexError: 122 | _field_body = text[field_idx[idx][1] :].strip() 123 | 124 | _field_body = " ".join( 125 | [_line.strip() for _line in _field_body.splitlines()] 126 | ).strip() 127 | 128 | # Add a space before the field body unless the field body is a link. 129 | if not _field_body.startswith("`") and _field_body: 130 | _field_body = f" {_field_body}" 131 | 132 | # Is there a blank line between field lists? Keep it if so. 133 | if text[field_idx[idx][1] : field_idx[idx][1] + 2] == "\n\n": 134 | _field_body = "\n" 135 | 136 | return _field_body 137 | 138 | 139 | def _do_wrap_field(field_name, field_body, indentation, wrap_length): 140 | """Wrap complete field at wrap_length characters. 141 | 142 | Parameters 143 | ---------- 144 | field_name : str 145 | The name text of the field. 146 | field_body : str 147 | The body text of the field. 148 | indentation : str 149 | The string to use for indentation of the first line in the field. 150 | wrap_length : int 151 | The number of characters at which to wrap the field. 152 | 153 | Returns 154 | ------- 155 | _wrapped_field : str 156 | The field wrapped at wrap_length characters. 157 | """ 158 | if len(indentation) > DEFAULT_INDENT: 159 | _subsequent = indentation + int(0.5 * len(indentation)) * " " 160 | else: 161 | _subsequent = 2 * indentation 162 | 163 | _wrapped_field = textwrap.wrap( 164 | textwrap.dedent(f"{field_name}{field_body}"), 165 | width=wrap_length, 166 | initial_indent=indentation, 167 | subsequent_indent=_subsequent, 168 | ) 169 | 170 | for _idx, _field in enumerate(_wrapped_field): 171 | _indent = indentation if _idx == 0 else _subsequent 172 | _wrapped_field[_idx] = f"{_indent}{re.sub(' +', ' ', _field.strip())}" 173 | 174 | return _wrapped_field 175 | -------------------------------------------------------------------------------- /.github/workflows/do-release.yml: -------------------------------------------------------------------------------- 1 | # This workflow runs when a pull request is closed. 2 | # 3 | # - Gets list of PR labels. 4 | # - If 'release' label: 5 | # - Get release version using Poetry. 6 | # - Build the release. 7 | # - Draft a new GitHub release. 8 | # - Upload the wheel to the new GitHub release. 9 | # - Upload wheel to Test PyPi if build succeeds. (Future) 10 | # - Test install from Test PyPi. (Future) 11 | # - Upload wheel to PyPi if install test succeeds. (Future) 12 | # - Generate new CHANGELOG. 13 | # - Get next semantic version. 14 | # - Close old milestones. 15 | # - Create new minor version milestone. 16 | # - Create new major version milestone. 17 | name: Do Release Workflow 18 | 19 | on: 20 | pull_request: 21 | branches: 22 | - master 23 | types: 24 | - closed 25 | 26 | jobs: 27 | create_new_release: 28 | name: Create New Release 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v3 32 | with: 33 | fetch-depth: 0 34 | 35 | - name: Get PR labels 36 | id: prlabels 37 | uses: joerick/pr-labels-action@v1.0.8 38 | 39 | - name: Get release version 40 | id: relversion 41 | if: contains(steps.prlabels.outputs.labels, ' release ') 42 | run: | 43 | pip install poetry 44 | echo "version=$(echo $(poetry version | cut -d' ' -f2))" >> $GITHUB_OUTPUT 45 | if [[ $version != *"-rc"* ]]; then 46 | echo "do_release=1" >> $GITHUB_ENV 47 | echo "do_changelog=1" >> $GITHUB_ENV 48 | echo "do_milestones=1" >> $GITHUB_ENV 49 | fi 50 | 51 | - name: Build release 52 | id: build 53 | if: ${{ env.do_release == 1 }} 54 | run: | 55 | pip install -U pip poetry twine 56 | poetry build && twine check dist/* && echo "build_ok=1" >> $GITHUB_ENV 57 | 58 | - name: Cut the release 59 | id: cutrelease 60 | if: ${{ env.build_ok == 1 }} 61 | uses: release-drafter/release-drafter@v5 62 | with: 63 | name: "${{ steps.relversion.outputs.new_tag }}" 64 | tag: "${{ steps.relversion.outputs.new_tag }}" 65 | version: "${{ steps.relversion.outputs.new_tag }}" 66 | prerelease: false 67 | publish: true 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 70 | 71 | - name: Upload wheel to GitHub release 72 | id: upload-wheel 73 | if: ${{ env.build_ok == 1 }} 74 | uses: shogo82148/actions-upload-release-asset@v1 75 | with: 76 | upload_url: ${{ steps.cutrelease.outputs.upload_url }} 77 | asset_path: ./dist/*.whl 78 | 79 | # - name: Publish to Test PyPi 80 | # if: ${{ env.build_ok == 1 }} 81 | # uses: pypa/gh-action-pypi-publish@release/v1 82 | # with: 83 | # user: __token__ 84 | # password: ${{ secrets.TEST_PYPI_API_TOKEN }} 85 | # repository_url: https://test.pypi.org/legacy/ 86 | 87 | # - name: Test install from Test PyPI 88 | # if: ${{ env.build_ok == 1 }} 89 | # run: | 90 | # sudo apt-get update 91 | # pip install \ 92 | # --index-url https://test.pypi.org/simple/ \ 93 | # --extra-index-url https://pypi.org/simple \ 94 | # docformatter==${{ steps.newversion.outputs.new_version }} && echo "install_ok=1" >> $GITHUB_ENV 95 | 96 | # - name: Publish to PyPi 97 | # if: ${{ env.install_ok == 1 }} 98 | # uses: pypa/gh-action-pypi-publish@release/v1 99 | # with: 100 | # user: __token__ 101 | # password: ${{ secrets.PYPI_API_TOKEN }} 102 | 103 | - name: Generate release changelog 104 | uses: heinrichreimer/github-changelog-generator-action@master 105 | if: ${{ env.do_changelog == 1 }} 106 | with: 107 | token: ${{ secrets.GITHUB_TOKEN }} 108 | sinceTag: "v1.3.1" 109 | excludeTagsRegex: "-rc[0-9]" 110 | breakingLabel: "Breaking Changes" 111 | breakingLabels: "V: major" 112 | enhancementLabel: "Features" 113 | enhancementLabels: "P: enhancement" 114 | bugsLabel: "Bug Fixes" 115 | bugLabels: "P: bug" 116 | excludeLabels: "release" 117 | issues: false 118 | issuesWoLabels: false 119 | maxIssues: 100 120 | pullRequests: true 121 | prWoLabels: false 122 | author: true 123 | unreleased: true 124 | compareLink: true 125 | stripGeneratorNotice: true 126 | verbose: true 127 | 128 | - name: Check if diff 129 | if: ${{ env.do_changelog == 1 }} 130 | continue-on-error: true 131 | run: > 132 | git diff --exit-code CHANGELOG.md && 133 | (echo "### No update" && exit 1) || (echo "### Commit update") 134 | 135 | - uses: EndBug/add-and-commit@v9 136 | name: Commit and push if diff 137 | if: ${{ env.do_changelog == 1 }} 138 | with: 139 | add: CHANGELOG.md 140 | message: 'chore: update CHANGELOG.md for new release' 141 | author_name: GitHub Actions 142 | author_email: action@github.com 143 | committer_name: GitHub Actions 144 | committer_email: actions@github.com 145 | push: true 146 | 147 | - name: Get next semantic version 148 | id: nextversion 149 | if: ${{ env.do_milestones == 1 }} 150 | uses: WyriHaximus/github-action-next-semvers@v1.2.1 151 | with: 152 | version: ${{ steps.relversion.outputs.version }} 153 | 154 | - name: Close old milestone 155 | if: ${{ env.do_milestones == 1 }} 156 | uses: WyriHaximus/github-action-close-milestone@master 157 | with: 158 | number: ${{ steps.relversion.outputs.version }} 159 | 160 | - name: Create new minor release milestone 161 | if: ${{ env.do_milestones == 1 }} 162 | uses: WyriHaximus/github-action-create-milestone@v1.2.0 163 | with: 164 | title: "${{ steps.nextversion.outputs.v_minor }}" 165 | env: 166 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 167 | 168 | - name: Create new major release milestone 169 | if: ${{ env.do_milestones == 1 }} 170 | uses: WyriHaximus/github-action-create-milestone@v1.2.0 171 | with: 172 | title: "${{ steps.nextversion.outputs.v_major }}" 173 | env: 174 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 175 | -------------------------------------------------------------------------------- /src/docformatter/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # docformatter.__main__.py is part of the docformatter project 4 | # 5 | # Copyright (C) 2012-2023 Steven Myint 6 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining 9 | # a copy of this software and associated documentation files (the 10 | # "Software"), to deal in the Software without restriction, including 11 | # without limitation the rights to use, copy, modify, merge, publish, 12 | # distribute, sublicense, and/or sell copies of the Software, and to 13 | # permit persons to whom the Software is furnished to do so, subject to 14 | # the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | # SOFTWARE. 27 | """Formats docstrings to follow PEP 257.""" 28 | 29 | 30 | # Standard Library Imports 31 | import contextlib 32 | import signal 33 | import sys 34 | 35 | # docformatter Package Imports 36 | import docformatter.configuration as _configuration 37 | import docformatter.format as _format 38 | 39 | 40 | def _help(): 41 | """Print docformatter's help.""" 42 | print( 43 | """\ 44 | usage: docformatter [-h] [-i | -c] [-d] [-r] [-e [EXCLUDE ...]] 45 | [-n [NON-CAP ...]] [-s [style]] [--rest-section-adorns REGEX] 46 | [--black] [--wrap-summaries length] 47 | [--wrap-descriptions length] [--force-wrap] 48 | [--tab-width width] [--blank] [--pre-summary-newline] 49 | [--pre-summary-space] [--make-summary-multi-line] 50 | [--close-quotes-on-newline] [--range line line] 51 | [--docstring-length length length] [--non-strict] 52 | [--config CONFIG] [--version] files [files ...] 53 | 54 | positional arguments: 55 | files files to format or '-' for standard in 56 | 57 | options: 58 | -h, --help show this help message and exit 59 | -i, --in-place make changes to files instead of printing diffs 60 | -c, --check only check and report incorrectly formatted files 61 | -d, --diff when used with `--check` or `--in-place`, also what 62 | changes would be made 63 | -r, --recursive drill down directories recursively 64 | -e [EXCLUDE ...], --exclude [EXCLUDE ...] 65 | in recursive mode, exclude directories and files by 66 | names 67 | -n [NON-CAP ...], --non-cap [NON-CAP ...] 68 | list of words not to capitalize when they appear as the 69 | first word in the summary 70 | 71 | -s style, --style style 72 | the docstring style to use when formatting parameter 73 | lists. One of epytext, sphinx. (default: sphinx) 74 | --rest-section-adorns REGEX 75 | regular expression for identifying reST section adornments 76 | (default: [!\"#$%&'()*+,-./\\:;<=>?@[]^_`{|}~]{4,}) 77 | --black make formatting compatible with standard black options 78 | (default: False) 79 | --wrap-summaries length 80 | wrap long summary lines at this length; set to 0 to 81 | disable wrapping (default: 79, 88 with --black option) 82 | --wrap-descriptions length 83 | wrap descriptions at this length; set to 0 to disable 84 | wrapping (default: 72, 88 with --black option) 85 | --force-wrap force descriptions to be wrapped even if it may result 86 | in a mess (default: False) 87 | --tab-width width tabs in indentation are this many characters when 88 | wrapping lines (default: 1) 89 | --blank add blank line after description (default: False) 90 | --pre-summary-newline 91 | add a newline before the summary of a multi-line 92 | docstring (default: False) 93 | --pre-summary-space add a space after the opening triple quotes 94 | (default: False) 95 | --make-summary-multi-line 96 | add a newline before and after the summary of a 97 | one-line docstring (default: False) 98 | --close-quotes-on-newline 99 | place closing triple quotes on a new-line when a 100 | one-line docstring wraps to two or more lines 101 | (default: False) 102 | --range line line apply docformatter to docstrings between these lines; 103 | line numbers are indexed at 1 (default: None) 104 | --docstring-length length length 105 | apply docformatter to docstrings of given length range 106 | (default: None) 107 | --non-strict don't strictly follow reST syntax to identify lists 108 | (see issue #67) (default: False) 109 | --config CONFIG path to file containing docformatter options 110 | --version show program's version number and exit 111 | """ 112 | ) 113 | 114 | 115 | def _main(argv, standard_out, standard_error, standard_in): 116 | """Run internal main entry point.""" 117 | configurator = _configuration.Configurater(argv) 118 | 119 | if "--help" in configurator.args_lst or "-h" in configurator.args_lst: 120 | _help() 121 | return 0 122 | else: 123 | configurator.do_parse_arguments() 124 | 125 | formator = _format.Formatter( 126 | configurator.args, 127 | stderror=standard_error, 128 | stdin=standard_in, 129 | stdout=standard_out, 130 | ) 131 | 132 | if "-" in configurator.args.files: 133 | formator.do_format_standard_in( 134 | configurator.parser, 135 | ) 136 | else: 137 | return formator.do_format_files() 138 | 139 | 140 | def main(): 141 | """Run the main entry point.""" 142 | # SIGPIPE is not available on Windows. 143 | with contextlib.suppress(AttributeError): 144 | # Exit on broken pipe. 145 | signal.signal(signal.SIGPIPE, signal.SIG_DFL) 146 | try: 147 | return _main( 148 | sys.argv, 149 | standard_out=sys.stdout, 150 | standard_error=sys.stderr, 151 | standard_in=sys.stdin, 152 | ) 153 | except KeyboardInterrupt: # pragma: no cover 154 | return _format.FormatResult.interrupted # pragma: no cover 155 | 156 | 157 | if __name__ == "__main__": 158 | sys.exit(main()) 159 | -------------------------------------------------------------------------------- /tests/test_classify_functions.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | # type: ignore 3 | # 4 | # tests.test_classify_functions.py is part of the docformatter project 5 | # 6 | # Copyright (C) 2012-2023 Steven Myint 7 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining 10 | # a copy of this software and associated documentation files (the 11 | # "Software"), to deal in the Software without restriction, including 12 | # without limitation the rights to use, copy, modify, merge, publish, 13 | # distribute, sublicense, and/or sell copies of the Software, and to 14 | # permit persons to whom the Software is furnished to do so, subject to 15 | # the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be 18 | # included in all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 24 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 25 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 26 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | # SOFTWARE. 28 | """Module for testing the classification functions.""" 29 | 30 | # Standard Library Imports 31 | import contextlib 32 | import sys 33 | import tokenize 34 | from io import BytesIO 35 | 36 | with contextlib.suppress(ImportError): 37 | if sys.version_info >= (3, 11): 38 | # Standard Library Imports 39 | import tomllib 40 | else: 41 | # Third Party Imports 42 | import tomli as tomllib 43 | 44 | # Third Party Imports 45 | import pytest 46 | 47 | # docformatter Package Imports 48 | from docformatter.classify import ( 49 | do_find_docstring_blocks, 50 | is_attribute_docstring, 51 | is_class_docstring, 52 | is_closing_quotes, 53 | is_code_line, 54 | is_definition_line, 55 | is_f_string, 56 | is_function_or_method_docstring, 57 | is_inline_comment, 58 | is_line_following_indent, 59 | is_module_docstring, 60 | is_nested_definition_line, 61 | is_newline_continuation, 62 | is_string_variable, 63 | ) 64 | 65 | with open("tests/_data/string_files/classify_functions.toml", "rb") as f: 66 | TEST_STRINGS = tomllib.load(f) 67 | 68 | 69 | def get_tokens(source: str) -> list[tokenize.TokenInfo]: 70 | return list(tokenize.tokenize(BytesIO(source.encode()).readline)) 71 | 72 | 73 | def get_string_index(tokens: list[tokenize.TokenInfo]) -> int: 74 | for i, tok in enumerate(tokens): 75 | if tok.type == tokenize.STRING: 76 | return i 77 | raise ValueError("No string token found.") 78 | 79 | 80 | def build_token(prefix: str, test_key: str) -> tokenize.TokenInfo: 81 | """Build a tokenize.TokenInfo from test data using a prefix ('' or 'prev_').""" 82 | data = TEST_STRINGS[test_key] 83 | return tokenize.TokenInfo( 84 | type=data[f"{prefix}type"], 85 | string=data[f"{prefix}string"], 86 | start=tuple(data[f"{prefix}start"]), 87 | end=tuple(data[f"{prefix}end"]), 88 | line=data[f"{prefix}line"], 89 | ) 90 | 91 | 92 | @pytest.mark.unit 93 | @pytest.mark.parametrize( 94 | "test_key, classifier", 95 | [ 96 | ("is_module_docstring", is_module_docstring), 97 | ("is_class_docstring", is_class_docstring), 98 | ("is_method_docstring", is_function_or_method_docstring), 99 | ("is_function_docstring", is_function_or_method_docstring), 100 | ("is_attribute_docstring", is_attribute_docstring), 101 | ("is_not_attribute_docstring", is_attribute_docstring), 102 | ], 103 | ) 104 | def test_docstring_classifiers(test_key, classifier): 105 | source = TEST_STRINGS[test_key]["instring"] 106 | expected = TEST_STRINGS[test_key]["expected"] 107 | 108 | tokens = get_tokens(source) 109 | index = get_string_index(tokens) 110 | 111 | result = classifier(tokens, index) 112 | assert result == expected, f"\nFailed {test_key}\nExpected {expected}\nGot {result}" 113 | 114 | 115 | @pytest.mark.unit 116 | @pytest.mark.parametrize( 117 | "test_key,classifier", 118 | [ 119 | ("is_code_line", is_code_line), 120 | ("is_closing_quotes", is_closing_quotes), 121 | ("is_definition_line_class", is_definition_line), 122 | ("is_definition_line_function", is_definition_line), 123 | ("is_definition_line_async_function", is_definition_line), 124 | ("is_not_definition_line_function", is_definition_line), 125 | ("is_inline_comment", is_inline_comment), 126 | ("is_line_following_indent", is_line_following_indent), 127 | ("is_nested_definition_line_class", is_nested_definition_line), 128 | ("is_nested_definition_line_function", is_nested_definition_line), 129 | ("is_nested_definition_line_async_function", is_nested_definition_line), 130 | ("is_not_nested_definition_line_function", is_nested_definition_line), 131 | ("is_newline_continuation", is_newline_continuation), 132 | ("is_string_variable", is_string_variable), 133 | ], 134 | ) 135 | def test_misc_classifiers(test_key, classifier): 136 | token = build_token("", test_key) 137 | 138 | try: 139 | prev_token = build_token("prev_", test_key) 140 | result = classifier(token, prev_token) 141 | except KeyError: 142 | result = classifier(token) 143 | 144 | expected = TEST_STRINGS[test_key]["expected"] 145 | assert result == expected, f"Failed {test_key}\nExpected {expected}\nGot {result}" 146 | 147 | 148 | @pytest.mark.unit 149 | @pytest.mark.skipif(sys.version_info < (3, 12), reason="requires Python 3.12 or higher") 150 | def test_is_f_string(): 151 | """Test is_f_string classifier (requires Python 3.12+).""" 152 | token = build_token("", "is_f_string") 153 | prev_token = build_token("prev_", "is_f_string") 154 | 155 | if sys.version_info >= (3, 13): 156 | expected = TEST_STRINGS["is_f_string"]["expected313"] 157 | else: 158 | expected = TEST_STRINGS["is_f_string"]["expected"] 159 | result = is_f_string(token, prev_token) 160 | 161 | assert result == expected, f"Failed is_f_string\nExpected {expected}\nGot {result}" 162 | 163 | 164 | @pytest.mark.integration 165 | @pytest.mark.order(5) 166 | @pytest.mark.parametrize( 167 | "test_key, expected", 168 | [ 169 | ("find_module_docstring", [(0, 1, "module")]), 170 | ("find_class_docstring", [(1, 6, "class")]), 171 | ("find_function_docstring", [(1, 8, "function")]), 172 | ("find_function_docstring_with_decorator", [(4, 11, "function")]), 173 | ("find_attribute_docstring", [(1, 5, "attribute")]), 174 | ( 175 | "find_multiple_docstrings", 176 | [(0, 1, "module"), (4, 9, "class"), (12, 20, "function")], 177 | ), 178 | ], 179 | ) 180 | def test_find_docstring_blocks(test_key, expected): 181 | source = TEST_STRINGS[test_key]["instring"] 182 | tokens = get_tokens(source) 183 | 184 | result = do_find_docstring_blocks(tokens) 185 | assert result == expected, f"Failed {test_key}\nExpected {expected}\nGot {result}" 186 | -------------------------------------------------------------------------------- /src/docformatter/patterns/fields.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # docformatter.patterns.fields.py is part of the docformatter project 4 | # 5 | # Copyright (C) 2012-2023 Steven Myint 6 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining 9 | # a copy of this software and associated documentation files (the 10 | # "Software"), to deal in the Software without restriction, including 11 | # without limitation the rights to use, copy, modify, merge, publish, 12 | # distribute, sublicense, and/or sell copies of the Software, and to 13 | # permit persons to whom the Software is furnished to do so, subject to 14 | # the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | # SOFTWARE. 27 | """This module provides docformatter's field list pattern recognition functions.""" 28 | 29 | 30 | # Standard Library Imports 31 | import re 32 | from re import Match 33 | from typing import Union 34 | 35 | # docformatter Package Imports 36 | from docformatter.constants import ( 37 | EPYTEXT_REGEX, 38 | GOOGLE_REGEX, 39 | NUMPY_REGEX, 40 | SPHINX_REGEX, 41 | ) 42 | 43 | 44 | def do_find_field_lists( 45 | text: str, 46 | style: str, 47 | ) -> tuple[list[tuple[int, int]], bool]: 48 | r"""Determine if docstring contains any field lists. 49 | 50 | Parameters 51 | ---------- 52 | text : str 53 | The docstring description to check for field list patterns. 54 | style : str 55 | The field list style used. 56 | 57 | Returns 58 | ------- 59 | tuple[list[tuple], bool] 60 | A tuple containing lists of tuples and a boolean. Each inner tuple 61 | contains the starting and ending position of each field list found in the 62 | description. The boolean indicates whether long field list lines should 63 | be wrapped. 64 | """ 65 | _field_idx = [] 66 | _wrap_parameters = False 67 | 68 | if style == "epytext": 69 | _field_idx = [ 70 | (_field.start(0), _field.end(0)) 71 | for _field in re.finditer(EPYTEXT_REGEX, text) 72 | ] 73 | _wrap_parameters = True 74 | elif style == "sphinx": 75 | _field_idx = [ 76 | (_field.start(0), _field.end(0)) 77 | for _field in re.finditer(SPHINX_REGEX, text) 78 | ] 79 | _wrap_parameters = True 80 | 81 | return _field_idx, _wrap_parameters 82 | 83 | 84 | def is_field_list( 85 | text: str, 86 | style: str, 87 | ) -> bool: 88 | """Determine if docstring contains field lists. 89 | 90 | Parameters 91 | ---------- 92 | text : str 93 | The docstring text. 94 | style : str 95 | The field list style to use. 96 | 97 | Returns 98 | ------- 99 | is_field_list : bool 100 | Whether the field list pattern for style was found in the docstring. 101 | """ 102 | split_lines = text.rstrip().splitlines() 103 | 104 | if style == "epytext": 105 | return any(is_epytext_field_list(line) for line in split_lines) 106 | elif style == "sphinx": 107 | return any(is_sphinx_field_list(line) for line in split_lines) 108 | 109 | return False 110 | 111 | 112 | def is_epytext_field_list(line: str) -> Union[Match[str], None]: 113 | """Check if the line is an Epytext field list. 114 | 115 | Parameters 116 | ---------- 117 | line : str 118 | The line to check for Epytext field list patterns. 119 | 120 | Returns 121 | ------- 122 | Match[str] | None 123 | A match object if the line matches an Epytext field list pattern, None 124 | otherwise. 125 | 126 | Notes 127 | ----- 128 | Epytext field lists have the following pattern: 129 | @param x: 130 | @type x: 131 | """ 132 | return re.match(EPYTEXT_REGEX, line) 133 | 134 | 135 | def is_google_field_list(line: str) -> Union[Match[str], None]: 136 | """Check if the line is a Google field list. 137 | 138 | Parameters 139 | ---------- 140 | line: str 141 | The line to check for Google field list patterns. 142 | 143 | Returns 144 | ------- 145 | Match[str] | None 146 | A match object if the line matches a Google field list pattern, None otherwise. 147 | 148 | Notes 149 | ----- 150 | Google field lists have the following pattern: 151 | x (int): Description of x. 152 | """ 153 | return re.match(GOOGLE_REGEX, line) 154 | 155 | 156 | def is_numpy_field_list(line: str) -> Union[Match[str], None]: 157 | """Check if the line is a NumPy field list. 158 | 159 | Parameters 160 | ---------- 161 | line: str 162 | The line to check for NumPy field list patterns. 163 | 164 | Returns 165 | ------- 166 | Match[str] | None 167 | A match object if the line matches a NumPy field list pattern, None otherwise. 168 | 169 | Notes 170 | ----- 171 | NumPy field lists have the following pattern: 172 | x : int 173 | Description of x. 174 | x 175 | Description of x. 176 | """ 177 | return re.match(NUMPY_REGEX, line) 178 | 179 | 180 | def is_sphinx_field_list(line: str) -> Union[Match[str], None]: 181 | """Check if the line is a Sphinx field list. 182 | 183 | Parameters 184 | ---------- 185 | line: str 186 | The line to check for Sphinx field list patterns. 187 | 188 | Returns 189 | ------- 190 | Match[str] | None 191 | A match object if the line matches a Sphinx field list pattern, None otherwise. 192 | 193 | Notes 194 | ----- 195 | Sphinx field lists have the following pattern: 196 | :parameter: description 197 | """ 198 | return re.match(SPHINX_REGEX, line) 199 | 200 | 201 | # TODO: Add a USER_DEFINED_REGEX to constants.py and use that instead of the 202 | # hardcoded patterns. 203 | def is_user_defined_field_list(line: str) -> Union[Match[str], None]: 204 | """Check if the line is a user-defined field list. 205 | 206 | Parameters 207 | ---------- 208 | line: str 209 | The line to check for user-defined field list patterns. 210 | 211 | Returns 212 | ------- 213 | Match[str] | None 214 | A match object if the line matches a user-defined field list pattern, None 215 | otherwise. 216 | 217 | Notes 218 | ----- 219 | User-defined field lists have the following pattern: 220 | parameter - description 221 | parameter -- description 222 | @parameter description 223 | 224 | These patterns were in the original docformatter code. These patterns do not 225 | conform to any common docstring styles. There is no documented reason they were 226 | included and are retained for historical purposes. 227 | """ 228 | return ( 229 | re.match(r"[\S ]+ - \S+", line) 230 | or re.match(r"\s*\S+\s+--\s+", line) 231 | or re.match(r"^ *@[a-zA-Z0-9_\- ]*(?:(?!:).)*$", line) 232 | ) 233 | -------------------------------------------------------------------------------- /src/docformatter/patterns/lists.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # docformatter.patterns.lists.py is part of the docformatter project 4 | # 5 | # Copyright (C) 2012-2023 Steven Myint 6 | # Copyright (C) 2023-2025 Doyle "weibullguy" Rowland 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining 9 | # a copy of this software and associated documentation files (the 10 | # "Software"), to deal in the Software without restriction, including 11 | # without limitation the rights to use, copy, modify, merge, publish, 12 | # distribute, sublicense, and/or sell copies of the Software, and to 13 | # permit persons to whom the Software is furnished to do so, subject to 14 | # the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | # SOFTWARE. 27 | """This module provides docformatter's list pattern recognition functions.""" 28 | 29 | 30 | # Standard Library Imports 31 | import re 32 | from re import Match 33 | from typing import Union 34 | 35 | # docformatter Package Imports 36 | from docformatter.constants import ( 37 | BULLET_REGEX, 38 | ENUM_REGEX, 39 | HEURISTIC_MIN_LIST_ASPECT_RATIO, 40 | OPTION_REGEX, 41 | ) 42 | 43 | # docformatter Local Imports 44 | from .fields import ( 45 | is_epytext_field_list, 46 | is_field_list, 47 | is_google_field_list, 48 | is_numpy_field_list, 49 | is_sphinx_field_list, 50 | is_user_defined_field_list, 51 | ) 52 | from .headers import is_alembic_header, is_numpy_section_header, is_rest_section_header 53 | from .misc import is_inline_math, is_literal_block 54 | 55 | 56 | def is_type_of_list( 57 | text: str, 58 | strict: bool, 59 | style: str, 60 | ) -> bool: 61 | """Determine if docstring line is a list. 62 | 63 | Parameters 64 | ---------- 65 | text : str 66 | The text to check for potential lists. 67 | strict : bool 68 | Whether to strictly adhere to the wrap length argument. If True, 69 | even heuristic lists will be wrapped. 70 | style : str 71 | The docstring style in use. One of 'epytext', 'sphinx', numpy', or 'googlw'. 72 | 73 | Returns 74 | ------- 75 | bool 76 | True if a list pattern is identified, False otherwise. 77 | """ 78 | split_lines = text.rstrip().splitlines() 79 | 80 | if is_heuristic_list(text, strict): 81 | return True 82 | 83 | if is_field_list(text, style): 84 | return False 85 | 86 | return any( 87 | ( 88 | is_bullet_list(line) 89 | or is_enumerated_list(line) 90 | or is_rest_section_header(line) 91 | or is_option_list(line) 92 | or is_epytext_field_list(line) 93 | or is_sphinx_field_list(line) 94 | or is_numpy_field_list(line) 95 | or is_numpy_section_header(line) 96 | or is_google_field_list(line) 97 | or is_user_defined_field_list(line) 98 | or is_literal_block(line) 99 | or is_inline_math(line) 100 | or is_alembic_header(line) 101 | ) 102 | for line in split_lines 103 | ) 104 | 105 | 106 | def is_bullet_list(line: str) -> Union[Match[str], None]: 107 | """Check if the line is a bullet list item. 108 | 109 | Parameters 110 | ---------- 111 | line : str 112 | The line to check for bullet list patterns. 113 | 114 | Returns 115 | ------- 116 | Match[str] | None 117 | A match object if the line matches a bullet list pattern, None otherwise. 118 | 119 | Notes 120 | ----- 121 | Bullet list items have the following pattern: 122 | - item 123 | * item 124 | + item 125 | 126 | See `_ 127 | """ 128 | return re.match(BULLET_REGEX, line) 129 | 130 | 131 | def is_definition_list(line: str) -> Union[Match[str], None]: 132 | """Check if the line is a definition list item. 133 | 134 | Parameters 135 | ---------- 136 | line : str 137 | The line to check for definition list patterns. 138 | 139 | Returns 140 | ------- 141 | Match[str] | None 142 | A match object if the line matches a definition list pattern, None otherwise. 143 | 144 | Notes 145 | ----- 146 | Definition list items have the following pattern: 147 | term: definition 148 | 149 | See `_ 150 | """ 151 | return re.match(ENUM_REGEX, line) 152 | 153 | 154 | def is_enumerated_list(line: str) -> Union[Match[str], None]: 155 | """Check if the line is an enumerated list item. 156 | 157 | Parameters 158 | ---------- 159 | line : str 160 | The line to check for enumerated list patterns. 161 | 162 | Returns 163 | ------- 164 | Match[str] | None 165 | A match object if the line matches an enumerated list pattern, None otherwise. 166 | 167 | Notes 168 | ----- 169 | Enumerated list items have the following pattern: 170 | 1. item 171 | 2. item 172 | 173 | See `_ 174 | """ 175 | return re.match(ENUM_REGEX, line) 176 | 177 | 178 | def is_heuristic_list(text: str, strict: bool) -> bool: 179 | """Check if the line is a heuristic list item. 180 | 181 | Heuristic lists are identified by a long number of lines with short columns. 182 | 183 | Parameters 184 | ---------- 185 | text : str 186 | The text to check for heuristic list patterns. 187 | strict: bool 188 | If True, the function will return False. 189 | If False, it will return True if the text has a high aspect ratio, 190 | indicating it is likely a list. 191 | 192 | Returns 193 | ------- 194 | Match[str] | None 195 | A match object if the line matches a heuristic list pattern, None otherwise. 196 | """ 197 | split_lines = text.rstrip().splitlines() 198 | 199 | # TODO: Find a better way of doing this. Conversely, create a logger and log 200 | # potential lists for the user to decide if they are lists or not. 201 | # Very large number of lines but short columns probably means a list of 202 | # items. 203 | if ( 204 | len(split_lines) / max([len(line.strip()) for line in split_lines] + [1]) 205 | > HEURISTIC_MIN_LIST_ASPECT_RATIO 206 | ) and not strict: 207 | return True 208 | 209 | return False 210 | 211 | 212 | def is_option_list(line: str) -> Union[Match[str], None]: 213 | """Check if the line is an option list item. 214 | 215 | Parameters 216 | ---------- 217 | line : str 218 | The line to check for option list patterns. 219 | 220 | Returns 221 | ------- 222 | Match[str] | None 223 | A match object if the line matches an option list pattern, None otherwise. 224 | 225 | Notes 226 | ----- 227 | Option list items have the following pattern: 228 | -a, --all: Show all items. 229 | -h, --help: Show help message. 230 | 231 | See `_ 232 | """ 233 | return re.match(OPTION_REGEX, line) 234 | --------------------------------------------------------------------------------