├── tests ├── __init__.py ├── unit │ ├── __init__.py │ ├── result.py │ ├── test___init__raises.py │ ├── test___init__args.py │ ├── test___init__.py │ └── test___init__attrs.py ├── integration │ ├── __init__.py │ └── test___init__.py └── test_docstring.py ├── poetry.toml ├── .vscode └── settings.json ├── .flake8 ├── flake8_docstrings_complete ├── constants.py ├── types_.py ├── args.py ├── docstring.py ├── raises.py ├── attrs.py └── __init__.py ├── .devcontainer └── devcontainer.json ├── .github ├── dependabot.yml └── workflows │ └── ci-cd.yaml ├── pyproject.toml ├── tox.ini ├── .gitignore ├── CHANGELOG.md └── LICENSE /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests.""" 2 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit tests.""" 2 | -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | """Integration tests.""" 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.analysis.typeCheckingMode": "basic", 3 | } -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 99 3 | max-doc-length = 99 4 | extend-ignore = E203,W503,E231,E201 5 | per-file-ignores = 6 | tests/*:D205,D400 7 | flake8_docstrings_complete/*:N802 8 | test-docs-pattern = given/when/then 9 | -------------------------------------------------------------------------------- /flake8_docstrings_complete/constants.py: -------------------------------------------------------------------------------- 1 | """Shared constants for the linter.""" 2 | 3 | ERROR_CODE_PREFIX = "DCO" 4 | MORE_INFO_BASE = ( 5 | ", more information: https://github.com/jdkandersson/flake8-docstrings-complete#fix-" 6 | ) 7 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Python 3", 3 | "image": "mcr.microsoft.com/devcontainers/python:0-3.11", 4 | "features": { 5 | "ghcr.io/devcontainers/features/python:1": {}, 6 | "ghcr.io/devcontainers-contrib/features/black:1": {}, 7 | "ghcr.io/devcontainers-contrib/features/tox:1": {}, 8 | "ghcr.io/devcontainers-contrib/features/isort:1": {}, 9 | "ghcr.io/devcontainers-contrib/features/poetry:1": {} 10 | } 11 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "monthly" 12 | - package-ecosystem: "pip" 13 | directory: "/" 14 | schedule: 15 | interval: "monthly" 16 | -------------------------------------------------------------------------------- /tests/unit/result.py: -------------------------------------------------------------------------------- 1 | """Get the linting result.""" 2 | 3 | from __future__ import annotations 4 | 5 | import ast 6 | 7 | from flake8_docstrings_complete import Plugin 8 | 9 | 10 | def get(code: str, filename: str = "source.py") -> tuple[str, ...]: 11 | """Generate linting results. 12 | 13 | Args: 14 | code: The code to check. 15 | filename: The name of the file the code is in. 16 | 17 | Returns: 18 | The linting result. 19 | """ 20 | tree = ast.parse(code) 21 | plugin = Plugin(tree, filename) 22 | return tuple(f"{line}:{col} {msg}" for line, col, msg, _ in plugin.run()) 23 | -------------------------------------------------------------------------------- /flake8_docstrings_complete/types_.py: -------------------------------------------------------------------------------- 1 | """Types that support execution.""" 2 | 3 | from __future__ import annotations 4 | 5 | import enum 6 | from typing import NamedTuple 7 | 8 | 9 | class Problem(NamedTuple): 10 | """Represents a problem within the code. 11 | 12 | Attrs: 13 | lineno: The line number the problem occurred on 14 | col_offset: The column the problem occurred on 15 | msg: The message explaining the problem 16 | """ 17 | 18 | lineno: int 19 | col_offset: int 20 | msg: str 21 | 22 | 23 | class FileType(str, enum.Enum): 24 | """The type of file being processed. 25 | 26 | Attrs: 27 | TEST: A file with tests. 28 | FIXTURE: A file with fixtures. 29 | DEFAULT: All other files. 30 | """ 31 | 32 | TEST = "test" 33 | FIXTURE = "fixture" 34 | DEFAULT = "default" 35 | 36 | 37 | class Node(NamedTuple): 38 | """Information about a node. 39 | 40 | Attrs: 41 | name: Short description of the node. 42 | lineno: The line number the node is on. 43 | col_offset: The column of the node. 44 | """ 45 | 46 | name: str 47 | lineno: int 48 | col_offset: int 49 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "flake8-docstrings-complete" 3 | version = "1.4.1" 4 | description = "A linter that checks docstrings are complete" 5 | authors = ["David Andersson "] 6 | license = "Apache 2.0" 7 | readme = "README.md" 8 | packages = [{ include = "flake8_docstrings_complete" }] 9 | classifiers = [ 10 | "Framework :: Flake8", 11 | "Environment :: Console", 12 | "Intended Audience :: Developers", 13 | "License :: OSI Approved :: Apache Software License", 14 | "Programming Language :: Python", 15 | "Programming Language :: Python :: 3", 16 | "Programming Language :: Python :: 3.13", 17 | "Programming Language :: Python :: 3.12", 18 | "Programming Language :: Python :: 3.11", 19 | "Programming Language :: Python :: 3.10", 20 | "Programming Language :: Python :: 3.9", 21 | "Topic :: Software Development :: Libraries :: Python Modules", 22 | "Topic :: Software Development :: Quality Assurance", 23 | ] 24 | 25 | [tool.poetry.dependencies] 26 | python = "^3.9.0" 27 | flake8 = ">= 5" 28 | 29 | [build-system] 30 | requires = ["poetry-core"] 31 | build-backend = "poetry.core.masonry.api" 32 | 33 | [tool.poetry.plugins."flake8.extension"] 34 | DCO = "flake8_docstrings_complete:Plugin" 35 | 36 | [tool.black] 37 | line-length = 99 38 | target-version = ["py38"] 39 | 40 | [tool.isort] 41 | line_length = 99 42 | profile = "black" 43 | 44 | [tool.coverage.run] 45 | branch = true 46 | 47 | [tool.coverage.report] 48 | fail_under = 100 49 | show_missing = true 50 | 51 | [tool.mypy] 52 | ignore_missing_imports = true 53 | check_untyped_defs = true 54 | disallow_untyped_defs = true 55 | 56 | [[tool.mypy.overrides]] 57 | module = "tests.*" 58 | disallow_untyped_defs = false 59 | 60 | [tool.pylint.messages_control] 61 | enable = ["useless-suppression"] 62 | disable = ["wrong-import-position"] 63 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skipsdist=True 3 | envlist = lint, test-flake8{5,6}, coverage-report 4 | 5 | [vars] 6 | src_path = {toxinidir}/flake8_docstrings_complete 7 | tst_path = {toxinidir}/tests/ 8 | all_path = {[vars]src_path} {[vars]tst_path} 9 | 10 | [testenv] 11 | allowlist_externals=python,poetry 12 | setenv = 13 | PYTHONPATH = {toxinidir}:{toxinidir}/lib:{[vars]src_path} 14 | PYTHONBREAKPOINT=ipdb.set_trace 15 | PY_COLORS=1 16 | passenv = 17 | PYTHONPATH 18 | 19 | [testenv:fmt] 20 | description = Apply coding style standards to code 21 | deps = 22 | isort>=5,<6 23 | black>=22 24 | commands = 25 | isort {[vars]all_path} 26 | black {[vars]all_path} 27 | 28 | [testenv:lint] 29 | description = Check code against coding style standards 30 | deps = 31 | mypy>=0,<1 32 | isort>=5,<6 33 | black>=22 34 | flake8-docstrings>=1,<2 35 | flake8-builtins>=2,<3 36 | flake8-test-docs>=1,<2 37 | pep8-naming>=0,<1 38 | codespell>=2,<3 39 | pylint>=2,<3 40 | pydocstyle>=6,<7 41 | pytest>=7,<8 42 | commands = 43 | pydocstyle {[vars]src_path} 44 | codespell {toxinidir} --skip {toxinidir}/.git --skip {toxinidir}/.tox \ 45 | --skip {toxinidir}/.venv --skip {toxinidir}/.mypy_cache 46 | flake8 {[vars]all_path} 47 | isort --check-only --diff {[vars]all_path} 48 | black --check --diff {[vars]all_path} 49 | mypy {[vars]all_path} 50 | pylint {[vars]all_path} 51 | pydocstyle {[vars]src_path} 52 | 53 | [testenv:test-flake8{5,6,7}] 54 | description = Run tests 55 | deps = 56 | flake85: flake8>=5,<6 57 | flake86: flake8>=6,<7 58 | flake87: flake8>=7,<8 59 | pytest>=8,<9 60 | pytest-cov>=6,<7 61 | astpretty>=3,<4 62 | coverage[toml]>=7,<8 63 | poetry 64 | commands = 65 | poetry install --only-root 66 | flake8 --version 67 | coverage run \ 68 | -m pytest -v --tb native -s {posargs} 69 | coverage report 70 | 71 | [testenv:coverage-report] 72 | description = Create test coverage report 73 | deps = 74 | coverage[toml]>=7,<8 75 | commands = 76 | coverage report 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Don't commit lock file 132 | poetry.lock 133 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ## [v1.4.1] - 2024-11-07 6 | 7 | ### Added 8 | 9 | - Support for Python 3.12 and 3.13 and Flake8 7. 10 | 11 | ## [v1.4.0] - 2024-11-07 12 | 13 | ### Added 14 | 15 | - Exceptions for docstring contents for private functions. 16 | 17 | ## [v1.3.0] - 2023-11-29 18 | 19 | ### Added 20 | 21 | - Support for `typing.overload`. 22 | 23 | ## [v1.2.0] - 2023-07-12 24 | 25 | ### Added 26 | 27 | - Support for `functools.cached_property`. 28 | 29 | ## [v1.1.0] - 2023-01-26 30 | 31 | ### Added 32 | 33 | - Lint check that ensures all function/ method arguments are described only 34 | once. 35 | - Lint check that ensures all class attributes are described only once. 36 | - Lint check that ensures all raised exceptions are described only once. 37 | 38 | ## [v1.0.4] - 2023-01-13 39 | 40 | ### Changed 41 | 42 | - Changed only class attributes to be required in class attributes section, 43 | instance attributes are now optional 44 | 45 | ## [v1.0.3] - 2023-01-05 46 | 47 | ### Added 48 | 49 | - Support for class properties 50 | 51 | ## [v1.0.2] - 2023-01-04 52 | 53 | ### Added 54 | 55 | - Support for flake8 version 5 56 | 57 | ## [v1.0.1] - 2023-01-03 58 | 59 | ### Fixed 60 | 61 | - Fixed definition of a section start to be a non-empty line rather than based 62 | on whether it has a named header 63 | 64 | ## [v1.0.0] - 2023-01-02 65 | 66 | ### Added 67 | 68 | #### Function/ Method Arguments 69 | 70 | - Lint check that ensures all function/ method arguments are documented 71 | - Lint check that ensures docstring doesn't describe arguments the function/ 72 | method doesn't have 73 | - Lint check that ensures there is at most one arguments section in the 74 | docstring 75 | - Lint check that ensures there is no empty arguments section in the docstring 76 | - Support for unused arguments for which descriptions are optional 77 | - Support `*args` and `**kwargs` 78 | - Support positional only arguments 79 | - Support keyword only arguments 80 | - Support ignoring `self` and `cls` arguments 81 | - Support for skipping test functions in test files 82 | - Support for skipping test fixtures in test and fixture files 83 | - Support async functions/ methods 84 | 85 | #### Function/ Method Return Value 86 | 87 | - Lint check that ensures all functions/ methods that return a value have the 88 | returns section in the docstring 89 | - Lint check that ensures a function that does not return a value does not have 90 | the returns section 91 | - Lint check that ensures there is at most one returns section in the docstring 92 | 93 | #### Function/ Method Yield Value 94 | 95 | - Lint check that ensures all functions/ methods that yield a value have the 96 | yields section in the docstring 97 | - Lint check that ensures a function that does not yield a value does not have 98 | the yields section 99 | - Lint check that ensures there is at most one yields section in the docstring 100 | 101 | #### Function/ Method Exception Handling 102 | 103 | - Lint check that ensures all function/ method exceptions are documented 104 | - Lint check that ensures docstring doesn't describe exceptions the function/ 105 | method doesn't raise 106 | - Lint check that ensures there is at most one raises section in the docstring 107 | - Lint check that ensures the raises section describes at least one exception 108 | 109 | #### Class Attributes 110 | 111 | - Lint check that ensures all class attributes are documented 112 | - Lint check that ensures docstring doesn't describe attributes the class 113 | doesn't have 114 | - Lint check that ensures there is at most one attributes section in the 115 | docstring 116 | - Support for private attributes for which descriptions are optional 117 | - Support for class attributes defined on the class and other `classmethod` 118 | methods 119 | - Support for instance attributes defined in `__init__` and other non-static and 120 | non-`classmethod` methods 121 | - Support async functions/ methods 122 | 123 | [//]: # "Release links" 124 | [v1.0.0]: https://github.com/jdkandersson/flake8-docstrings-complete/releases/v1.0.0 125 | [v1.0.1]: https://github.com/jdkandersson/flake8-docstrings-complete/releases/v1.0.1 126 | [v1.0.2]: https://github.com/jdkandersson/flake8-docstrings-complete/releases/v1.0.2 127 | [v1.0.3]: https://github.com/jdkandersson/flake8-docstrings-complete/releases/v1.0.3 128 | [v1.0.4]: https://github.com/jdkandersson/flake8-docstrings-complete/releases/v1.0.4 129 | [v1.1.0]: https://github.com/jdkandersson/flake8-docstrings-complete/releases/v1.1.0 130 | [v1.2.0]: https://github.com/jdkandersson/flake8-docstrings-complete/releases/v1.2.0 131 | [v1.3.0]: https://github.com/jdkandersson/flake8-docstrings-complete/releases/v1.3.0 132 | [v1.4.0]: https://github.com/jdkandersson/flake8-docstrings-complete/releases/v1.4.0 133 | [v1.4.1]: https://github.com/jdkandersson/flake8-docstrings-complete/releases/v1.4.1 134 | -------------------------------------------------------------------------------- /flake8_docstrings_complete/args.py: -------------------------------------------------------------------------------- 1 | """The arguments section checks.""" 2 | 3 | from __future__ import annotations 4 | 5 | import ast 6 | from collections import Counter 7 | from typing import Iterator 8 | 9 | from . import docstring, types_ 10 | from .constants import ERROR_CODE_PREFIX, MORE_INFO_BASE 11 | 12 | ARGS_SECTION_NOT_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}020" 13 | ARGS_SECTION_NOT_IN_DOCSTR_MSG = ( 14 | f"{ARGS_SECTION_NOT_IN_DOCSTR_CODE} a function/ method with arguments should have the " 15 | "arguments section in the docstring" 16 | f"{MORE_INFO_BASE}{ARGS_SECTION_NOT_IN_DOCSTR_CODE.lower()}" 17 | ) 18 | ARGS_SECTION_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}021" 19 | ARGS_SECTION_IN_DOCSTR_MSG = ( 20 | f"{ARGS_SECTION_IN_DOCSTR_CODE} a function/ method without arguments should not have the " 21 | "arguments section in the docstring" 22 | f"{MORE_INFO_BASE}{ARGS_SECTION_IN_DOCSTR_CODE.lower()}" 23 | ) 24 | MULT_ARGS_SECTIONS_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}022" 25 | MULT_ARGS_SECTIONS_IN_DOCSTR_MSG = ( 26 | f"{MULT_ARGS_SECTIONS_IN_DOCSTR_CODE} a docstring should only contain a single arguments " 27 | f"section, found %s{MORE_INFO_BASE}{MULT_ARGS_SECTIONS_IN_DOCSTR_CODE.lower()}" 28 | ) 29 | ARG_NOT_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}023" 30 | ARG_NOT_IN_DOCSTR_MSG = ( 31 | f'{ARG_NOT_IN_DOCSTR_CODE} "%s" argument should be described in the docstring{MORE_INFO_BASE}' 32 | f"{ARG_NOT_IN_DOCSTR_CODE.lower()}" 33 | ) 34 | ARG_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}024" 35 | ARG_IN_DOCSTR_MSG = ( 36 | f'{ARG_IN_DOCSTR_CODE} "%s" argument should not be described in the docstring{MORE_INFO_BASE}' 37 | f"{ARG_IN_DOCSTR_CODE.lower()}" 38 | ) 39 | DUPLICATE_ARG_CODE = f"{ERROR_CODE_PREFIX}025" 40 | DUPLICATE_ARG_MSG = ( 41 | f'{DUPLICATE_ARG_CODE} "%s" argument documented multiple times{MORE_INFO_BASE}' 42 | f"{DUPLICATE_ARG_CODE.lower()}" 43 | ) 44 | 45 | SKIP_ARGS = {"self", "cls"} 46 | UNUSED_ARGS_PREFIX = "_" 47 | 48 | 49 | def _iter_args(args: ast.arguments) -> Iterator[ast.arg]: 50 | """Iterate over all arguments. 51 | 52 | Adds vararg and kwarg to the args. 53 | 54 | Args: 55 | args: The arguments to iter over. 56 | 57 | Yields: 58 | All the arguments. 59 | """ 60 | yield from (arg for arg in args.args if arg.arg not in SKIP_ARGS) 61 | yield from (arg for arg in args.posonlyargs if arg.arg not in SKIP_ARGS) 62 | yield from (arg for arg in args.kwonlyargs) 63 | if args.vararg: 64 | yield args.vararg 65 | if args.kwarg: 66 | yield args.kwarg 67 | 68 | 69 | def check( 70 | docstr_info: docstring.Docstring, 71 | docstr_node: ast.Constant, 72 | args: ast.arguments, 73 | is_private: bool, 74 | ) -> Iterator[types_.Problem]: 75 | """Check that all function/ method arguments are described in the docstring. 76 | 77 | Check the function/ method has at most one args section. 78 | Check that all arguments of the function/ method are documented except certain arguments (like 79 | self). 80 | Check that a function/ method without arguments does not have an args section. 81 | 82 | Args: 83 | docstr_info: Information about the docstring. 84 | docstr_node: The docstring node. 85 | args: The arguments of the function. 86 | is_private: If the function for the docstring is private. 87 | 88 | Yields: 89 | All the problems with the arguments. 90 | """ 91 | all_args = list(_iter_args(args)) 92 | all_used_args = list(arg for arg in all_args if not arg.arg.startswith(UNUSED_ARGS_PREFIX)) 93 | 94 | # Check that args section is in docstring if function/ method has used arguments 95 | if all_used_args and docstr_info.args is None and not is_private: 96 | yield types_.Problem( 97 | docstr_node.lineno, docstr_node.col_offset, ARGS_SECTION_NOT_IN_DOCSTR_MSG 98 | ) 99 | # Check that args section is not in docstring if function/ method has no arguments 100 | if not all_args and docstr_info.args is not None: 101 | yield types_.Problem( 102 | docstr_node.lineno, docstr_node.col_offset, ARGS_SECTION_IN_DOCSTR_MSG 103 | ) 104 | 105 | # Checks for function with arguments and args section 106 | if all_args and docstr_info.args is not None: 107 | docstr_args = set(docstr_info.args) 108 | 109 | # Check for multiple args sections 110 | if len(docstr_info.args_sections) > 1: 111 | yield types_.Problem( 112 | docstr_node.lineno, 113 | docstr_node.col_offset, 114 | MULT_ARGS_SECTIONS_IN_DOCSTR_MSG % ",".join(docstr_info.args_sections), 115 | ) 116 | 117 | # Check for function arguments that are not in the docstring 118 | yield from ( 119 | types_.Problem(arg.lineno, arg.col_offset, ARG_NOT_IN_DOCSTR_MSG % arg.arg) 120 | for arg in all_used_args 121 | if arg.arg not in docstr_args 122 | ) 123 | 124 | # Check for arguments in the docstring that are not function arguments 125 | func_args = set(arg.arg for arg in all_args) 126 | yield from ( 127 | types_.Problem(docstr_node.lineno, docstr_node.col_offset, ARG_IN_DOCSTR_MSG % arg) 128 | for arg in sorted(docstr_args - func_args) 129 | ) 130 | 131 | # Check for duplicate arguments 132 | arg_occurrences = Counter(docstr_info.args) 133 | yield from ( 134 | types_.Problem(docstr_node.lineno, docstr_node.col_offset, DUPLICATE_ARG_MSG % arg) 135 | for arg, occurrences in arg_occurrences.items() 136 | if occurrences > 1 137 | ) 138 | 139 | # Check for empty args section 140 | if not all_used_args and len(docstr_info.args) == 0: 141 | yield types_.Problem( 142 | docstr_node.lineno, docstr_node.col_offset, ARGS_SECTION_IN_DOCSTR_MSG 143 | ) 144 | -------------------------------------------------------------------------------- /.github/workflows/ci-cd.yaml: -------------------------------------------------------------------------------- 1 | name: CI-CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "v[0-9]+.[0-9]+.[0-9]+" 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | jobs: 14 | constants: 15 | name: Constants 16 | runs-on: ubuntu-latest 17 | outputs: 18 | package_name: ${{ steps.output.outputs.package_name }} 19 | package_version: ${{ steps.output.outputs.package_version }} 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: actions/setup-python@v4 23 | with: 24 | python-version: '3.13' 25 | - id: output 26 | run: | 27 | echo package_name=$(python -c 'import tomllib;from pathlib import Path;print(tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))["tool"]["poetry"]["name"])') >> $GITHUB_OUTPUT 28 | echo package_version=$(python -c 'import tomllib;from pathlib import Path;print(tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))["tool"]["poetry"]["version"])') >> $GITHUB_OUTPUT 29 | lint: 30 | name: Lint 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v4 34 | - uses: actions/setup-python@v4 35 | with: 36 | python-version: '3.11' 37 | - name: Install tox 38 | run: python -m pip install tox 39 | - name: Run linting 40 | run: tox -e lint 41 | test: 42 | name: Test 43 | runs-on: ubuntu-latest 44 | strategy: 45 | matrix: 46 | python-version: 47 | - "3.9" 48 | - "3.10" 49 | - "3.11" 50 | - "3.12" 51 | - "3.13" 52 | env: 53 | - "test-flake85" 54 | - "test-flake86" 55 | - "test-flake87" 56 | steps: 57 | - uses: actions/checkout@v4 58 | - name: Set up Python ${{ matrix.python-version }} 59 | uses: actions/setup-python@v4 60 | with: 61 | python-version: ${{ matrix.python-version }} 62 | - name: Install tox 63 | run: python -m pip install tox 64 | - name: Run testing 65 | run: tox -e ${{ matrix.env }} 66 | tests-passed: 67 | name: Tests Passed 68 | runs-on: ubuntu-latest 69 | needs: 70 | - test 71 | steps: 72 | - run: echo tests passed 73 | release-test-pypi: 74 | runs-on: ubuntu-latest 75 | if: startsWith(github.ref, 'refs/tags/') 76 | needs: 77 | - test 78 | - lint 79 | steps: 80 | - uses: actions/checkout@v4 81 | - uses: actions/setup-python@v4 82 | with: 83 | python-version: '3.11' 84 | - name: Install poetry 85 | run: python -m pip install poetry 86 | - name: Publish 87 | run: | 88 | poetry config repositories.test-pypi https://test.pypi.org/legacy/ 89 | poetry publish --build -u __token__ -p ${{ secrets.test_pypi_password }} -r test-pypi 90 | test-release-test-pypi: 91 | runs-on: ubuntu-latest 92 | if: startsWith(github.ref, 'refs/tags/') 93 | needs: 94 | - release-test-pypi 95 | - constants 96 | strategy: 97 | matrix: 98 | python-version: 99 | - "3.9" 100 | - "3.10" 101 | - "3.11" 102 | - "3.12" 103 | - "3.13" 104 | steps: 105 | - name: Set up Python ${{ matrix.python-version }} 106 | uses: actions/setup-python@v4 107 | with: 108 | python-version: ${{ matrix.python-version }} 109 | - name: Run check 110 | run: | 111 | for i in 1 2 3 4 5; do python -m pip install flake8 ${{ needs.constants.outputs.package_name }}==${{ needs.constants.outputs.package_version }} --extra-index-url https://test.pypi.org/simple/ && break || sleep 10; done 112 | echo '"""Docstring."""' > source.py 113 | flake8 source.py 114 | release-pypi: 115 | runs-on: ubuntu-latest 116 | if: startsWith(github.ref, 'refs/tags/') 117 | needs: 118 | - test-release-test-pypi 119 | steps: 120 | - uses: actions/checkout@v4 121 | - uses: actions/setup-python@v4 122 | with: 123 | python-version: '3.11' 124 | - name: Install poetry 125 | run: python -m pip install poetry 126 | - name: Publish 127 | run: poetry publish --build -u __token__ -p ${{ secrets.pypi_password }} 128 | release-github: 129 | runs-on: ubuntu-latest 130 | if: startsWith(github.ref, 'refs/tags/') 131 | needs: 132 | - release-pypi 133 | steps: 134 | - name: Get version from tag 135 | id: tag_name 136 | run: | 137 | echo current_version=${GITHUB_REF#refs/tags/v} >> $GITHUB_OUTPUT 138 | shell: bash 139 | - uses: actions/checkout@v4 140 | - name: Get latest Changelog Entry 141 | id: changelog_reader 142 | uses: mindsers/changelog-reader-action@v2 143 | with: 144 | version: v${{ steps.tag_name.outputs.current_version }} 145 | path: ./CHANGELOG.md 146 | - name: Create Release 147 | id: create_release 148 | uses: actions/create-release@v1 149 | env: 150 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 151 | with: 152 | tag_name: ${{ steps.changelog_reader.outputs.version }} 153 | release_name: Release ${{ steps.changelog_reader.outputs.version }} 154 | body: ${{ steps.changelog_reader.outputs.changes }} 155 | prerelease: ${{ steps.changelog_reader.outputs.status == 'prereleased' }} 156 | draft: ${{ steps.changelog_reader.outputs.status == 'unreleased' }} 157 | test-release-pypi: 158 | runs-on: ubuntu-latest 159 | if: startsWith(github.ref, 'refs/tags/') 160 | needs: 161 | - release-pypi 162 | - constants 163 | strategy: 164 | matrix: 165 | python-version: 166 | - "3.9" 167 | - "3.10" 168 | - "3.11" 169 | - "3.12" 170 | - "3.13" 171 | steps: 172 | - name: Set up Python ${{ matrix.python-version }} 173 | uses: actions/setup-python@v4 174 | with: 175 | python-version: ${{ matrix.python-version }} 176 | - name: Run check 177 | run: | 178 | for i in 1 2 3 4 5; do python -m pip install flake8 ${{ needs.constants.outputs.package_name }}==${{ needs.constants.outputs.package_version }} && break || sleep 10; done 179 | echo '"""Docstring."""' > source.py 180 | flake8 source.py 181 | -------------------------------------------------------------------------------- /flake8_docstrings_complete/docstring.py: -------------------------------------------------------------------------------- 1 | """Parse a docstring to retrieve the sections and sub-sections.""" 2 | 3 | from __future__ import annotations 4 | 5 | import contextlib 6 | import itertools 7 | import re 8 | from typing import Iterable, Iterator, NamedTuple 9 | 10 | 11 | class _Section(NamedTuple): 12 | """Represents a docstring section. 13 | 14 | Attrs: 15 | name: Short description of the section. 16 | subs: The names of the sub-sections included in the section. None if the section has no 17 | sub-sections. 18 | """ 19 | 20 | name: str | None 21 | subs: tuple[str, ...] 22 | 23 | 24 | class Docstring(NamedTuple): 25 | """Represents a docstring. 26 | 27 | Attrs: 28 | args: The arguments described in the docstring. None if the docstring doesn't have the args 29 | section. 30 | args_sections: All the arguments sections. 31 | attrs: The attributes described in the docstring. None if the docstring doesn't have the 32 | attrs section. 33 | attrs_sections: All the attributes sections. 34 | returns_sections: All the returns sections. 35 | yields_sections: All the yields sections. 36 | raises: The exceptions described in the docstring. None if the docstring doesn't have the 37 | raises section. 38 | raises_sections: All the raises sections. 39 | """ 40 | 41 | args: tuple[str, ...] | None = None 42 | args_sections: tuple[str, ...] = () 43 | attrs: tuple[str, ...] | None = None 44 | attrs_sections: tuple[str, ...] = () 45 | returns_sections: tuple[str, ...] = () 46 | yields_sections: tuple[str, ...] = () 47 | raises: tuple[str, ...] | None = None 48 | raises_sections: tuple[str, ...] = () 49 | 50 | 51 | _SECTION_NAMES = { 52 | "args": {"args", "arguments", "parameters"}, 53 | "attrs": {"attributes", "attrs"}, 54 | "returns": {"return", "returns"}, 55 | "yields": {"yield", "yields"}, 56 | "raises": {"raises", "raise"}, 57 | } 58 | _WHITESPACE_REGEX = r"\s*" 59 | _SECTION_NAME_PATTERN = re.compile(rf"{_WHITESPACE_REGEX}(\w+):") 60 | _SUB_SECTION_PATTERN = re.compile(rf"{_WHITESPACE_REGEX}(\w+)( \(.*\))?:") 61 | _SECTION_END_PATTERN = re.compile(rf"{_WHITESPACE_REGEX}$") 62 | 63 | 64 | def _get_sections(lines: Iterable[str]) -> Iterator[_Section]: 65 | """Retrieve all the sectiond from the docstring. 66 | 67 | A section start is indicated by a line that starts with zero or more whitespace followed by a 68 | word and then a colon. 69 | A section end is indicated by a line with just whitespace or that there a no more lines in the 70 | docstring. 71 | A sub-section is indicated by a line with zero or more whitespace characters, followed by a 72 | word, optionally followed by arbitrary characters enclosed in brackets followed by a colon. 73 | 74 | Args: 75 | lines: The lines of the docstring. 76 | 77 | Yields: 78 | All the sections in the docstring. 79 | """ 80 | lines = iter(lines) 81 | 82 | with contextlib.suppress(StopIteration): 83 | while True: 84 | # Find the start of the next section 85 | section_start = next(line for line in lines if line.strip()) 86 | section_name_match = _SECTION_NAME_PATTERN.match(section_start) 87 | section_name = section_name_match.group(1) if section_name_match else None 88 | 89 | # Get all the lines of the section 90 | section_lines = itertools.takewhile( 91 | lambda line: _SECTION_END_PATTERN.match(line) is None, lines 92 | ) 93 | 94 | # Retrieve sub section from section lines 95 | sub_section_matches = (_SUB_SECTION_PATTERN.match(line) for line in section_lines) 96 | sub_sections = (match.group(1) for match in sub_section_matches if match is not None) 97 | 98 | yield _Section(name=section_name, subs=tuple(sub_sections)) 99 | 100 | 101 | def _get_section_by_name(name: str, sections: Iterable[_Section]) -> _Section | None: 102 | """Get the section by name. 103 | 104 | Args: 105 | name: The name of the section. 106 | sections: The sections to retrieve from. 107 | 108 | Returns: 109 | The section or None if it wasn't found. 110 | """ 111 | sections = iter(sections) 112 | return next( 113 | ( 114 | section 115 | for section in sections 116 | if section.name is not None and section.name.lower() in _SECTION_NAMES[name] 117 | ), 118 | None, 119 | ) 120 | 121 | 122 | def _get_all_section_names_by_name(name: str, sections: Iterable[_Section]) -> Iterator[str]: 123 | """Get all the section names in a docstring by name. 124 | 125 | Args: 126 | name: The name of the section. 127 | sections: The sections to retrieve from. 128 | 129 | Yields: 130 | The names of the sections that match the name. 131 | """ 132 | sections = iter(sections) 133 | yield from ( 134 | section.name 135 | for section in sections 136 | if section.name is not None and section.name.lower() in _SECTION_NAMES[name] 137 | ) 138 | 139 | 140 | def parse(value: str) -> Docstring: 141 | """Parse a docstring. 142 | 143 | Args: 144 | value: The docstring to parse. 145 | 146 | Returns: 147 | The parsed docstring. 148 | """ 149 | sections = list(_get_sections(lines=value.splitlines())) 150 | 151 | args_section = _get_section_by_name("args", sections) 152 | attrs_section = _get_section_by_name("attrs", sections) 153 | raises_section = _get_section_by_name("raises", sections) 154 | 155 | return Docstring( 156 | args=args_section.subs if args_section is not None else None, 157 | args_sections=tuple(_get_all_section_names_by_name(name="args", sections=sections)), 158 | attrs=attrs_section.subs if attrs_section is not None else None, 159 | attrs_sections=tuple(_get_all_section_names_by_name(name="attrs", sections=sections)), 160 | returns_sections=tuple(_get_all_section_names_by_name(name="returns", sections=sections)), 161 | yields_sections=tuple(_get_all_section_names_by_name(name="yields", sections=sections)), 162 | raises=raises_section.subs if raises_section is not None else None, 163 | raises_sections=tuple(_get_all_section_names_by_name(name="raises", sections=sections)), 164 | ) 165 | -------------------------------------------------------------------------------- /flake8_docstrings_complete/raises.py: -------------------------------------------------------------------------------- 1 | """The raises section checks.""" 2 | 3 | from __future__ import annotations 4 | 5 | import ast 6 | from collections import Counter 7 | from typing import Iterable, Iterator 8 | 9 | from . import docstring, types_ 10 | from .constants import ERROR_CODE_PREFIX, MORE_INFO_BASE 11 | 12 | RAISES_SECTION_NOT_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}050" 13 | RAISES_SECTION_NOT_IN_DOCSTR_MSG = ( 14 | f"{RAISES_SECTION_NOT_IN_DOCSTR_CODE} a function/ method that raises an exception should have " 15 | "the raises section in the docstring" 16 | f"{MORE_INFO_BASE}{RAISES_SECTION_NOT_IN_DOCSTR_CODE.lower()}" 17 | ) 18 | RAISES_SECTION_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}051" 19 | RAISES_SECTION_IN_DOCSTR_MSG = ( 20 | f"{RAISES_SECTION_IN_DOCSTR_CODE} a function/ method that does not raise an exception should " 21 | "not have the raises section in the docstring" 22 | f"{MORE_INFO_BASE}{RAISES_SECTION_IN_DOCSTR_CODE.lower()}" 23 | ) 24 | MULT_RAISES_SECTIONS_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}052" 25 | MULT_RAISES_SECTIONS_IN_DOCSTR_MSG = ( 26 | f"{MULT_RAISES_SECTIONS_IN_DOCSTR_CODE} a docstring should only contain a single raises " 27 | "section, found %s" 28 | f"{MORE_INFO_BASE}{MULT_RAISES_SECTIONS_IN_DOCSTR_CODE.lower()}" 29 | ) 30 | EXC_NOT_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}053" 31 | EXC_NOT_IN_DOCSTR_MSG = ( 32 | f'{EXC_NOT_IN_DOCSTR_CODE} "%s" exception should be described in the docstring{MORE_INFO_BASE}' 33 | f"{EXC_NOT_IN_DOCSTR_CODE.lower()}" 34 | ) 35 | EXC_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}054" 36 | EXC_IN_DOCSTR_MSG = ( 37 | f'{EXC_IN_DOCSTR_CODE} "%s" exception should not be described in the docstring{MORE_INFO_BASE}' 38 | f"{EXC_IN_DOCSTR_CODE.lower()}" 39 | ) 40 | RE_RAISE_NO_EXC_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}055" 41 | RE_RAISE_NO_EXC_IN_DOCSTR_MSG = ( 42 | f"{RE_RAISE_NO_EXC_IN_DOCSTR_CODE} a function/ method that re-raises exceptions should " 43 | "describe at least one exception in the raises section of the docstring" 44 | f"{MORE_INFO_BASE}{RE_RAISE_NO_EXC_IN_DOCSTR_CODE.lower()}" 45 | ) 46 | DUPLICATE_EXC_CODE = f"{ERROR_CODE_PREFIX}056" 47 | DUPLICATE_EXC_MSG = ( 48 | f'{DUPLICATE_EXC_CODE} "%s" exception documented multiple times{MORE_INFO_BASE}' 49 | f"{DUPLICATE_EXC_CODE.lower()}" 50 | ) 51 | 52 | 53 | def _get_exc_node(node: ast.Raise) -> types_.Node | None: 54 | """Get the exception value from raise. 55 | 56 | Args: 57 | node: The raise node. 58 | 59 | Returns: 60 | The exception node. 61 | """ 62 | if isinstance(node.exc, ast.Name): 63 | return types_.Node( 64 | name=node.exc.id, lineno=node.exc.lineno, col_offset=node.exc.col_offset 65 | ) 66 | if isinstance(node.exc, ast.Attribute): 67 | return types_.Node( 68 | name=node.exc.attr, lineno=node.exc.lineno, col_offset=node.exc.col_offset 69 | ) 70 | if isinstance(node.exc, ast.Call): 71 | if isinstance(node.exc.func, ast.Name): 72 | return types_.Node( 73 | name=node.exc.func.id, 74 | lineno=node.exc.func.lineno, 75 | col_offset=node.exc.func.col_offset, 76 | ) 77 | if isinstance(node.exc.func, ast.Attribute): 78 | return types_.Node( 79 | name=node.exc.func.attr, 80 | lineno=node.exc.func.lineno, 81 | col_offset=node.exc.func.col_offset, 82 | ) 83 | 84 | return None 85 | 86 | 87 | def check( 88 | docstr_info: docstring.Docstring, 89 | docstr_node: ast.Constant, 90 | raise_nodes: Iterable[ast.Raise], 91 | is_private: bool, 92 | ) -> Iterator[types_.Problem]: 93 | """Check that all raised exceptions arguments are described in the docstring. 94 | 95 | Check the function/ method has at most one raises section. 96 | Check that all raised exceptions of the function/ method are documented. 97 | Check that a function/ method that doesn't raise exceptions does not have a raises section. 98 | 99 | Args: 100 | docstr_info: Information about the docstring. 101 | docstr_node: The docstring node. 102 | raise_nodes: The raise nodes. 103 | is_private: If the function for the docstring is private. 104 | 105 | Yields: 106 | All the problems with exceptions. 107 | """ 108 | all_excs = list(_get_exc_node(node) for node in raise_nodes) 109 | has_raise_no_value = any(exc is None for exc in all_excs) 110 | all_raise_no_value = all(exc is None for exc in all_excs) 111 | 112 | # Check that raises section is in docstring if function/ method raises exceptions 113 | if all_excs and docstr_info.raises is None and not is_private: 114 | yield types_.Problem( 115 | docstr_node.lineno, docstr_node.col_offset, RAISES_SECTION_NOT_IN_DOCSTR_MSG 116 | ) 117 | # Check that raises section is not in docstring if function/ method raises no exceptions 118 | if not all_excs and docstr_info.raises is not None: 119 | yield types_.Problem( 120 | docstr_node.lineno, docstr_node.col_offset, RAISES_SECTION_IN_DOCSTR_MSG 121 | ) 122 | # Check for empty raises section 123 | if (all_excs and all_raise_no_value) and ( 124 | docstr_info.raises is None or len(docstr_info.raises) == 0 125 | ): 126 | yield types_.Problem( 127 | docstr_node.lineno, docstr_node.col_offset, RE_RAISE_NO_EXC_IN_DOCSTR_MSG 128 | ) 129 | 130 | # Checks for exceptions raised and raises section in docstring 131 | if all_excs and docstr_info.raises is not None: 132 | docstr_raises = set(docstr_info.raises) 133 | 134 | # Check for multiple raises sections 135 | if len(docstr_info.raises_sections) > 1: 136 | yield types_.Problem( 137 | docstr_node.lineno, 138 | docstr_node.col_offset, 139 | MULT_RAISES_SECTIONS_IN_DOCSTR_MSG % ",".join(docstr_info.raises_sections), 140 | ) 141 | 142 | # Check for exceptions that are not raised 143 | yield from ( 144 | types_.Problem(exc.lineno, exc.col_offset, EXC_NOT_IN_DOCSTR_MSG % exc.name) 145 | for exc in all_excs 146 | if exc and exc.name not in docstr_raises 147 | ) 148 | 149 | # Check for duplicate exceptions in raises 150 | exc_occurrences = Counter(docstr_info.raises) 151 | yield from ( 152 | types_.Problem(docstr_node.lineno, docstr_node.col_offset, DUPLICATE_EXC_MSG % exc) 153 | for exc, occurrences in exc_occurrences.items() 154 | if occurrences > 1 155 | ) 156 | 157 | # Check for exceptions in the docstring that are not raised unless function has a raises 158 | # without an exception 159 | if not has_raise_no_value: 160 | func_exc = set(exc.name for exc in all_excs if exc is not None) 161 | yield from ( 162 | types_.Problem(docstr_node.lineno, docstr_node.col_offset, EXC_IN_DOCSTR_MSG % exc) 163 | for exc in sorted(docstr_raises - func_exc) 164 | ) 165 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /flake8_docstrings_complete/attrs.py: -------------------------------------------------------------------------------- 1 | """The attributes section checks.""" 2 | 3 | from __future__ import annotations 4 | 5 | import ast 6 | from collections import Counter 7 | from itertools import chain 8 | from typing import Iterable, Iterator 9 | 10 | from . import docstring, types_ 11 | from .constants import ERROR_CODE_PREFIX, MORE_INFO_BASE 12 | 13 | ATTRS_SECTION_NOT_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}060" 14 | ATTRS_SECTION_NOT_IN_DOCSTR_MSG = ( 15 | f"{ATTRS_SECTION_NOT_IN_DOCSTR_CODE} a class with attributes should have the attributes " 16 | f"section in the docstring{MORE_INFO_BASE}{ATTRS_SECTION_NOT_IN_DOCSTR_CODE.lower()}" 17 | ) 18 | ATTRS_SECTION_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}061" 19 | ATTRS_SECTION_IN_DOCSTR_MSG = ( 20 | f"{ATTRS_SECTION_IN_DOCSTR_CODE} a function/ method without attributes should not have the " 21 | f"attributes section in the docstring{MORE_INFO_BASE}{ATTRS_SECTION_IN_DOCSTR_CODE.lower()}" 22 | ) 23 | MULT_ATTRS_SECTIONS_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}062" 24 | MULT_ATTRS_SECTIONS_IN_DOCSTR_MSG = ( 25 | f"{MULT_ATTRS_SECTIONS_IN_DOCSTR_CODE} a docstring should only contain a single attributes " 26 | f"section, found %s{MORE_INFO_BASE}{MULT_ATTRS_SECTIONS_IN_DOCSTR_CODE.lower()}" 27 | ) 28 | ATTR_NOT_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}063" 29 | ATTR_NOT_IN_DOCSTR_MSG = ( 30 | f'{ATTR_NOT_IN_DOCSTR_CODE} "%s" attribute/ property should be described in the docstring' 31 | f"{MORE_INFO_BASE}{ATTR_NOT_IN_DOCSTR_CODE.lower()}" 32 | ) 33 | ATTR_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}064" 34 | ATTR_IN_DOCSTR_MSG = ( 35 | f'{ATTR_IN_DOCSTR_CODE} "%s" attribute should not be described in the docstring' 36 | f"{MORE_INFO_BASE}{ATTR_IN_DOCSTR_CODE.lower()}" 37 | ) 38 | DUPLICATE_ATTR_CODE = f"{ERROR_CODE_PREFIX}065" 39 | DUPLICATE_ATTR_MSG = ( 40 | f'{DUPLICATE_ATTR_CODE} "%s" attribute documented multiple times{MORE_INFO_BASE}' 41 | f"{DUPLICATE_ATTR_CODE.lower()}" 42 | ) 43 | 44 | CLASS_SELF_CLS = {"self", "cls"} 45 | PRIVATE_ATTR_PREFIX = "_" 46 | 47 | 48 | def is_property_decorator(node: ast.expr) -> bool: 49 | """Determine whether an expression is a property decorator. 50 | 51 | Args: 52 | node: The node to check. 53 | 54 | Returns: 55 | Whether the node is a property decorator. 56 | """ 57 | if isinstance(node, ast.Name): 58 | return node.id in {"property", "cached_property"} 59 | 60 | # Handle call 61 | if isinstance(node, ast.Call): 62 | return is_property_decorator(node=node.func) 63 | 64 | # Handle attr 65 | if isinstance(node, ast.Attribute): 66 | value = node.value 67 | return ( 68 | node.attr == "cached_property" 69 | and isinstance(value, ast.Name) 70 | and value.id == "functools" 71 | ) 72 | 73 | # There is no valid syntax that gets to here 74 | return False # pragma: nocover 75 | 76 | 77 | def _get_class_target_name(target: ast.expr) -> ast.Name | None: 78 | """Get the name of the target for an assignment on the class. 79 | 80 | Args: 81 | target: The target node of an assignment expression. 82 | 83 | Returns: 84 | The Name node of the target. 85 | """ 86 | if isinstance(target, ast.Name): 87 | return target 88 | if isinstance(target, ast.Attribute): 89 | if isinstance(target.value, ast.Name): 90 | return target.value 91 | if isinstance(target.value, ast.Attribute): 92 | return _get_class_target_name(target=target.value) 93 | 94 | # There is no valid syntax that gets to here 95 | return None # pragma: nocover 96 | 97 | 98 | def _iter_class_attrs( 99 | nodes: Iterable[ast.Assign | ast.AnnAssign | ast.AugAssign | types_.Node], 100 | ) -> Iterator[types_.Node]: 101 | """Get the node of the variable being assigned at the class level if the target is a Name. 102 | 103 | Args: 104 | nodes: The assign nodes. 105 | 106 | Yields: 107 | All the nodes of name targets of the assignment expressions. 108 | """ 109 | for node in nodes: 110 | if isinstance(node, types_.Node): 111 | yield node 112 | elif isinstance(node, ast.Assign): 113 | target_names = filter( 114 | None, (_get_class_target_name(target) for target in node.targets) 115 | ) 116 | yield from ( 117 | types_.Node(lineno=name.lineno, col_offset=name.col_offset, name=name.id) 118 | for name in target_names 119 | ) 120 | else: 121 | target_name = _get_class_target_name(target=node.target) 122 | # No valid syntax reaches else 123 | if target_name is not None: # pragma: nobranch 124 | yield types_.Node( 125 | lineno=target_name.lineno, 126 | col_offset=target_name.col_offset, 127 | name=target_name.id, 128 | ) 129 | 130 | 131 | def _get_method_target_node(target: ast.expr) -> types_.Node | None: 132 | """Get the node of the target for an assignment in a method. 133 | 134 | Args: 135 | target: The target node of an assignment expression. 136 | 137 | Returns: 138 | The Name node of the target. 139 | """ 140 | if isinstance(target, ast.Attribute): 141 | if isinstance(target.value, ast.Name) and target.value.id in CLASS_SELF_CLS: 142 | return types_.Node( 143 | lineno=target.lineno, col_offset=target.col_offset, name=target.attr 144 | ) 145 | # No valid syntax reaches else 146 | if isinstance(target.value, ast.Attribute): # pragma: nobranch 147 | return _get_method_target_node(target=target.value) 148 | 149 | return None 150 | 151 | 152 | def _iter_method_attrs( 153 | nodes: Iterable[ast.Assign | ast.AnnAssign | ast.AugAssign], 154 | ) -> Iterator[types_.Node]: 155 | """Get the node of the class or instance variable being assigned in methods. 156 | 157 | Args: 158 | nodes: The assign nodes. 159 | 160 | Yields: 161 | All the nodes of name targets of the assignment expressions in methods. 162 | """ 163 | for node in nodes: 164 | if isinstance(node, ast.Assign): 165 | yield from filter(None, (_get_method_target_node(target) for target in node.targets)) 166 | else: 167 | target_node = _get_method_target_node(node.target) 168 | # No valid syntax reaches else 169 | if target_node is not None: # pragma: nobranch 170 | yield target_node 171 | 172 | 173 | def check( 174 | docstr_info: docstring.Docstring, 175 | docstr_node: ast.Constant, 176 | class_assign_nodes: Iterable[ast.Assign | ast.AnnAssign | ast.AugAssign | types_.Node], 177 | method_assign_nodes: Iterable[ast.Assign | ast.AnnAssign | ast.AugAssign], 178 | ) -> Iterator[types_.Problem]: 179 | """Check that all class attributes are described in the docstring. 180 | 181 | Check the class has at most one attrs section. 182 | Check that all attributes of the class are documented. 183 | Check that a class without attributes does not have an attrs section. 184 | 185 | Args: 186 | docstr_info: Information about the docstring. 187 | docstr_node: The docstring node. 188 | class_assign_nodes: The attributes of the class assigned in the class. 189 | method_assign_nodes: The attributes of the class assigned in the methods. 190 | 191 | Yields: 192 | All the problems with the attributes. 193 | """ 194 | all_class_targets = list(_iter_class_attrs(class_assign_nodes)) 195 | all_public_class_targets = list( 196 | target for target in all_class_targets if not target.name.startswith(PRIVATE_ATTR_PREFIX) 197 | ) 198 | all_targets = list(chain(all_class_targets, _iter_method_attrs(method_assign_nodes))) 199 | 200 | # Check that attrs section is in docstring if function/ method has public attributes 201 | if all_public_class_targets and docstr_info.attrs is None: 202 | yield types_.Problem( 203 | docstr_node.lineno, docstr_node.col_offset, ATTRS_SECTION_NOT_IN_DOCSTR_MSG 204 | ) 205 | 206 | # Check that attrs section is not in docstring if class has no attributes 207 | if not all_targets and docstr_info.attrs is not None: 208 | yield types_.Problem( 209 | docstr_node.lineno, docstr_node.col_offset, ATTRS_SECTION_IN_DOCSTR_MSG 210 | ) 211 | 212 | # Checks for class with attributes and an attrs section 213 | if all_targets and docstr_info.attrs is not None: 214 | docstr_attrs = set(docstr_info.attrs) 215 | 216 | # Check for multiple attrs sections 217 | if len(docstr_info.attrs_sections) > 1: 218 | yield types_.Problem( 219 | docstr_node.lineno, 220 | docstr_node.col_offset, 221 | MULT_ATTRS_SECTIONS_IN_DOCSTR_MSG % ",".join(docstr_info.attrs_sections), 222 | ) 223 | 224 | # Check for class attributes that are not in the docstring 225 | yield from ( 226 | types_.Problem(target.lineno, target.col_offset, ATTR_NOT_IN_DOCSTR_MSG % target.name) 227 | for target in all_public_class_targets 228 | if target.name not in docstr_attrs 229 | ) 230 | 231 | # Check for attributes in the docstring that are not class attributes 232 | class_attrs = set(target.name for target in all_targets) 233 | yield from ( 234 | types_.Problem(docstr_node.lineno, docstr_node.col_offset, ATTR_IN_DOCSTR_MSG % attr) 235 | for attr in sorted(docstr_attrs - class_attrs) 236 | ) 237 | 238 | # Check for duplicate attributes 239 | attr_occurrences = Counter(docstr_info.attrs) 240 | yield from ( 241 | types_.Problem(docstr_node.lineno, docstr_node.col_offset, DUPLICATE_ATTR_MSG % attr) 242 | for attr, occurrences in attr_occurrences.items() 243 | if occurrences > 1 244 | ) 245 | 246 | # Check for empty attrs section 247 | if not all_public_class_targets and len(docstr_info.attrs) == 0: 248 | yield types_.Problem( 249 | docstr_node.lineno, docstr_node.col_offset, ATTRS_SECTION_IN_DOCSTR_MSG 250 | ) 251 | 252 | 253 | class VisitorWithinClass(ast.NodeVisitor): 254 | """Visits AST nodes within a class but not nested class and functions nested within functions. 255 | 256 | Attrs: 257 | class_assign_nodes: All the return nodes encountered within the class. 258 | method_assign_nodes: All the return nodes encountered within the class methods. 259 | """ 260 | 261 | class_assign_nodes: list[ast.Assign | ast.AnnAssign | ast.AugAssign | types_.Node] 262 | method_assign_nodes: list[ast.Assign | ast.AnnAssign | ast.AugAssign] 263 | _visited_once: bool 264 | _visited_top_level: bool 265 | 266 | def __init__(self) -> None: 267 | """Construct.""" 268 | self.class_assign_nodes = [] 269 | self.method_assign_nodes = [] 270 | self._visited_once = False 271 | self._visited_top_level = False 272 | 273 | def visit_assign(self, node: ast.Assign | ast.AnnAssign | ast.AugAssign) -> None: 274 | """Record assign node. 275 | 276 | Args: 277 | node: The assign node to record. 278 | """ 279 | if not self._visited_top_level: 280 | self.class_assign_nodes.append(node) 281 | else: 282 | self.method_assign_nodes.append(node) 283 | 284 | # Ensure recursion continues 285 | self.generic_visit(node) 286 | 287 | def visit_any_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None: 288 | """Visit a function definition node. 289 | 290 | Args: 291 | node: The function definition to check. 292 | """ 293 | if any(is_property_decorator(decorator) for decorator in node.decorator_list): 294 | self.class_assign_nodes.append( 295 | types_.Node(lineno=node.lineno, col_offset=node.col_offset, name=node.name) 296 | ) 297 | self.visit_top_level(node=node) 298 | 299 | def visit_once(self, node: ast.AST) -> None: 300 | """Visit the node once and then skip. 301 | 302 | Args: 303 | node: The node being visited. 304 | """ 305 | if not self._visited_once: 306 | self._visited_once = True 307 | self.generic_visit(node=node) 308 | 309 | def visit_top_level(self, node: ast.AST) -> None: 310 | """Visit the top level node but skip any nested nodes. 311 | 312 | Args: 313 | node: The node being visited. 314 | """ 315 | if not self._visited_top_level: 316 | self._visited_top_level = True 317 | self.generic_visit(node=node) 318 | self._visited_top_level = False 319 | 320 | # The functions must be called the same as the name of the node 321 | # Visit assign nodes 322 | visit_Assign = visit_assign # noqa: N815,DCO063 323 | visit_AnnAssign = visit_assign # noqa: N815,DCO063 324 | visit_AugAssign = visit_assign # noqa: N815,DCO063 325 | # Ensure that nested functions and classes are not iterated over 326 | visit_FunctionDef = visit_any_function # noqa: N815,DCO063 327 | visit_AsyncFunctionDef = visit_any_function # noqa: N815,DCO063 328 | visit_ClassDef = visit_once # noqa: N815,DCO063 329 | -------------------------------------------------------------------------------- /tests/integration/test___init__.py: -------------------------------------------------------------------------------- 1 | """Integration tests for plugin.""" 2 | 3 | from __future__ import annotations 4 | 5 | import subprocess 6 | import sys 7 | from pathlib import Path 8 | 9 | import pytest 10 | 11 | from flake8_docstrings_complete import ( 12 | DOCSTR_MISSING_CODE, 13 | FIXTURE_DECORATOR_PATTERN_ARG_NAME, 14 | FIXTURE_DECORATOR_PATTERN_DEFAULT, 15 | FIXTURE_FILENAME_PATTERN_ARG_NAME, 16 | FIXTURE_FILENAME_PATTERN_DEFAULT, 17 | MULT_RETURNS_SECTIONS_IN_DOCSTR_CODE, 18 | MULT_YIELDS_SECTIONS_IN_DOCSTR_CODE, 19 | RETURNS_SECTION_IN_DOCSTR_CODE, 20 | RETURNS_SECTION_NOT_IN_DOCSTR_CODE, 21 | TEST_FILENAME_PATTERN_ARG_NAME, 22 | TEST_FILENAME_PATTERN_DEFAULT, 23 | TEST_FUNCTION_PATTERN_ARG_NAME, 24 | TEST_FUNCTION_PATTERN_DEFAULT, 25 | YIELDS_SECTION_IN_DOCSTR_CODE, 26 | YIELDS_SECTION_NOT_IN_DOCSTR_CODE, 27 | ) 28 | from flake8_docstrings_complete.args import ( 29 | ARG_IN_DOCSTR_CODE, 30 | ARG_NOT_IN_DOCSTR_CODE, 31 | ARGS_SECTION_IN_DOCSTR_CODE, 32 | ARGS_SECTION_NOT_IN_DOCSTR_CODE, 33 | ARGS_SECTION_NOT_IN_DOCSTR_MSG, 34 | DUPLICATE_ARG_CODE, 35 | MULT_ARGS_SECTIONS_IN_DOCSTR_CODE, 36 | ) 37 | from flake8_docstrings_complete.attrs import ( 38 | ATTR_IN_DOCSTR_CODE, 39 | ATTR_NOT_IN_DOCSTR_CODE, 40 | ATTRS_SECTION_IN_DOCSTR_CODE, 41 | ATTRS_SECTION_NOT_IN_DOCSTR_CODE, 42 | DUPLICATE_ATTR_CODE, 43 | MULT_ATTRS_SECTIONS_IN_DOCSTR_CODE, 44 | ) 45 | from flake8_docstrings_complete.raises import ( 46 | DUPLICATE_EXC_CODE, 47 | EXC_IN_DOCSTR_CODE, 48 | EXC_NOT_IN_DOCSTR_CODE, 49 | MULT_RAISES_SECTIONS_IN_DOCSTR_CODE, 50 | RAISES_SECTION_IN_DOCSTR_CODE, 51 | RAISES_SECTION_NOT_IN_DOCSTR_CODE, 52 | RE_RAISE_NO_EXC_IN_DOCSTR_CODE, 53 | ) 54 | 55 | 56 | def test_help(): 57 | """ 58 | given: linter 59 | when: the flake8 help message is generated 60 | then: plugin is registered with flake8 61 | """ 62 | with subprocess.Popen( 63 | f"{sys.executable} -m flake8 --help", 64 | stdout=subprocess.PIPE, 65 | shell=True, 66 | ) as proc: 67 | stdout = proc.communicate()[0].decode(encoding="utf-8") 68 | 69 | assert "flake8-docstrings-complete" in stdout 70 | assert TEST_FILENAME_PATTERN_ARG_NAME in stdout 71 | assert TEST_FILENAME_PATTERN_DEFAULT in stdout 72 | assert TEST_FUNCTION_PATTERN_ARG_NAME in stdout 73 | assert TEST_FUNCTION_PATTERN_DEFAULT in stdout 74 | assert FIXTURE_FILENAME_PATTERN_ARG_NAME in stdout 75 | assert FIXTURE_FILENAME_PATTERN_DEFAULT in stdout 76 | assert FIXTURE_DECORATOR_PATTERN_ARG_NAME in stdout 77 | assert FIXTURE_DECORATOR_PATTERN_DEFAULT in stdout 78 | 79 | 80 | def create_code_file(code: str, filename: str, base_path: Path) -> Path: 81 | """Create the code file with the given code. 82 | 83 | Args: 84 | code: The code to write to the file. 85 | filename: The name of the file to create. 86 | base_path: The path to create the file within 87 | 88 | Returns: 89 | The path to the code file. 90 | """ 91 | (code_file := base_path / filename).write_text(f'"""Docstring."""\n\n{code}') 92 | return code_file 93 | 94 | 95 | def test_fail(tmp_path: Path): 96 | """ 97 | given: file with Python code that fails the linting 98 | when: flake8 is run against the code 99 | then: the process exits with non-zero code and includes the error message 100 | """ 101 | code_file = create_code_file( 102 | '\ndef foo(arg_1):\n """Docstring."""\n', "source.py", tmp_path 103 | ) 104 | 105 | with subprocess.Popen( 106 | f"{sys.executable} -m flake8 {code_file}", 107 | stdout=subprocess.PIPE, 108 | shell=True, 109 | ) as proc: 110 | stdout = proc.communicate()[0].decode(encoding="utf-8") 111 | 112 | assert ARGS_SECTION_NOT_IN_DOCSTR_MSG in stdout 113 | assert proc.returncode 114 | 115 | 116 | @pytest.mark.parametrize( 117 | "code, filename, extra_args", 118 | [ 119 | pytest.param( 120 | ''' 121 | def foo(): 122 | """Docstring.""" 123 | ''', 124 | "source.py", 125 | "", 126 | id="default", 127 | ), 128 | pytest.param( 129 | ''' 130 | def _test(arg_1): 131 | """ 132 | arrange: line 1 133 | act: line 2 134 | assert: line 3 135 | """ 136 | ''', 137 | "_test.py", 138 | ( 139 | f"{TEST_FILENAME_PATTERN_ARG_NAME} .*_test\\.py " 140 | f"{TEST_FUNCTION_PATTERN_ARG_NAME} _test" 141 | ), 142 | id="custom test filename and function pattern", 143 | ), 144 | pytest.param( 145 | ''' 146 | def custom(): 147 | """Docstring.""" 148 | 149 | 150 | @custom 151 | def fixture(arg_1): 152 | """Docstring.""" 153 | ''', 154 | "fixture.py", 155 | ( 156 | f"{FIXTURE_FILENAME_PATTERN_ARG_NAME} fixture\\.py " 157 | f"{FIXTURE_DECORATOR_PATTERN_ARG_NAME} custom" 158 | ), 159 | id="custom fixture filename and function pattern", 160 | ), 161 | pytest.param( 162 | f""" 163 | def foo(): # noqa: {DOCSTR_MISSING_CODE} 164 | pass 165 | """, 166 | "source.py", 167 | "", 168 | id=f"{DOCSTR_MISSING_CODE} disabled", 169 | ), 170 | pytest.param( 171 | f''' 172 | def foo(arg_1): 173 | """Docstring.""" # noqa: {ARGS_SECTION_NOT_IN_DOCSTR_CODE} 174 | ''', 175 | "source.py", 176 | "", 177 | id=f"{ARGS_SECTION_NOT_IN_DOCSTR_CODE} disabled", 178 | ), 179 | pytest.param( 180 | f''' 181 | def foo(): 182 | """Docstring. 183 | 184 | Args: 185 | Arguments. 186 | """ # noqa: {ARGS_SECTION_IN_DOCSTR_CODE} 187 | ''', 188 | "source.py", 189 | "", 190 | id=f"{ARGS_SECTION_IN_DOCSTR_CODE} disabled", 191 | ), 192 | pytest.param( 193 | f''' 194 | def foo(arg_1): 195 | """Docstring. 196 | 197 | Args: 198 | arg_1: 199 | 200 | Parameters: 201 | arg_1: 202 | """ # noqa: {MULT_ARGS_SECTIONS_IN_DOCSTR_CODE} 203 | ''', 204 | "source.py", 205 | "", 206 | id=f"{MULT_ARGS_SECTIONS_IN_DOCSTR_CODE} disabled", 207 | ), 208 | pytest.param( 209 | f''' 210 | def foo(arg_1): # noqa: {ARG_NOT_IN_DOCSTR_CODE} 211 | """Docstring. 212 | 213 | Args: 214 | Arguments. 215 | """ 216 | ''', 217 | "source.py", 218 | "", 219 | id=f"{ARG_NOT_IN_DOCSTR_CODE} disabled", 220 | ), 221 | pytest.param( 222 | f''' 223 | def foo( 224 | arg_1, 225 | arg2, # noqa: {ARG_NOT_IN_DOCSTR_CODE} 226 | ): 227 | """Docstring. 228 | 229 | Args: 230 | arg_1: 231 | """ 232 | ''', 233 | "source.py", 234 | "", 235 | id=f"{ARG_NOT_IN_DOCSTR_CODE} disabled specific arg", 236 | ), 237 | pytest.param( 238 | f''' 239 | def foo(arg_1): 240 | """Docstring. 241 | 242 | Args: 243 | arg_1: 244 | arg_2: 245 | """ # noqa: {ARG_IN_DOCSTR_CODE} 246 | ''', 247 | "source.py", 248 | "", 249 | id=f"{ARG_IN_DOCSTR_CODE} disabled", 250 | ), 251 | pytest.param( 252 | f''' 253 | def foo(arg_1): 254 | """Docstring. 255 | 256 | Args: 257 | arg_1: 258 | arg_1: 259 | """ # noqa: {DUPLICATE_ARG_CODE} 260 | ''', 261 | "source.py", 262 | "", 263 | id=f"{DUPLICATE_ARG_CODE} disabled", 264 | ), 265 | pytest.param( 266 | f''' 267 | def foo(): 268 | """Docstring.""" 269 | return 1 # noqa: {RETURNS_SECTION_NOT_IN_DOCSTR_CODE} 270 | ''', 271 | "source.py", 272 | "", 273 | id=f"{RETURNS_SECTION_NOT_IN_DOCSTR_CODE} disabled", 274 | ), 275 | pytest.param( 276 | f''' 277 | def foo(): 278 | """Docstring. 279 | 280 | Returns: 281 | A value. 282 | """ # noqa: {RETURNS_SECTION_IN_DOCSTR_CODE} 283 | ''', 284 | "source.py", 285 | "", 286 | id=f"{RETURNS_SECTION_IN_DOCSTR_CODE} disabled", 287 | ), 288 | pytest.param( 289 | f''' 290 | def foo(): 291 | """Docstring. 292 | 293 | Returns: 294 | A value. 295 | 296 | Return: 297 | A value. 298 | """ # noqa: {MULT_RETURNS_SECTIONS_IN_DOCSTR_CODE} 299 | return 1 300 | ''', 301 | "source.py", 302 | "", 303 | id=f"{MULT_RETURNS_SECTIONS_IN_DOCSTR_CODE} disabled", 304 | ), 305 | pytest.param( 306 | f''' 307 | def foo(): 308 | """Docstring.""" 309 | yield 1 # noqa: {YIELDS_SECTION_NOT_IN_DOCSTR_CODE} 310 | ''', 311 | "source.py", 312 | "", 313 | id=f"{YIELDS_SECTION_NOT_IN_DOCSTR_CODE} disabled", 314 | ), 315 | pytest.param( 316 | f''' 317 | def foo(): 318 | """Docstring. 319 | 320 | Yields: 321 | A value. 322 | """ # noqa: {YIELDS_SECTION_IN_DOCSTR_CODE} 323 | ''', 324 | "source.py", 325 | "", 326 | id=f"{YIELDS_SECTION_IN_DOCSTR_CODE} disabled", 327 | ), 328 | pytest.param( 329 | f''' 330 | def foo(): 331 | """Docstring. 332 | 333 | Yields: 334 | A value. 335 | 336 | Yield: 337 | A value. 338 | """ # noqa: {MULT_YIELDS_SECTIONS_IN_DOCSTR_CODE} 339 | yield 1 340 | ''', 341 | "source.py", 342 | "", 343 | id=f"{MULT_YIELDS_SECTIONS_IN_DOCSTR_CODE} disabled", 344 | ), 345 | pytest.param( 346 | f''' 347 | class Exc1Error(Exception): 348 | """Docstring.""" 349 | 350 | 351 | def foo(): 352 | """Docstring.""" # noqa: {RAISES_SECTION_NOT_IN_DOCSTR_CODE} 353 | raise Exc1Error 354 | ''', 355 | "source.py", 356 | "", 357 | id=f"{RAISES_SECTION_NOT_IN_DOCSTR_CODE} disabled", 358 | ), 359 | pytest.param( 360 | f''' 361 | def foo(): 362 | """Docstring. 363 | 364 | Raises: 365 | Exc1:. 366 | """ # noqa: {RAISES_SECTION_IN_DOCSTR_CODE} 367 | ''', 368 | "source.py", 369 | "", 370 | id=f"{RAISES_SECTION_IN_DOCSTR_CODE} disabled", 371 | ), 372 | pytest.param( 373 | f''' 374 | class Exc1Error(Exception): 375 | """Docstring.""" 376 | 377 | 378 | def foo(): 379 | """Docstring. 380 | 381 | Raises: 382 | Exc1Error: 383 | 384 | Raise: 385 | Exc1Error: 386 | """ # noqa: {MULT_RAISES_SECTIONS_IN_DOCSTR_CODE} 387 | raise Exc1Error 388 | ''', 389 | "source.py", 390 | "", 391 | id=f"{MULT_RAISES_SECTIONS_IN_DOCSTR_CODE} disabled", 392 | ), 393 | pytest.param( 394 | f''' 395 | class Exc1Error(Exception): 396 | """Docstring.""" 397 | 398 | 399 | class Exc2Error(Exception): 400 | """Docstring.""" 401 | 402 | 403 | def foo(): 404 | """Docstring. 405 | 406 | Raises: 407 | Exc1Error:. 408 | """ 409 | raise Exc1Error 410 | raise Exc2Error # noqa: {EXC_NOT_IN_DOCSTR_CODE} 411 | ''', 412 | "source.py", 413 | "", 414 | id=f"{EXC_NOT_IN_DOCSTR_CODE} disabled", 415 | ), 416 | pytest.param( 417 | f''' 418 | class Exc1Error(Exception): 419 | """Docstring.""" 420 | 421 | 422 | def foo(): 423 | """Docstring. 424 | 425 | Raises: 426 | Exc1Error: 427 | Exc2Error: 428 | """ # noqa: {EXC_IN_DOCSTR_CODE} 429 | raise Exc1Error 430 | ''', 431 | "source.py", 432 | "", 433 | id=f"{EXC_IN_DOCSTR_CODE} disabled", 434 | ), 435 | pytest.param( 436 | f''' 437 | def foo(): 438 | """Docstring. 439 | 440 | Raises: 441 | """ # noqa: {RE_RAISE_NO_EXC_IN_DOCSTR_CODE},D414 442 | raise 443 | ''', 444 | "source.py", 445 | "", 446 | id=f"{RE_RAISE_NO_EXC_IN_DOCSTR_CODE} disabled", 447 | ), 448 | pytest.param( 449 | f''' 450 | class Exc1Error(Exception): 451 | """Docstring.""" 452 | 453 | 454 | def foo(): 455 | """Docstring. 456 | 457 | Raises: 458 | Exc1Error: 459 | Exc1Error: 460 | """ # noqa: {DUPLICATE_EXC_CODE} 461 | raise Exc1Error 462 | ''', 463 | "source.py", 464 | "", 465 | id=f"{DUPLICATE_EXC_CODE} disabled", 466 | ), 467 | pytest.param( 468 | f''' 469 | class Class1: 470 | """Docstring.""" # noqa: {ATTRS_SECTION_NOT_IN_DOCSTR_CODE} 471 | 472 | attr_1 = "value 1" 473 | ''', 474 | "source.py", 475 | "", 476 | id=f"{ATTRS_SECTION_NOT_IN_DOCSTR_CODE} disabled", 477 | ), 478 | pytest.param( 479 | f''' 480 | class Class1: 481 | """Docstring. 482 | 483 | Attrs: 484 | Attributes. 485 | """ # noqa: {ATTRS_SECTION_IN_DOCSTR_CODE} 486 | ''', 487 | "source.py", 488 | "", 489 | id=f"{ATTRS_SECTION_IN_DOCSTR_CODE} disabled", 490 | ), 491 | pytest.param( 492 | f''' 493 | class Class1: 494 | """Docstring. 495 | 496 | Attrs: 497 | attr_1: 498 | 499 | Attributes: 500 | attr_1: 501 | """ # noqa: {MULT_ATTRS_SECTIONS_IN_DOCSTR_CODE} 502 | 503 | attr_1 = "value 1" 504 | ''', 505 | "source.py", 506 | "", 507 | id=f"{MULT_ATTRS_SECTIONS_IN_DOCSTR_CODE} disabled", 508 | ), 509 | pytest.param( 510 | f''' 511 | class Class1: 512 | """Docstring. 513 | 514 | Attrs: 515 | Attributes. 516 | """ 517 | 518 | attr_1 = "value 1" # noqa: {ATTR_NOT_IN_DOCSTR_CODE} 519 | ''', 520 | "source.py", 521 | "", 522 | id=f"{ATTR_NOT_IN_DOCSTR_CODE} disabled", 523 | ), 524 | pytest.param( 525 | f''' 526 | class Class1: 527 | """Docstring. 528 | 529 | Attrs: 530 | attr_1: 531 | """ 532 | 533 | attr_1 = "value 1" 534 | attr_2 = "value 2" # noqa: {ATTR_NOT_IN_DOCSTR_CODE} 535 | ''', 536 | "source.py", 537 | "", 538 | id=f"{ATTR_NOT_IN_DOCSTR_CODE} disabled specific arg", 539 | ), 540 | pytest.param( 541 | f''' 542 | class Class1: 543 | """Docstring. 544 | 545 | Attrs: 546 | attr_1: 547 | attr_2: 548 | """ # noqa: {ATTR_IN_DOCSTR_CODE} 549 | 550 | attr_1 = "value 1" 551 | ''', 552 | "source.py", 553 | "", 554 | id=f"{ATTR_IN_DOCSTR_CODE} disabled", 555 | ), 556 | pytest.param( 557 | f''' 558 | class Class1: 559 | """Docstring. 560 | 561 | Attrs: 562 | attr_1: 563 | attr_1: 564 | """ # noqa: {DUPLICATE_ATTR_CODE} 565 | 566 | attr_1 = "value 1" 567 | ''', 568 | "source.py", 569 | "", 570 | id=f"{DUPLICATE_ATTR_CODE} disabled", 571 | ), 572 | ], 573 | ) 574 | def test_pass(code: str, filename: str, extra_args: str, tmp_path: Path): 575 | """ 576 | given: file with Python code that passes the linting 577 | when: flake8 is run against the code 578 | then: the process exits with zero code and empty stdout 579 | """ 580 | code_file = create_code_file(code, filename, tmp_path) 581 | (config_file := tmp_path / ".flake8").touch() 582 | 583 | with subprocess.Popen( 584 | ( 585 | f"{sys.executable} -m flake8 {code_file} {extra_args} --ignore D205,D400,D103 " 586 | f"--config {config_file}" 587 | ), 588 | stdout=subprocess.PIPE, 589 | shell=True, 590 | ) as proc: 591 | stdout = proc.communicate()[0].decode(encoding="utf-8") 592 | 593 | assert not stdout, stdout 594 | assert not proc.returncode 595 | 596 | 597 | def test_self(): 598 | """ 599 | given: working linter 600 | when: flake8 is run against the source and tests of the linter 601 | then: the process exits with zero code and empty stdout 602 | """ 603 | with subprocess.Popen( 604 | f"{sys.executable} -m flake8 flake8_docstrings_complete/ tests/ --ignore D205,D400,D103", 605 | stdout=subprocess.PIPE, 606 | shell=True, 607 | ) as proc: 608 | stdout = proc.communicate()[0].decode(encoding="utf-8") 609 | 610 | assert not stdout, stdout 611 | assert not proc.returncode 612 | -------------------------------------------------------------------------------- /tests/test_docstring.py: -------------------------------------------------------------------------------- 1 | """Tests for docstring module.""" 2 | 3 | from __future__ import annotations 4 | 5 | import pytest 6 | 7 | from flake8_docstrings_complete import docstring 8 | 9 | # Need access to protected functions for testing 10 | # pylint: disable=protected-access 11 | 12 | 13 | @pytest.mark.parametrize( 14 | "lines, expected_sections", 15 | [ 16 | pytest.param((), (), id="empty"), 17 | pytest.param(("",), (), id="single not a section"), 18 | pytest.param((" ",), (), id="single not a section whitespace"), 19 | pytest.param(("\t",), (), id="single not a section alternate whitespace"), 20 | pytest.param( 21 | ("line 1",), (docstring._Section(None, ()),), id="single line section no name" 22 | ), 23 | pytest.param( 24 | ("line 1", "line 2"), (docstring._Section(None, ()),), id="multi line section no name" 25 | ), 26 | pytest.param( 27 | ("line 1", "name_1:"), 28 | (docstring._Section(None, ("name_1",)),), 29 | id="multi line section no name second like name", 30 | ), 31 | pytest.param( 32 | ("line 1:",), 33 | (docstring._Section(None, ()),), 34 | id="single section no name colon after first word", 35 | ), 36 | pytest.param(("name_1:",), (docstring._Section("name_1", ()),), id="single section"), 37 | pytest.param( 38 | (" name_1:",), 39 | (docstring._Section("name_1", ()),), 40 | id="single section leading whitespace single space", 41 | ), 42 | pytest.param( 43 | ("\tname_1:",), 44 | (docstring._Section("name_1", ()),), 45 | id="single section leading whitespace single tab", 46 | ), 47 | pytest.param( 48 | (" name_1:",), 49 | (docstring._Section("name_1", ()),), 50 | id="single section leading whitespace multiple", 51 | ), 52 | pytest.param( 53 | ("name_1: ",), 54 | (docstring._Section("name_1", ()),), 55 | id="single section trailing whitespace", 56 | ), 57 | pytest.param( 58 | ("name_1: description",), 59 | (docstring._Section("name_1", ()),), 60 | id="single section trailing characters", 61 | ), 62 | pytest.param( 63 | ("name_1:", "description 1"), 64 | (docstring._Section("name_1", ()),), 65 | id="single section multiple lines", 66 | ), 67 | pytest.param( 68 | ("name_1:", "sub_name_1:"), 69 | (docstring._Section("name_1", ("sub_name_1",)),), 70 | id="single section single sub-section", 71 | ), 72 | pytest.param( 73 | ("name_1:", "sub_name_1 (text 1):"), 74 | (docstring._Section("name_1", ("sub_name_1",)),), 75 | id="single section single sub-section brackets", 76 | ), 77 | pytest.param( 78 | ("name_1:", " sub_name_1:"), 79 | (docstring._Section("name_1", ("sub_name_1",)),), 80 | id="single section single sub-section leading whitespace", 81 | ), 82 | pytest.param( 83 | ("name_1:", "sub_name_1: "), 84 | (docstring._Section("name_1", ("sub_name_1",)),), 85 | id="single section single sub-section trailing whitespace", 86 | ), 87 | pytest.param( 88 | ("name_1:", "sub_name_1: description 1"), 89 | (docstring._Section("name_1", ("sub_name_1",)),), 90 | id="single section single sub-section trailing characters", 91 | ), 92 | pytest.param( 93 | ("name_1:", "sub_name_1:", "description 1"), 94 | (docstring._Section("name_1", ("sub_name_1",)),), 95 | id="single section single sub-section other sub first", 96 | ), 97 | pytest.param( 98 | ("name_1:", "description 1", "sub_name_1:"), 99 | (docstring._Section("name_1", ("sub_name_1",)),), 100 | id="single section single sub-section other sub last", 101 | ), 102 | pytest.param( 103 | ("name_1:", "sub_name_1:", "sub_name_2:"), 104 | (docstring._Section("name_1", ("sub_name_1", "sub_name_2")),), 105 | id="single section multiple sub-sections", 106 | ), 107 | pytest.param( 108 | ("name_1:", "sub_name_1:", "sub_name_2:", "sub_name_3:"), 109 | (docstring._Section("name_1", ("sub_name_1", "sub_name_2", "sub_name_3")),), 110 | id="single section many sub-sections", 111 | ), 112 | pytest.param( 113 | ("name_1:", "description 1", "description 2"), 114 | (docstring._Section("name_1", ()),), 115 | id="single section many lines", 116 | ), 117 | pytest.param( 118 | ("name_1:", ""), (docstring._Section("name_1", ()),), id="single section separator" 119 | ), 120 | pytest.param( 121 | ("name_1:", "", "name_2:"), 122 | (docstring._Section("name_1", ()), docstring._Section("name_2", ())), 123 | id="multiple sections separator empty", 124 | ), 125 | pytest.param( 126 | ("name_1:", " ", "name_2:"), 127 | (docstring._Section("name_1", ()), docstring._Section("name_2", ())), 128 | id="multiple sections separator single space", 129 | ), 130 | pytest.param( 131 | ("name_1:", "\t", "name_2:"), 132 | (docstring._Section("name_1", ()), docstring._Section("name_2", ())), 133 | id="multiple sections separator single tab", 134 | ), 135 | pytest.param( 136 | ("name_1:", " ", "name_2:"), 137 | (docstring._Section("name_1", ()), docstring._Section("name_2", ())), 138 | id="multiple sections separator multiple whitespace", 139 | ), 140 | pytest.param( 141 | ("name_1:", "sub_name_1:", "", "name_2:"), 142 | (docstring._Section("name_1", ("sub_name_1",)), docstring._Section("name_2", ())), 143 | id="multiple sections first has sub-section", 144 | ), 145 | pytest.param( 146 | ("name_1:", "", "name_2:", "sub_name_1:"), 147 | (docstring._Section("name_1", ()), docstring._Section("name_2", ("sub_name_1",))), 148 | id="multiple sections last has sub-section", 149 | ), 150 | pytest.param( 151 | ("name_1:", "", "name_2:", "", "name_3:"), 152 | ( 153 | docstring._Section("name_1", ()), 154 | docstring._Section("name_2", ()), 155 | docstring._Section("name_3", ()), 156 | ), 157 | id="many sections", 158 | ), 159 | ], 160 | ) 161 | def test__get_sections( 162 | lines: tuple[()] | tuple[str, ...], 163 | expected_sections: tuple[()] | tuple[docstring._Section, ...], 164 | ): 165 | """ 166 | given: lines of a docstring 167 | when: _get_sections is called with the lines 168 | then: the expected sections are returned. 169 | """ 170 | assert isinstance(lines, tuple) 171 | assert isinstance(expected_sections, tuple) 172 | 173 | returned_sections = tuple(docstring._get_sections(lines=lines)) 174 | 175 | assert returned_sections == expected_sections 176 | 177 | 178 | @pytest.mark.parametrize( 179 | "value, expected_docstring", 180 | [ 181 | pytest.param("", docstring.Docstring(), id="empty"), 182 | pytest.param("short description", docstring.Docstring(), id="short description"), 183 | pytest.param( 184 | """short description 185 | 186 | long description""", 187 | docstring.Docstring(), 188 | id="short and long description", 189 | ), 190 | pytest.param( 191 | """short description 192 | 193 | Args: 194 | """, 195 | docstring.Docstring(args=(), args_sections=("Args",)), 196 | id="args empty", 197 | ), 198 | pytest.param( 199 | """short description 200 | 201 | Args: 202 | arg_1: 203 | """, 204 | docstring.Docstring(args=("arg_1",), args_sections=("Args",)), 205 | id="args single", 206 | ), 207 | pytest.param( 208 | """short description 209 | 210 | Args: 211 | arg_1: 212 | arg_2: 213 | """, 214 | docstring.Docstring(args=("arg_1", "arg_2"), args_sections=("Args",)), 215 | id="args multiple", 216 | ), 217 | pytest.param( 218 | """short description 219 | 220 | args: 221 | arg_1: 222 | """, 223 | docstring.Docstring(args=("arg_1",), args_sections=("args",)), 224 | id="args lower case", 225 | ), 226 | pytest.param( 227 | """short description 228 | 229 | Arguments: 230 | arg_1: 231 | """, 232 | docstring.Docstring(args=("arg_1",), args_sections=("Arguments",)), 233 | id="args alternate Arguments", 234 | ), 235 | pytest.param( 236 | """short description 237 | 238 | Parameters: 239 | arg_1: 240 | """, 241 | docstring.Docstring(args=("arg_1",), args_sections=("Parameters",)), 242 | id="args alternate Parameters", 243 | ), 244 | pytest.param( 245 | """short description 246 | 247 | Args: 248 | arg_1: 249 | 250 | Parameters: 251 | arg_2: 252 | """, 253 | docstring.Docstring(args=("arg_1",), args_sections=("Args", "Parameters")), 254 | id="args multiple sections", 255 | ), 256 | pytest.param( 257 | """short description 258 | 259 | Attrs: 260 | """, 261 | docstring.Docstring(attrs=(), attrs_sections=("Attrs",)), 262 | id="attrs empty", 263 | ), 264 | pytest.param( 265 | """short description 266 | 267 | Attrs: 268 | 269 | Attributes: 270 | """, 271 | docstring.Docstring(attrs=(), attrs_sections=("Attrs", "Attributes")), 272 | id="multiple attrs empty", 273 | ), 274 | pytest.param( 275 | """short description 276 | 277 | Attrs: 278 | 279 | Attrs: 280 | """, 281 | docstring.Docstring(attrs=(), attrs_sections=("Attrs", "Attrs")), 282 | id="multiple attrs alternate empty", 283 | ), 284 | pytest.param( 285 | """short description 286 | 287 | Attrs: 288 | attr_1: 289 | """, 290 | docstring.Docstring(attrs=("attr_1",), attrs_sections=("Attrs",)), 291 | id="attrs single", 292 | ), 293 | pytest.param( 294 | """short description 295 | 296 | Attrs: 297 | attr_1: 298 | attr_2: 299 | """, 300 | docstring.Docstring(attrs=("attr_1", "attr_2"), attrs_sections=("Attrs",)), 301 | id="attrs multiple", 302 | ), 303 | pytest.param( 304 | """short description 305 | 306 | attrs: 307 | attr_1: 308 | """, 309 | docstring.Docstring(attrs=("attr_1",), attrs_sections=("attrs",)), 310 | id="attrs lower case", 311 | ), 312 | pytest.param( 313 | """short description 314 | 315 | Attributes: 316 | attr_1: 317 | """, 318 | docstring.Docstring(attrs=("attr_1",), attrs_sections=("Attributes",)), 319 | id="attrs alternate Attributes", 320 | ), 321 | pytest.param( 322 | """short description 323 | 324 | Returns: 325 | """, 326 | docstring.Docstring(returns_sections=("Returns",)), 327 | id="returns empty", 328 | ), 329 | pytest.param( 330 | """short description 331 | 332 | Returns: 333 | The return value. 334 | """, 335 | docstring.Docstring(returns_sections=("Returns",)), 336 | id="returns single line", 337 | ), 338 | pytest.param( 339 | """short description 340 | 341 | Return: 342 | """, 343 | docstring.Docstring(returns_sections=("Return",)), 344 | id="returns alternate", 345 | ), 346 | pytest.param( 347 | """short description 348 | 349 | Returns: 350 | 351 | Returns: 352 | """, 353 | docstring.Docstring(returns_sections=("Returns", "Returns")), 354 | id="multiple returns", 355 | ), 356 | pytest.param( 357 | """short description 358 | 359 | Returns: 360 | 361 | Return: 362 | """, 363 | docstring.Docstring(returns_sections=("Returns", "Return")), 364 | id="multiple returns alternate", 365 | ), 366 | pytest.param( 367 | """short description 368 | 369 | Yields: 370 | """, 371 | docstring.Docstring(yields_sections=("Yields",)), 372 | id="yields empty", 373 | ), 374 | pytest.param( 375 | """short description 376 | 377 | Yields: 378 | The return value. 379 | """, 380 | docstring.Docstring(yields_sections=("Yields",)), 381 | id="yields single line", 382 | ), 383 | pytest.param( 384 | """short description 385 | 386 | Yield: 387 | """, 388 | docstring.Docstring(yields_sections=("Yield",)), 389 | id="yields alternate", 390 | ), 391 | pytest.param( 392 | """short description 393 | 394 | Yields: 395 | 396 | Yields: 397 | """, 398 | docstring.Docstring(yields_sections=("Yields", "Yields")), 399 | id="multiple yields", 400 | ), 401 | pytest.param( 402 | """short description 403 | 404 | Yields: 405 | 406 | Yield: 407 | """, 408 | docstring.Docstring(yields_sections=("Yields", "Yield")), 409 | id="multiple yields alternate", 410 | ), 411 | pytest.param( 412 | """short description 413 | 414 | Raises: 415 | """, 416 | docstring.Docstring(raises=(), raises_sections=("Raises",)), 417 | id="raises empty", 418 | ), 419 | pytest.param( 420 | """short description 421 | 422 | Raises: 423 | 424 | Raises: 425 | """, 426 | docstring.Docstring(raises=(), raises_sections=("Raises", "Raises")), 427 | id="raises empty multiple", 428 | ), 429 | pytest.param( 430 | """short description 431 | 432 | Raises: 433 | 434 | Raise: 435 | """, 436 | docstring.Docstring(raises=(), raises_sections=("Raises", "Raise")), 437 | id="raises empty multiple alternate", 438 | ), 439 | pytest.param( 440 | """short description 441 | 442 | Raises: 443 | """, 444 | docstring.Docstring(raises=(), raises_sections=("Raises",)), 445 | id="raises empty multiple", 446 | ), 447 | pytest.param( 448 | """short description 449 | 450 | Raises: 451 | exc_1: 452 | """, 453 | docstring.Docstring(raises=("exc_1",), raises_sections=("Raises",)), 454 | id="raises single", 455 | ), 456 | pytest.param( 457 | """short description 458 | 459 | Raises: 460 | exc_1: 461 | exc_2: 462 | """, 463 | docstring.Docstring(raises=("exc_1", "exc_2"), raises_sections=("Raises",)), 464 | id="raises multiple", 465 | ), 466 | pytest.param( 467 | """short description 468 | 469 | raises: 470 | exc_1: 471 | """, 472 | docstring.Docstring(raises=("exc_1",), raises_sections=("raises",)), 473 | id="raises lower case", 474 | ), 475 | pytest.param( 476 | """short description 477 | 478 | Attrs: 479 | attr_1: 480 | 481 | Args: 482 | arg_1: 483 | 484 | Returns: 485 | The return value. 486 | 487 | Yields: 488 | The yield value. 489 | 490 | Raises: 491 | exc_1: 492 | """, 493 | docstring.Docstring( 494 | args=("arg_1",), 495 | args_sections=("Args",), 496 | attrs=("attr_1",), 497 | attrs_sections=("Attrs",), 498 | returns_sections=("Returns",), 499 | yields_sections=("Yields",), 500 | raises=("exc_1",), 501 | raises_sections=("Raises",), 502 | ), 503 | id="all defined", 504 | ), 505 | ], 506 | ) 507 | def test_parse(value: str, expected_docstring: docstring.Docstring): 508 | """ 509 | given: docstring value 510 | when: parse is called with the docstring 511 | then: the expected docstring information is returned. 512 | """ 513 | returned_docstring = docstring.parse(value=value) 514 | 515 | assert returned_docstring == expected_docstring 516 | -------------------------------------------------------------------------------- /tests/unit/test___init__raises.py: -------------------------------------------------------------------------------- 1 | """Unit tests for raises checks in the plugin.""" 2 | 3 | from __future__ import annotations 4 | 5 | import pytest 6 | 7 | from flake8_docstrings_complete.raises import ( 8 | DUPLICATE_EXC_MSG, 9 | EXC_IN_DOCSTR_MSG, 10 | EXC_NOT_IN_DOCSTR_MSG, 11 | MULT_RAISES_SECTIONS_IN_DOCSTR_MSG, 12 | RAISES_SECTION_IN_DOCSTR_MSG, 13 | RAISES_SECTION_NOT_IN_DOCSTR_MSG, 14 | RE_RAISE_NO_EXC_IN_DOCSTR_MSG, 15 | ) 16 | 17 | from . import result 18 | 19 | 20 | @pytest.mark.parametrize( 21 | "code, expected_result", 22 | [ 23 | pytest.param( 24 | ''' 25 | def function_1(): 26 | """Docstring 1.""" 27 | raise Exc1 28 | ''', 29 | (f"3:4 {RAISES_SECTION_NOT_IN_DOCSTR_MSG}",), 30 | id="function raises single exc docstring no raises section", 31 | ), 32 | pytest.param( 33 | ''' 34 | def _function_1(): 35 | """Docstring 1.""" 36 | raise Exc1 37 | ''', 38 | (), 39 | id="private function raises single exc docstring no raises section", 40 | ), 41 | pytest.param( 42 | ''' 43 | def function_1(): 44 | """Docstring 1.""" 45 | raise Exc1 46 | 47 | def function_2(): 48 | """Docstring 2.""" 49 | raise Exc2 50 | ''', 51 | ( 52 | f"3:4 {RAISES_SECTION_NOT_IN_DOCSTR_MSG}", 53 | f"7:4 {RAISES_SECTION_NOT_IN_DOCSTR_MSG}", 54 | ), 55 | id="multiple function raises single exc docstring no raises section", 56 | ), 57 | pytest.param( 58 | ''' 59 | def function_1(): 60 | """Docstring 1. 61 | 62 | Raises: 63 | """ 64 | ''', 65 | (f"3:4 {RAISES_SECTION_IN_DOCSTR_MSG}",), 66 | id="function raises no exc docstring raises section", 67 | ), 68 | pytest.param( 69 | ''' 70 | def _function_1(): 71 | """Docstring 1. 72 | 73 | Raises: 74 | """ 75 | ''', 76 | (f"3:4 {RAISES_SECTION_IN_DOCSTR_MSG}",), 77 | id="private function raises no exc docstring raises section", 78 | ), 79 | pytest.param( 80 | ''' 81 | def function_1(): 82 | """Docstring 1. 83 | 84 | Raises: 85 | Exc1: 86 | 87 | Raises: 88 | Exc1: 89 | """ 90 | raise Exc1 91 | ''', 92 | (f"3:4 {MULT_RAISES_SECTIONS_IN_DOCSTR_MSG % 'Raises,Raises'}",), 93 | id="function raises single excs docstring multiple raises sections same name", 94 | ), 95 | pytest.param( 96 | ''' 97 | def function_1(): 98 | """Docstring 1. 99 | 100 | Raises: 101 | Exc1: 102 | 103 | Raise: 104 | Exc1: 105 | """ 106 | raise Exc1 107 | ''', 108 | (f"3:4 {MULT_RAISES_SECTIONS_IN_DOCSTR_MSG % 'Raises,Raise'}",), 109 | id="function raises single excs docstring multiple raises sections different name", 110 | ), 111 | pytest.param( 112 | ''' 113 | def function_1(): 114 | """Docstring 1. 115 | 116 | Raises: 117 | """ 118 | raise Exc1 119 | ''', 120 | (f"7:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}",), 121 | id="function raises single exc docstring no exc", 122 | ), 123 | pytest.param( 124 | ''' 125 | def function_1(): 126 | """Docstring 1. 127 | 128 | Raises: 129 | """ 130 | raise Exc1 131 | raise 132 | ''', 133 | (f"7:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}",), 134 | id="function raises single exc and single no exc docstring no exc", 135 | ), 136 | pytest.param( 137 | ''' 138 | def function_1(): 139 | """Docstring 1. 140 | 141 | Raises: 142 | """ 143 | raise Exc1() 144 | ''', 145 | (f"7:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}",), 146 | id="function raises single exc call docstring no exc", 147 | ), 148 | pytest.param( 149 | ''' 150 | def function_1(): 151 | """Docstring 1. 152 | 153 | Raises: 154 | """ 155 | raise module.Exc1 156 | ''', 157 | (f"7:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}",), 158 | id="function raises single nested exc docstring no exc", 159 | ), 160 | pytest.param( 161 | ''' 162 | async def function_1(): 163 | """Docstring 1. 164 | 165 | Raises: 166 | """ 167 | raise Exc1 168 | ''', 169 | (f"7:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}",), 170 | id="async function raises single exc docstring no exc", 171 | ), 172 | pytest.param( 173 | ''' 174 | def function_1(): 175 | """Docstring 1. 176 | 177 | Raises: 178 | """ 179 | raise Exc1 180 | raise Exc2 181 | ''', 182 | (f"7:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}", f"8:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc2'}"), 183 | id="function multiple excs docstring no exc", 184 | ), 185 | pytest.param( 186 | ''' 187 | def function_1(): 188 | """Docstring 1. 189 | 190 | Raises: 191 | """ 192 | def function_2(): 193 | """Docstring 2. 194 | 195 | Raises: 196 | Exc1: 197 | """ 198 | raise Exc1 199 | raise Exc2 200 | ''', 201 | (f"14:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc2'}",), 202 | id="function multiple excs first nested function", 203 | ), 204 | pytest.param( 205 | ''' 206 | def function_1(): 207 | """Docstring 1. 208 | 209 | Raises: 210 | """ 211 | async def function_2(): 212 | """Docstring 2. 213 | 214 | Raises: 215 | Exc1: 216 | """ 217 | raise Exc1 218 | raise Exc2 219 | ''', 220 | (f"14:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc2'}",), 221 | id="function multiple excs first nested async function", 222 | ), 223 | pytest.param( 224 | ''' 225 | def function_1(): 226 | """Docstring 1. 227 | 228 | Raises: 229 | """ 230 | class Class1: 231 | """Docstring 2.""" 232 | raise Exc1 233 | raise Exc2 234 | ''', 235 | (f"10:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc2'}",), 236 | id="function multiple excs first nested class", 237 | ), 238 | pytest.param( 239 | ''' 240 | def function_1(): 241 | """Docstring 1. 242 | 243 | Raises: 244 | """ 245 | raise Exc1 246 | def function_2(): 247 | """Docstring 2. 248 | 249 | Raises: 250 | Exc2: 251 | """ 252 | raise Exc2 253 | ''', 254 | (f"7:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}",), 255 | id="function multiple excs second nested function", 256 | ), 257 | pytest.param( 258 | ''' 259 | def function_1(): 260 | """Docstring 1. 261 | 262 | Raises: 263 | Exc1: 264 | """ 265 | raise Exc1 266 | raise Exc2 267 | ''', 268 | (f"9:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc2'}",), 269 | id="function multiple excs docstring single exc first", 270 | ), 271 | pytest.param( 272 | ''' 273 | def function_1(): 274 | """Docstring 1. 275 | 276 | Raises: 277 | Exc2: 278 | """ 279 | raise Exc1 280 | raise Exc2 281 | ''', 282 | (f"8:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}",), 283 | id="function multiple excs docstring single exc second", 284 | ), 285 | pytest.param( 286 | ''' 287 | def function_1(): 288 | """Docstring 1. 289 | 290 | Raises: 291 | Exc2: 292 | """ 293 | raise Exc1 294 | ''', 295 | ( 296 | f"8:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}", 297 | f"3:4 {EXC_IN_DOCSTR_MSG % 'Exc2'}", 298 | ), 299 | id="function raises single exc docstring exc different", 300 | ), 301 | pytest.param( 302 | ''' 303 | def function_1(): 304 | """Docstring 1. 305 | 306 | Raises: 307 | Exc2: 308 | Exc3: 309 | """ 310 | raise Exc1 311 | ''', 312 | ( 313 | f"9:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}", 314 | f"3:4 {EXC_IN_DOCSTR_MSG % 'Exc2'}", 315 | f"3:4 {EXC_IN_DOCSTR_MSG % 'Exc3'}", 316 | ), 317 | id="function single exc docstring multiple exc different", 318 | ), 319 | pytest.param( 320 | ''' 321 | def function_1(): 322 | """Docstring 1. 323 | 324 | Raises: 325 | Exc3: 326 | Exc4: 327 | """ 328 | raise Exc1 329 | raise Exc2 330 | ''', 331 | ( 332 | f"9:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}", 333 | f"10:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc2'}", 334 | f"3:4 {EXC_IN_DOCSTR_MSG % 'Exc3'}", 335 | f"3:4 {EXC_IN_DOCSTR_MSG % 'Exc4'}", 336 | ), 337 | id="function multiple exc docstring multiple exc different", 338 | ), 339 | pytest.param( 340 | ''' 341 | def function_1(): 342 | """Docstring 1. 343 | 344 | Raises: 345 | Exc3: 346 | Exc2: 347 | """ 348 | raise Exc1 349 | raise Exc2 350 | ''', 351 | (f"9:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}", f"3:4 {EXC_IN_DOCSTR_MSG % 'Exc3'}"), 352 | id="function multiple exc docstring multiple exc first different", 353 | ), 354 | pytest.param( 355 | ''' 356 | def function_1(): 357 | """Docstring 1. 358 | 359 | Raises: 360 | Exc1: 361 | Exc3: 362 | """ 363 | raise Exc1 364 | raise Exc2 365 | ''', 366 | (f"10:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc2'}", f"3:4 {EXC_IN_DOCSTR_MSG % 'Exc3'}"), 367 | id="function multiple exc docstring multiple exc last different", 368 | ), 369 | pytest.param( 370 | ''' 371 | def function_1(): 372 | """Docstring 1.""" 373 | raise 374 | ''', 375 | ( 376 | f"3:4 {RAISES_SECTION_NOT_IN_DOCSTR_MSG}", 377 | f"3:4 {RE_RAISE_NO_EXC_IN_DOCSTR_MSG}", 378 | ), 379 | id="function single raise no exc docstring no raises exc", 380 | ), 381 | pytest.param( 382 | ''' 383 | class Class1: 384 | """Docstring.""" 385 | def function_1(self): 386 | """Docstring 1.""" 387 | raise 388 | ''', 389 | ( 390 | f"5:8 {RAISES_SECTION_NOT_IN_DOCSTR_MSG}", 391 | f"5:8 {RE_RAISE_NO_EXC_IN_DOCSTR_MSG}", 392 | ), 393 | id="method raise no exc docstring no raises", 394 | ), 395 | pytest.param( 396 | ''' 397 | def function_1(): 398 | """Docstring 1. 399 | 400 | Raises: 401 | """ 402 | raise 403 | ''', 404 | (f"3:4 {RE_RAISE_NO_EXC_IN_DOCSTR_MSG}",), 405 | id="function raise no exc docstring raises empty", 406 | ), 407 | pytest.param( 408 | ''' 409 | def function_1(): 410 | """Docstring 1. 411 | 412 | Raises: 413 | Exc1: 414 | Exc1: 415 | """ 416 | raise Exc1 417 | ''', 418 | (f"3:4 {DUPLICATE_EXC_MSG}" % "Exc1",), 419 | id="function single raise docstring raises duplicate", 420 | ), 421 | pytest.param( 422 | ''' 423 | def function_1(): 424 | """Docstring 1. 425 | 426 | Raises: 427 | Exc1: 428 | Exc1: 429 | Exc1: 430 | """ 431 | raise Exc1 432 | ''', 433 | (f"3:4 {DUPLICATE_EXC_MSG}" % "Exc1",), 434 | id="function single raise docstring raises duplicate many", 435 | ), 436 | pytest.param( 437 | ''' 438 | def function_1(): 439 | """Docstring 1. 440 | 441 | Raises: 442 | Exc1: 443 | Exc1: 444 | Exc2: 445 | """ 446 | raise Exc1 447 | raise Exc2 448 | ''', 449 | (f"3:4 {DUPLICATE_EXC_MSG}" % "Exc1",), 450 | id="function multiple raise docstring raises duplicate first", 451 | ), 452 | pytest.param( 453 | ''' 454 | def function_1(): 455 | """Docstring 1. 456 | 457 | Raises: 458 | Exc1: 459 | Exc2: 460 | Exc2: 461 | """ 462 | raise Exc1 463 | raise Exc2 464 | ''', 465 | (f"3:4 {DUPLICATE_EXC_MSG}" % "Exc2",), 466 | id="function multiple raise docstring raises duplicate second", 467 | ), 468 | pytest.param( 469 | ''' 470 | def function_1(): 471 | """Docstring 1. 472 | 473 | Raises: 474 | Exc1: 475 | Exc1: 476 | Exc2: 477 | Exc2: 478 | """ 479 | raise Exc1 480 | raise Exc2 481 | ''', 482 | ( 483 | f"3:4 {DUPLICATE_EXC_MSG}" % "Exc1", 484 | f"3:4 {DUPLICATE_EXC_MSG}" % "Exc2", 485 | ), 486 | id="function multiple raise docstring raises duplicate all", 487 | ), 488 | pytest.param( 489 | ''' 490 | def function_1(): 491 | """Docstring 1. 492 | 493 | Raises: 494 | Exc1: 495 | """ 496 | raise 497 | ''', 498 | (), 499 | id="function single raise no exc docstring raises exc", 500 | ), 501 | pytest.param( 502 | ''' 503 | def _function_1(): 504 | """Docstring 1. 505 | 506 | Raises: 507 | Exc1: 508 | """ 509 | raise 510 | ''', 511 | (), 512 | id="private function single raise no exc docstring raises exc", 513 | ), 514 | pytest.param( 515 | ''' 516 | def function_1(): 517 | """Docstring 1. 518 | 519 | Raises: 520 | Exc1: 521 | """ 522 | raise Exc1 523 | ''', 524 | (), 525 | id="function single raise exc docstring raises", 526 | ), 527 | pytest.param( 528 | ''' 529 | def function_1(): 530 | """Docstring 1.""" 531 | def function_2(): 532 | """Docstring 2. 533 | 534 | Raises: 535 | Exc1: 536 | """ 537 | raise Exc1 538 | ''', 539 | (), 540 | id="function single nested function exc docstring no raises", 541 | ), 542 | pytest.param( 543 | ''' 544 | def function_1(): 545 | """Docstring 1.""" 546 | async def function_2(): 547 | """Docstring 2. 548 | 549 | Raises: 550 | Exc1: 551 | """ 552 | raise Exc1 553 | ''', 554 | (), 555 | id="function single nested async function exc docstring no raises", 556 | ), 557 | pytest.param( 558 | ''' 559 | def function_1(): 560 | """Docstring 1.""" 561 | class Class1: 562 | """Docstring 2.""" 563 | raise Exc1 564 | ''', 565 | (), 566 | id="function single nested class exc docstring no raises", 567 | ), 568 | pytest.param( 569 | ''' 570 | def function_1(): 571 | """Docstring 1. 572 | 573 | Raises: 574 | Exc1: 575 | """ 576 | raise Exc1() 577 | ''', 578 | (), 579 | id="function single exc call docstring single exc", 580 | ), 581 | pytest.param( 582 | ''' 583 | def function_1(): 584 | """Docstring 1. 585 | 586 | Raises: 587 | Exc1: 588 | """ 589 | raise (lambda: True)() 590 | ''', 591 | (), 592 | id="function single exc lambda docstring single exc", 593 | ), 594 | pytest.param( 595 | ''' 596 | def function_1(): 597 | """Docstring 1. 598 | 599 | Raises: 600 | Exc1: 601 | """ 602 | raise module.Exc1 603 | ''', 604 | (), 605 | id="function single exc attribute docstring single exc", 606 | ), 607 | pytest.param( 608 | ''' 609 | def function_1(): 610 | """Docstring 1. 611 | 612 | Raises: 613 | Exc1: 614 | """ 615 | raise module.Exc1() 616 | ''', 617 | (), 618 | id="function single exc attribute call docstring single exc", 619 | ), 620 | pytest.param( 621 | ''' 622 | def function_1(): 623 | """Docstring 1. 624 | 625 | Raises: 626 | Exc1: 627 | Exc2: 628 | """ 629 | raise Exc1 630 | raise Exc2 631 | ''', 632 | (), 633 | id="function multiple exc docstring multiple exc", 634 | ), 635 | pytest.param( 636 | ''' 637 | class Class1: 638 | """Docstring.""" 639 | def function_1(self): 640 | """Docstring 1.""" 641 | raise Exc1 642 | ''', 643 | (f"5:8 {RAISES_SECTION_NOT_IN_DOCSTR_MSG}",), 644 | id="method raises single exc docstring no raises section", 645 | ), 646 | pytest.param( 647 | ''' 648 | class Class1: 649 | """Docstring.""" 650 | def function_1(self): 651 | """Docstring 1. 652 | 653 | Raises: 654 | """ 655 | ''', 656 | (f"5:8 {RAISES_SECTION_IN_DOCSTR_MSG}",), 657 | id="method raises no exc docstring raises section", 658 | ), 659 | pytest.param( 660 | ''' 661 | class Class1: 662 | """Docstring.""" 663 | def function_1(self): 664 | """Docstring 1. 665 | 666 | Raises: 667 | """ 668 | raise Exc1 669 | ''', 670 | (f"9:14 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}",), 671 | id="method raises single exc docstring no exc", 672 | ), 673 | pytest.param( 674 | ''' 675 | class Class1: 676 | """Docstring.""" 677 | @staticmethod 678 | def function_1(): 679 | """Docstring 1. 680 | 681 | Raises: 682 | """ 683 | raise Exc1 684 | ''', 685 | (f"10:14 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}",), 686 | id="method raises single exc docstring no exc staticmethod", 687 | ), 688 | pytest.param( 689 | ''' 690 | class Class1: 691 | """Docstring.""" 692 | @classmethod 693 | def function_1(cls): 694 | """Docstring 1. 695 | 696 | Raises: 697 | """ 698 | raise Exc1 699 | ''', 700 | (f"10:14 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}",), 701 | id="method raises single exc docstring no exc classmethod", 702 | ), 703 | pytest.param( 704 | ''' 705 | class Class1: 706 | """Docstring.""" 707 | def function_1(self): 708 | """Docstring 1. 709 | 710 | Raises: 711 | Exc1: 712 | """ 713 | raise Exc1 714 | ''', 715 | (), 716 | id="method single exc docstring single exc", 717 | ), 718 | pytest.param( 719 | ''' 720 | class Class1: 721 | """Docstring.""" 722 | @staticmethod 723 | def function_1(): 724 | """Docstring 1. 725 | 726 | Raises: 727 | Exc1: 728 | """ 729 | raise Exc1 730 | ''', 731 | (), 732 | id="method single exc docstring single exc staticmethod", 733 | ), 734 | pytest.param( 735 | ''' 736 | class Class1: 737 | """Docstring.""" 738 | @classmethod 739 | def function_1(cls): 740 | """Docstring 1. 741 | 742 | Raises: 743 | Exc1: 744 | """ 745 | raise Exc1 746 | ''', 747 | (), 748 | id="method single exc docstring single exc classmethod", 749 | ), 750 | ], 751 | ) 752 | def test_plugin(code: str, expected_result: tuple[str, ...]): 753 | """ 754 | given: code 755 | when: linting is run on the code 756 | then: the expected result is returned 757 | """ 758 | assert result.get(code) == expected_result 759 | -------------------------------------------------------------------------------- /tests/unit/test___init__args.py: -------------------------------------------------------------------------------- 1 | """Unit tests for args checks in the plugin.""" 2 | 3 | from __future__ import annotations 4 | 5 | import pytest 6 | 7 | from flake8_docstrings_complete.args import ( 8 | ARG_IN_DOCSTR_MSG, 9 | ARG_NOT_IN_DOCSTR_MSG, 10 | ARGS_SECTION_IN_DOCSTR_MSG, 11 | ARGS_SECTION_NOT_IN_DOCSTR_MSG, 12 | DUPLICATE_ARG_MSG, 13 | MULT_ARGS_SECTIONS_IN_DOCSTR_MSG, 14 | ) 15 | 16 | from . import result 17 | 18 | 19 | @pytest.mark.parametrize( 20 | "code, expected_result", 21 | [ 22 | pytest.param( 23 | ''' 24 | def function_1(arg_1): 25 | """Docstring 1.""" 26 | ''', 27 | (f"3:4 {ARGS_SECTION_NOT_IN_DOCSTR_MSG}",), 28 | id="function has single arg docstring no args section", 29 | ), 30 | pytest.param( 31 | ''' 32 | def function_1(arg_1): 33 | """Docstring 1.""" 34 | 35 | def function_2(arg_2): 36 | """Docstring 2.""" 37 | ''', 38 | ( 39 | f"3:4 {ARGS_SECTION_NOT_IN_DOCSTR_MSG}", 40 | f"6:4 {ARGS_SECTION_NOT_IN_DOCSTR_MSG}", 41 | ), 42 | id="multiple function has single arg docstring no args section", 43 | ), 44 | pytest.param( 45 | ''' 46 | def function_1(): 47 | """Docstring 1. 48 | 49 | Args: 50 | """ 51 | ''', 52 | (f"3:4 {ARGS_SECTION_IN_DOCSTR_MSG}",), 53 | id="function has no args docstring args section", 54 | ), 55 | pytest.param( 56 | ''' 57 | def _function_1(): 58 | """Docstring 1. 59 | 60 | Args: 61 | """ 62 | ''', 63 | (f"3:4 {ARGS_SECTION_IN_DOCSTR_MSG}",), 64 | id="private function has no args docstring args section", 65 | ), 66 | pytest.param( 67 | ''' 68 | def function_1(arg_1): 69 | """Docstring 1. 70 | 71 | Args: 72 | arg_1: 73 | 74 | Args: 75 | arg_1: 76 | """ 77 | ''', 78 | (f"3:4 {MULT_ARGS_SECTIONS_IN_DOCSTR_MSG % 'Args,Args'}",), 79 | id="function has single args docstring multiple args sections same name", 80 | ), 81 | pytest.param( 82 | ''' 83 | def function_1(arg_1): 84 | """Docstring 1. 85 | 86 | Args: 87 | arg_1: 88 | 89 | Arguments: 90 | arg_1: 91 | """ 92 | ''', 93 | (f"3:4 {MULT_ARGS_SECTIONS_IN_DOCSTR_MSG % 'Args,Arguments'}",), 94 | id="function has single args docstring multiple args sections different name", 95 | ), 96 | pytest.param( 97 | ''' 98 | def function_1(arg_1): 99 | """Docstring 1. 100 | 101 | Args: 102 | """ 103 | ''', 104 | (f"2:15 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}",), 105 | id="function has single arg docstring no arg", 106 | ), 107 | pytest.param( 108 | ''' 109 | def function_1(_arg_1): 110 | """Docstring 1. 111 | 112 | Args: 113 | """ 114 | ''', 115 | (f"3:4 {ARGS_SECTION_IN_DOCSTR_MSG}",), 116 | id="function has single unused arg docstring args", 117 | ), 118 | pytest.param( 119 | ''' 120 | async def function_1(arg_1): 121 | """Docstring 1. 122 | 123 | Args: 124 | """ 125 | ''', 126 | (f"2:21 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}",), 127 | id="async function has single arg docstring no arg", 128 | ), 129 | pytest.param( 130 | ''' 131 | def function_1(arg_1, /): 132 | """Docstring 1. 133 | 134 | Args: 135 | """ 136 | ''', 137 | (f"2:15 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}",), 138 | id="function has single positional only arg docstring no arg", 139 | ), 140 | pytest.param( 141 | ''' 142 | def function_1(arg_1, arg_2, /): 143 | """Docstring 1. 144 | 145 | Args: 146 | """ 147 | ''', 148 | (f"2:15 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}", f"2:22 {ARG_NOT_IN_DOCSTR_MSG % 'arg_2'}"), 149 | id="function has multiple positional only arg docstring no arg", 150 | ), 151 | pytest.param( 152 | ''' 153 | class Class1: 154 | """Docstring.""" 155 | def function_1(self, arg_1, /): 156 | """Docstring 1. 157 | 158 | Args: 159 | """ 160 | ''', 161 | (f"4:25 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}",), 162 | id="method has single positional only arg docstring no arg", 163 | ), 164 | pytest.param( 165 | ''' 166 | def function_1(*, arg_1): 167 | """Docstring 1. 168 | 169 | Args: 170 | """ 171 | ''', 172 | (f"2:18 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}",), 173 | id="function has single keyword only arg docstring no arg", 174 | ), 175 | pytest.param( 176 | ''' 177 | def function_1(*, arg_1, arg_2): 178 | """Docstring 1. 179 | 180 | Args: 181 | """ 182 | ''', 183 | (f"2:18 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}", f"2:25 {ARG_NOT_IN_DOCSTR_MSG % 'arg_2'}"), 184 | id="function has multiple keyword only arg docstring no arg", 185 | ), 186 | pytest.param( 187 | ''' 188 | class Class1: 189 | """Docstring.""" 190 | def function_1(self, *, arg_1): 191 | """Docstring 1. 192 | 193 | Args: 194 | """ 195 | ''', 196 | (f"4:28 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}",), 197 | id="method has single keyword only arg docstring no arg", 198 | ), 199 | pytest.param( 200 | ''' 201 | def function_1(*args): 202 | """Docstring 1. 203 | 204 | Args: 205 | """ 206 | ''', 207 | (f"2:16 {ARG_NOT_IN_DOCSTR_MSG % 'args'}",), 208 | id="function has *args docstring no arg", 209 | ), 210 | pytest.param( 211 | ''' 212 | def function_1(**kwargs): 213 | """Docstring 1. 214 | 215 | Args: 216 | """ 217 | ''', 218 | (f"2:17 {ARG_NOT_IN_DOCSTR_MSG % 'kwargs'}",), 219 | id="function has **kwargs docstring no arg", 220 | ), 221 | pytest.param( 222 | ''' 223 | def function_1(*args, **kwargs): 224 | """Docstring 1. 225 | 226 | Args: 227 | """ 228 | ''', 229 | ( 230 | f"2:16 {ARG_NOT_IN_DOCSTR_MSG % 'args'}", 231 | f"2:24 {ARG_NOT_IN_DOCSTR_MSG % 'kwargs'}", 232 | ), 233 | id="function has *args and **kwargs docstring no arg", 234 | ), 235 | pytest.param( 236 | ''' 237 | def function_1(*args, arg_1): 238 | """Docstring 1. 239 | 240 | Args: 241 | """ 242 | ''', 243 | (f"2:22 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}", f"2:16 {ARG_NOT_IN_DOCSTR_MSG % 'args'}"), 244 | id="function has *args docstring no arg", 245 | ), 246 | pytest.param( 247 | ''' 248 | def function_1(arg_1, arg_2): 249 | """Docstring 1. 250 | 251 | Args: 252 | """ 253 | ''', 254 | (f"2:15 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}", f"2:22 {ARG_NOT_IN_DOCSTR_MSG % 'arg_2'}"), 255 | id="function multiple args docstring no arg", 256 | ), 257 | pytest.param( 258 | ''' 259 | def function_1(_arg_1, arg_2): 260 | """Docstring 1. 261 | 262 | Args: 263 | """ 264 | ''', 265 | (f"2:23 {ARG_NOT_IN_DOCSTR_MSG % 'arg_2'}",), 266 | id="function multiple args first unused docstring no arg", 267 | ), 268 | pytest.param( 269 | ''' 270 | def function_1(arg_1, _arg_2): 271 | """Docstring 1. 272 | 273 | Args: 274 | """ 275 | ''', 276 | (f"2:15 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}",), 277 | id="function multiple args second unused docstring no arg", 278 | ), 279 | pytest.param( 280 | ''' 281 | def function_1(arg_1, arg_2): 282 | """Docstring 1. 283 | 284 | Args: 285 | arg_1: 286 | """ 287 | ''', 288 | (f"2:22 {ARG_NOT_IN_DOCSTR_MSG % 'arg_2'}",), 289 | id="function multiple args docstring single arg first", 290 | ), 291 | pytest.param( 292 | ''' 293 | def function_1(arg_1, arg_2): 294 | """Docstring 1. 295 | 296 | Args: 297 | arg_2: 298 | """ 299 | ''', 300 | (f"2:15 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}",), 301 | id="function multiple args docstring single arg second", 302 | ), 303 | pytest.param( 304 | ''' 305 | def function_1(arg_1): 306 | """Docstring 1. 307 | 308 | Args: 309 | arg_2: 310 | """ 311 | ''', 312 | ( 313 | f"2:15 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}", 314 | f"3:4 {ARG_IN_DOCSTR_MSG % 'arg_2'}", 315 | ), 316 | id="function has single arg docstring arg different", 317 | ), 318 | pytest.param( 319 | ''' 320 | def function_1(arg_1): 321 | """Docstring 1. 322 | 323 | Args: 324 | arg_2: 325 | arg_3: 326 | """ 327 | ''', 328 | ( 329 | f"2:15 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}", 330 | f"3:4 {ARG_IN_DOCSTR_MSG % 'arg_2'}", 331 | f"3:4 {ARG_IN_DOCSTR_MSG % 'arg_3'}", 332 | ), 333 | id="function single arg docstring multiple args different", 334 | ), 335 | pytest.param( 336 | ''' 337 | def function_1(arg_1, arg_2): 338 | """Docstring 1. 339 | 340 | Args: 341 | arg_3: 342 | arg_4: 343 | """ 344 | ''', 345 | ( 346 | f"2:15 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}", 347 | f"2:22 {ARG_NOT_IN_DOCSTR_MSG % 'arg_2'}", 348 | f"3:4 {ARG_IN_DOCSTR_MSG % 'arg_3'}", 349 | f"3:4 {ARG_IN_DOCSTR_MSG % 'arg_4'}", 350 | ), 351 | id="function multiple arg docstring multiple args different", 352 | ), 353 | pytest.param( 354 | ''' 355 | def function_1(arg_1, arg_2): 356 | """Docstring 1. 357 | 358 | Args: 359 | arg_3: 360 | arg_2: 361 | """ 362 | ''', 363 | (f"2:15 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}", f"3:4 {ARG_IN_DOCSTR_MSG % 'arg_3'}"), 364 | id="function multiple arg docstring multiple args first different", 365 | ), 366 | pytest.param( 367 | ''' 368 | def function_1(arg_1, arg_2): 369 | """Docstring 1. 370 | 371 | Args: 372 | arg_1: 373 | arg_3: 374 | """ 375 | ''', 376 | (f"2:22 {ARG_NOT_IN_DOCSTR_MSG % 'arg_2'}", f"3:4 {ARG_IN_DOCSTR_MSG % 'arg_3'}"), 377 | id="function multiple arg docstring multiple args last different", 378 | ), 379 | pytest.param( 380 | ''' 381 | def function_1(arg_1): 382 | """Docstring 1. 383 | 384 | Args: 385 | arg_1: 386 | arg_1: 387 | """ 388 | ''', 389 | (f"3:4 {DUPLICATE_ARG_MSG % 'arg_1'}",), 390 | id="function single arg docstring duplicate arg", 391 | ), 392 | pytest.param( 393 | ''' 394 | def function_1(_arg_1): 395 | """Docstring 1. 396 | 397 | Args: 398 | _arg_1: 399 | _arg_1: 400 | """ 401 | ''', 402 | (f"3:4 {DUPLICATE_ARG_MSG % '_arg_1'}",), 403 | id="function single unused arg docstring duplicate arg", 404 | ), 405 | pytest.param( 406 | ''' 407 | def function_1(arg_1): 408 | """Docstring 1. 409 | 410 | Args: 411 | arg_1: 412 | arg_1: 413 | arg_1: 414 | """ 415 | ''', 416 | (f"3:4 {DUPLICATE_ARG_MSG % 'arg_1'}",), 417 | id="function single arg docstring duplicate arg many", 418 | ), 419 | pytest.param( 420 | ''' 421 | def function_1(arg_1, arg_2): 422 | """Docstring 1. 423 | 424 | Args: 425 | arg_1: 426 | arg_1: 427 | arg_2: 428 | """ 429 | ''', 430 | (f"3:4 {DUPLICATE_ARG_MSG % 'arg_1'}",), 431 | id="function multiple arg docstring duplicate arg first", 432 | ), 433 | pytest.param( 434 | ''' 435 | def function_1(arg_1, arg_2): 436 | """Docstring 1. 437 | 438 | Args: 439 | arg_1: 440 | arg_2: 441 | arg_2: 442 | """ 443 | ''', 444 | (f"3:4 {DUPLICATE_ARG_MSG % 'arg_2'}",), 445 | id="function multiple arg docstring duplicate arg second", 446 | ), 447 | pytest.param( 448 | ''' 449 | def function_1(arg_1, arg_2): 450 | """Docstring 1. 451 | 452 | Args: 453 | arg_1: 454 | arg_1: 455 | arg_2: 456 | arg_2: 457 | """ 458 | ''', 459 | (f"3:4 {DUPLICATE_ARG_MSG % 'arg_1'}", f"3:4 {DUPLICATE_ARG_MSG % 'arg_2'}"), 460 | id="function multiple arg docstring duplicate arg all", 461 | ), 462 | pytest.param( 463 | ''' 464 | def function_1(arg_1): 465 | """Docstring 1. 466 | 467 | Args: 468 | arg_1: 469 | """ 470 | ''', 471 | (), 472 | id="function single arg docstring single arg", 473 | ), 474 | pytest.param( 475 | ''' 476 | def _function_1(arg_1): 477 | """Docstring 1. 478 | 479 | Args: 480 | arg_1: 481 | """ 482 | ''', 483 | (), 484 | id="private function single arg docstring single arg", 485 | ), 486 | pytest.param( 487 | ''' 488 | def function_1(_arg_1): 489 | """Docstring 1. 490 | 491 | Args: 492 | _arg_1: 493 | """ 494 | ''', 495 | (), 496 | id="function single unused arg docstring single arg", 497 | ), 498 | pytest.param( 499 | ''' 500 | def _function_1(arg_1): 501 | """Docstring 1.""" 502 | ''', 503 | (), 504 | id="private function single arg docstring no arg", 505 | ), 506 | pytest.param( 507 | ''' 508 | def function_1(_arg_1): 509 | """Docstring 1.""" 510 | ''', 511 | (), 512 | id="function single unused arg docstring no args", 513 | ), 514 | pytest.param( 515 | ''' 516 | def function_1(*_args): 517 | """Docstring 1. 518 | 519 | Args: 520 | _args: 521 | """ 522 | ''', 523 | (), 524 | id="function single unused *args docstring single arg", 525 | ), 526 | pytest.param( 527 | ''' 528 | def function_1(*_args): 529 | """Docstring 1.""" 530 | ''', 531 | (), 532 | id="function single unused *args docstring no args", 533 | ), 534 | pytest.param( 535 | ''' 536 | def function_1(**_kwargs): 537 | """Docstring 1. 538 | 539 | Args: 540 | _kwargs: 541 | """ 542 | ''', 543 | (), 544 | id="function single unused **kwargs docstring single arg", 545 | ), 546 | pytest.param( 547 | ''' 548 | def function_1(**_kwargs): 549 | """Docstring 1.""" 550 | ''', 551 | (), 552 | id="function single unused **kwargs docstring no args", 553 | ), 554 | pytest.param( 555 | ''' 556 | def function_1(*args): 557 | """Docstring 1. 558 | 559 | Args: 560 | args: 561 | """ 562 | ''', 563 | (), 564 | id="function single arg docstring *args", 565 | ), 566 | pytest.param( 567 | ''' 568 | def function_1(**kwargs): 569 | """Docstring 1. 570 | 571 | Args: 572 | kwargs: 573 | """ 574 | ''', 575 | (), 576 | id="function single arg docstring **kwargs", 577 | ), 578 | pytest.param( 579 | ''' 580 | def function_1(*args, **kwargs): 581 | """Docstring 1. 582 | 583 | Args: 584 | args: 585 | kwargs: 586 | """ 587 | ''', 588 | (), 589 | id="function single arg docstring *args and **kwargs", 590 | ), 591 | pytest.param( 592 | ''' 593 | def function_1(arg_1, arg_2): 594 | """Docstring 1. 595 | 596 | Args: 597 | arg_1: 598 | arg_2: 599 | """ 600 | ''', 601 | (), 602 | id="function multiple arg docstring multiple arg", 603 | ), 604 | pytest.param( 605 | ''' 606 | def function_1(_arg_1, arg_2): 607 | """Docstring 1. 608 | 609 | Args: 610 | arg_2: 611 | """ 612 | ''', 613 | (), 614 | id="function multiple arg first unused docstring single arg", 615 | ), 616 | pytest.param( 617 | ''' 618 | def function_1(arg_1, _arg_2): 619 | """Docstring 1. 620 | 621 | Args: 622 | arg_1: 623 | """ 624 | ''', 625 | (), 626 | id="function multiple arg first unused docstring single arg", 627 | ), 628 | pytest.param( 629 | ''' 630 | class Class1: 631 | """Docstring.""" 632 | def function_1(self, arg_1): 633 | """Docstring 1.""" 634 | ''', 635 | (f"5:8 {ARGS_SECTION_NOT_IN_DOCSTR_MSG}",), 636 | id="method has single arg docstring no args section", 637 | ), 638 | pytest.param( 639 | ''' 640 | class Class1: 641 | """Docstring.""" 642 | def function_1(self): 643 | """Docstring 1. 644 | 645 | Args: 646 | """ 647 | ''', 648 | (f"5:8 {ARGS_SECTION_IN_DOCSTR_MSG}",), 649 | id="method has no args docstring args section", 650 | ), 651 | pytest.param( 652 | ''' 653 | class Class1: 654 | """Docstring.""" 655 | def function_1(self, arg_1): 656 | """Docstring 1. 657 | 658 | Args: 659 | """ 660 | ''', 661 | (f"4:25 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}",), 662 | id="method has single arg docstring no arg", 663 | ), 664 | pytest.param( 665 | ''' 666 | class Class1: 667 | """Docstring.""" 668 | @staticmethod 669 | def function_1(arg_1): 670 | """Docstring 1. 671 | 672 | Args: 673 | """ 674 | ''', 675 | (f"5:19 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}",), 676 | id="method has single arg docstring no arg staticmethod", 677 | ), 678 | pytest.param( 679 | ''' 680 | class Class1: 681 | """Docstring.""" 682 | @classmethod 683 | def function_1(cls, arg_1): 684 | """Docstring 1. 685 | 686 | Args: 687 | """ 688 | ''', 689 | (f"5:24 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}",), 690 | id="method has single arg docstring no arg classmethod", 691 | ), 692 | pytest.param( 693 | ''' 694 | class Class1: 695 | """Docstring.""" 696 | def function_1(self, arg_1): 697 | """Docstring 1. 698 | 699 | Args: 700 | arg_1: 701 | arg_1: 702 | """ 703 | ''', 704 | (f"5:8 {DUPLICATE_ARG_MSG % 'arg_1'}",), 705 | id="method single arg docstring single arg duplicate", 706 | ), 707 | pytest.param( 708 | ''' 709 | class Class1: 710 | """Docstring.""" 711 | def function_1(self, arg_1): 712 | """Docstring 1. 713 | 714 | Args: 715 | arg_1: 716 | """ 717 | ''', 718 | (), 719 | id="method single arg docstring single arg", 720 | ), 721 | pytest.param( 722 | ''' 723 | class Class1: 724 | """Docstring.""" 725 | @staticmethod 726 | def function_1(arg_1): 727 | """Docstring 1. 728 | 729 | Args: 730 | arg_1: 731 | """ 732 | ''', 733 | (), 734 | id="method single arg docstring single arg staticmethod", 735 | ), 736 | pytest.param( 737 | ''' 738 | class Class1: 739 | """Docstring.""" 740 | @classmethod 741 | def function_1(cls, arg_1): 742 | """Docstring 1. 743 | 744 | Args: 745 | arg_1: 746 | """ 747 | ''', 748 | (), 749 | id="method single arg docstring single arg classmethod", 750 | ), 751 | ], 752 | ) 753 | def test_plugin(code: str, expected_result: tuple[str, ...]): 754 | """ 755 | given: code 756 | when: linting is run on the code 757 | then: the expected result is returned 758 | """ 759 | assert result.get(code) == expected_result 760 | -------------------------------------------------------------------------------- /flake8_docstrings_complete/__init__.py: -------------------------------------------------------------------------------- 1 | """A linter that checks docstring include all expected descriptions.""" 2 | 3 | from __future__ import annotations 4 | 5 | import argparse 6 | import ast 7 | import re 8 | from pathlib import Path 9 | from typing import Iterable, Iterator 10 | 11 | from flake8.options.manager import OptionManager 12 | 13 | from . import args, attrs, docstring, raises, types_ 14 | from .constants import ERROR_CODE_PREFIX, MORE_INFO_BASE 15 | 16 | DOCSTR_MISSING_CODE = f"{ERROR_CODE_PREFIX}010" 17 | DOCSTR_MISSING_MSG = ( 18 | f"{DOCSTR_MISSING_CODE} docstring should be defined for a function/ method/ class" 19 | f"{MORE_INFO_BASE}{DOCSTR_MISSING_CODE.lower()}" 20 | ) 21 | RETURNS_SECTION_NOT_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}030" 22 | RETURNS_SECTION_NOT_IN_DOCSTR_MSG = ( 23 | f"{RETURNS_SECTION_NOT_IN_DOCSTR_CODE} function/ method that returns a value should have the " 24 | f"returns section in the docstring{MORE_INFO_BASE}{RETURNS_SECTION_NOT_IN_DOCSTR_CODE.lower()}" 25 | ) 26 | RETURNS_SECTION_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}031" 27 | RETURNS_SECTION_IN_DOCSTR_MSG = ( 28 | f"{RETURNS_SECTION_IN_DOCSTR_CODE} function/ method that does not return a value should not " 29 | f"have the returns section in the docstring" 30 | f"{MORE_INFO_BASE}{RETURNS_SECTION_IN_DOCSTR_CODE.lower()}" 31 | ) 32 | MULT_RETURNS_SECTIONS_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}032" 33 | MULT_RETURNS_SECTIONS_IN_DOCSTR_MSG = ( 34 | f"{MULT_RETURNS_SECTIONS_IN_DOCSTR_CODE} a docstring should only contain a single returns " 35 | "section, found %s" 36 | f"{MORE_INFO_BASE}{MULT_RETURNS_SECTIONS_IN_DOCSTR_CODE.lower()}" 37 | ) 38 | YIELDS_SECTION_NOT_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}040" 39 | YIELDS_SECTION_NOT_IN_DOCSTR_MSG = ( 40 | f"{YIELDS_SECTION_NOT_IN_DOCSTR_CODE} function/ method that yields a value should have the " 41 | f"yields section in the docstring{MORE_INFO_BASE}{YIELDS_SECTION_NOT_IN_DOCSTR_CODE.lower()}" 42 | ) 43 | YIELDS_SECTION_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}041" 44 | YIELDS_SECTION_IN_DOCSTR_MSG = ( 45 | f"{YIELDS_SECTION_IN_DOCSTR_CODE} function/ method that does not yield a value should not " 46 | f"have the yields section in the docstring" 47 | f"{MORE_INFO_BASE}{YIELDS_SECTION_IN_DOCSTR_CODE.lower()}" 48 | ) 49 | MULT_YIELDS_SECTIONS_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}042" 50 | MULT_YIELDS_SECTIONS_IN_DOCSTR_MSG = ( 51 | f"{MULT_YIELDS_SECTIONS_IN_DOCSTR_CODE} a docstring should only contain a single yields " 52 | "section, found %s" 53 | f"{MORE_INFO_BASE}{MULT_YIELDS_SECTIONS_IN_DOCSTR_CODE.lower()}" 54 | ) 55 | 56 | PRIVATE_FUNCTION_PATTERN = r"_[^_].*" 57 | TEST_FILENAME_PATTERN_ARG_NAME = "--docstrings-complete-test-filename-pattern" 58 | TEST_FILENAME_PATTERN_DEFAULT = r"test_.*\.py" 59 | TEST_FUNCTION_PATTERN_ARG_NAME = "--docstrings-complete-test-function-pattern" 60 | TEST_FUNCTION_PATTERN_DEFAULT = r"test_.*" 61 | FIXTURE_FILENAME_PATTERN_ARG_NAME = "--docstrings-complete-fixture-filename-pattern" 62 | FIXTURE_FILENAME_PATTERN_DEFAULT = r"conftest\.py" 63 | FIXTURE_DECORATOR_PATTERN_ARG_NAME = "--docstrings-complete-fixture-decorator-pattern" 64 | FIXTURE_DECORATOR_PATTERN_DEFAULT = r"(^|\.)fixture$" 65 | 66 | 67 | # Helper function for option management, tested in integration tests 68 | def _cli_arg_name_to_attr(cli_arg_name: str) -> str: 69 | """Transform CLI argument name to the attribute name on the namespace. 70 | 71 | Args: 72 | cli_arg_name: The CLI argument name to transform. 73 | 74 | Returns: 75 | The namespace name for the argument. 76 | """ 77 | return cli_arg_name.lstrip("-").replace("-", "_") # pragma: nocover 78 | 79 | 80 | def _check_returns( 81 | docstr_info: docstring.Docstring, 82 | docstr_node: ast.Constant, 83 | return_nodes: Iterable[ast.Return], 84 | is_private: bool, 85 | ) -> Iterator[types_.Problem]: 86 | """Check function/ method returns section. 87 | 88 | Args: 89 | docstr_info: Information about the docstring. 90 | docstr_node: The docstring node. 91 | return_nodes: The return nodes of the function. 92 | is_private: If the function for the docstring is private. 93 | 94 | Yields: 95 | All the problems with the returns section. 96 | """ 97 | return_nodes_with_value = list(node for node in return_nodes if node.value is not None) 98 | 99 | # Check for return statements with value and no returns section in docstring 100 | if return_nodes_with_value and not docstr_info.returns_sections and not is_private: 101 | yield from ( 102 | types_.Problem(node.lineno, node.col_offset, RETURNS_SECTION_NOT_IN_DOCSTR_MSG) 103 | for node in return_nodes_with_value 104 | ) 105 | 106 | # Check for multiple returns sections 107 | if return_nodes_with_value and len(docstr_info.returns_sections) > 1: 108 | yield types_.Problem( 109 | docstr_node.lineno, 110 | docstr_node.col_offset, 111 | MULT_RETURNS_SECTIONS_IN_DOCSTR_MSG % ",".join(docstr_info.returns_sections), 112 | ) 113 | 114 | # Check for returns section in docstring in function that does not return a value 115 | if not return_nodes_with_value and docstr_info.returns_sections: 116 | yield types_.Problem( 117 | docstr_node.lineno, docstr_node.col_offset, RETURNS_SECTION_IN_DOCSTR_MSG 118 | ) 119 | 120 | 121 | def _check_yields( 122 | docstr_info: docstring.Docstring, 123 | docstr_node: ast.Constant, 124 | yield_nodes: Iterable[ast.Yield | ast.YieldFrom], 125 | is_private: bool, 126 | ) -> Iterator[types_.Problem]: 127 | """Check function/ method yields section. 128 | 129 | Args: 130 | docstr_info: Information about the docstring. 131 | docstr_node: The docstring node. 132 | yield_nodes: The yield and yield from nodes of the function. 133 | is_private: If the function for the docstring is private. 134 | 135 | Yields: 136 | All the problems with the yields section. 137 | """ 138 | yield_nodes_with_value = list(node for node in yield_nodes if node.value is not None) 139 | 140 | # Check for yield statements with value and no yields section in docstring 141 | if yield_nodes_with_value and not docstr_info.yields_sections and not is_private: 142 | yield from ( 143 | types_.Problem(node.lineno, node.col_offset, YIELDS_SECTION_NOT_IN_DOCSTR_MSG) 144 | for node in yield_nodes_with_value 145 | ) 146 | 147 | # Check for multiple yields sections 148 | if yield_nodes_with_value and len(docstr_info.yields_sections) > 1: 149 | yield types_.Problem( 150 | docstr_node.lineno, 151 | docstr_node.col_offset, 152 | MULT_YIELDS_SECTIONS_IN_DOCSTR_MSG % ",".join(docstr_info.yields_sections), 153 | ) 154 | 155 | # Check for yields section in docstring in function that does not yield a value 156 | if not yield_nodes_with_value and docstr_info.yields_sections: 157 | yield types_.Problem( 158 | docstr_node.lineno, docstr_node.col_offset, YIELDS_SECTION_IN_DOCSTR_MSG 159 | ) 160 | 161 | 162 | class VisitorWithinFunction(ast.NodeVisitor): 163 | """Visits AST nodes within a functions but not nested functions or classes. 164 | 165 | Attrs: 166 | return_nodes: All the return nodes encountered within the function. 167 | yield_nodes: All the yield nodes encountered within the function. 168 | raise_nodes: All the raise nodes encountered within the function. 169 | """ 170 | 171 | return_nodes: list[ast.Return] 172 | yield_nodes: list[ast.Yield | ast.YieldFrom] 173 | raise_nodes: list[ast.Raise] 174 | _visited_once: bool 175 | 176 | def __init__(self) -> None: 177 | """Construct.""" 178 | self.return_nodes = [] 179 | self.yield_nodes = [] 180 | self.raise_nodes = [] 181 | self._visited_once = False 182 | 183 | # The function must be called the same as the name of the node 184 | def visit_Return(self, node: ast.Return) -> None: # pylint: disable=invalid-name 185 | """Record return node. 186 | 187 | Args: 188 | node: The return node to record. 189 | """ 190 | self.return_nodes.append(node) 191 | 192 | # Ensure recursion continues 193 | self.generic_visit(node) 194 | 195 | # The function must be called the same as the name of the node 196 | def visit_Yield(self, node: ast.Yield) -> None: # pylint: disable=invalid-name 197 | """Record yield node. 198 | 199 | Args: 200 | node: The yield node to record. 201 | """ 202 | self.yield_nodes.append(node) 203 | 204 | # Ensure recursion continues 205 | self.generic_visit(node) 206 | 207 | # The function must be called the same as the name of the node 208 | def visit_YieldFrom(self, node: ast.YieldFrom) -> None: # pylint: disable=invalid-name 209 | """Record yield from node. 210 | 211 | Args: 212 | node: The yield from node to record. 213 | """ 214 | self.yield_nodes.append(node) 215 | 216 | # Ensure recursion continues 217 | self.generic_visit(node) 218 | 219 | # The function must be called the same as the name of the node 220 | def visit_Raise(self, node: ast.Raise) -> None: # pylint: disable=invalid-name 221 | """Record raise node. 222 | 223 | Args: 224 | node: The raise node to record. 225 | """ 226 | self.raise_nodes.append(node) 227 | 228 | # Ensure recursion continues 229 | self.generic_visit(node) 230 | 231 | def visit_once(self, node: ast.AST) -> None: 232 | """Visit the node once and then skip. 233 | 234 | Args: 235 | node: The node being visited. 236 | """ 237 | if not self._visited_once: 238 | self._visited_once = True 239 | self.generic_visit(node=node) 240 | 241 | # Ensure that nested functions and classes are not iterated over 242 | # The functions must be called the same as the name of the node 243 | visit_FunctionDef = visit_once # noqa: N815,DCO063 244 | visit_AsyncFunctionDef = visit_once # noqa: N815,DCO063 245 | visit_ClassDef = visit_once # noqa: N815,DCO063 246 | 247 | 248 | class Visitor(ast.NodeVisitor): 249 | """Visits AST nodes and check docstrings of functions and classes. 250 | 251 | Attrs: 252 | problems: All the problems that were encountered. 253 | """ 254 | 255 | problems: list[types_.Problem] 256 | _file_type: types_.FileType 257 | _test_function_pattern: str 258 | _fixture_decorator_pattern: str 259 | 260 | def __init__( 261 | self, 262 | file_type: types_.FileType, 263 | test_function_pattern: str, 264 | fixture_decorator_pattern: str, 265 | ) -> None: 266 | """Construct. 267 | 268 | Args: 269 | file_type: The type of file being processed. 270 | test_function_pattern: The pattern to match test functions with. 271 | fixture_decorator_pattern: The pattern to match decorators of fixture function with. 272 | """ 273 | self.problems = [] 274 | self._file_type = file_type 275 | self._test_function_pattern = test_function_pattern 276 | self._fixture_decorator_pattern = fixture_decorator_pattern 277 | 278 | def _is_fixture_decorator(self, node: ast.expr) -> bool: 279 | """Determine whether an expression is a fixture decorator. 280 | 281 | Args: 282 | node: The node to check. 283 | 284 | Returns: 285 | Whether the node is a fixture decorator. 286 | """ 287 | # Handle variable 288 | fixture_name: str | None = None 289 | if isinstance(node, ast.Name): 290 | fixture_name = node.id 291 | if isinstance(node, ast.Attribute): 292 | fixture_name = node.attr 293 | if fixture_name is not None: 294 | return ( 295 | re.search(self._fixture_decorator_pattern, fixture_name, re.IGNORECASE) is not None 296 | ) 297 | 298 | # Handle call 299 | if isinstance(node, ast.Call): 300 | return self._is_fixture_decorator(node=node.func) 301 | 302 | # No valid syntax can reach here 303 | return False # pragma: nocover 304 | 305 | def _is_overload_decorator(self, node: ast.expr) -> bool: 306 | """Determine whether an expression is an overload decorator. 307 | 308 | Args: 309 | node: The node to check. 310 | 311 | Returns: 312 | Whether the node is an overload decorator. 313 | """ 314 | if isinstance(node, ast.Name): 315 | return node.id == "overload" 316 | 317 | # Handle call 318 | if isinstance(node, ast.Call): 319 | return self._is_overload_decorator(node=node.func) 320 | 321 | # Handle attr 322 | if isinstance(node, ast.Attribute): 323 | value = node.value 324 | return node.attr == "overload" and isinstance(value, ast.Name) and value.id == "typing" 325 | 326 | # There is no valid syntax that gets to here 327 | return False # pragma: nocover 328 | 329 | def _skip_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool: 330 | """Check whether to skip a function. 331 | 332 | A function is skipped if it is a test function in a test file, if it is a fixture in a test 333 | or fixture file or if it is a property. 334 | 335 | Args: 336 | node: The function to check 337 | 338 | Returns: 339 | Whether to skip the function. 340 | """ 341 | # Check for properties 342 | if any(attrs.is_property_decorator(decorator) for decorator in node.decorator_list): 343 | return True 344 | 345 | # Check for test functions 346 | if self._file_type == types_.FileType.TEST and re.match( 347 | self._test_function_pattern, node.name 348 | ): 349 | return True 350 | 351 | # Check for fixtures 352 | if self._file_type in {types_.FileType.TEST, types_.FileType.FIXTURE}: 353 | return any(self._is_fixture_decorator(decorator) for decorator in node.decorator_list) 354 | 355 | # Check for overload 356 | if any(self._is_overload_decorator(decorator) for decorator in node.decorator_list): 357 | return True 358 | 359 | return False 360 | 361 | def visit_any_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None: 362 | """Check a function definition node. 363 | 364 | Args: 365 | node: The function definition to check. 366 | """ 367 | if not self._skip_function(node=node): 368 | # Check docstring is defined 369 | if ast.get_docstring(node) is None: 370 | self.problems.append( 371 | types_.Problem( 372 | lineno=node.lineno, col_offset=node.col_offset, msg=DOCSTR_MISSING_MSG 373 | ) 374 | ) 375 | 376 | if ( 377 | node.body 378 | and isinstance(node.body[0], ast.Expr) 379 | and isinstance(node.body[0].value, ast.Constant) 380 | and isinstance(node.body[0].value.value, str) 381 | ): 382 | is_private = bool(re.match(PRIVATE_FUNCTION_PATTERN, node.name)) 383 | # Check args 384 | docstr_info = docstring.parse(value=node.body[0].value.value) 385 | docstr_node = node.body[0].value 386 | self.problems.extend( 387 | args.check( 388 | docstr_info=docstr_info, 389 | docstr_node=docstr_node, 390 | args=node.args, 391 | is_private=is_private, 392 | ) 393 | ) 394 | 395 | # Check returns 396 | visitor_within_function = VisitorWithinFunction() 397 | visitor_within_function.visit(node=node) 398 | self.problems.extend( 399 | _check_returns( 400 | docstr_info=docstr_info, 401 | docstr_node=docstr_node, 402 | return_nodes=visitor_within_function.return_nodes, 403 | is_private=is_private, 404 | ) 405 | ) 406 | 407 | # Check yields 408 | self.problems.extend( 409 | _check_yields( 410 | docstr_info=docstr_info, 411 | docstr_node=docstr_node, 412 | yield_nodes=visitor_within_function.yield_nodes, 413 | is_private=is_private, 414 | ) 415 | ) 416 | 417 | # Check raises 418 | self.problems.extend( 419 | raises.check( 420 | docstr_info=docstr_info, 421 | docstr_node=docstr_node, 422 | raise_nodes=visitor_within_function.raise_nodes, 423 | is_private=is_private, 424 | ) 425 | ) 426 | 427 | # Ensure recursion continues 428 | self.generic_visit(node) 429 | 430 | # The functions must be called the same as the name of the node 431 | visit_FunctionDef = visit_any_function # noqa: N815,DCO063 432 | visit_AsyncFunctionDef = visit_any_function # noqa: N815,DCO063 433 | 434 | # The function must be called the same as the name of the node 435 | def visit_ClassDef(self, node: ast.ClassDef) -> None: # pylint: disable=invalid-name 436 | """Check a class definition node. 437 | 438 | Args: 439 | node: The class definition to check. 440 | """ 441 | # Check docstring is defined 442 | if ast.get_docstring(node) is None: 443 | self.problems.append( 444 | types_.Problem( 445 | lineno=node.lineno, col_offset=node.col_offset, msg=DOCSTR_MISSING_MSG 446 | ) 447 | ) 448 | 449 | if ( 450 | node.body 451 | and isinstance(node.body[0], ast.Expr) 452 | and isinstance(node.body[0].value, ast.Constant) 453 | and isinstance(node.body[0].value.value, str) 454 | ): 455 | # Check attrs 456 | docstr_info = docstring.parse(value=node.body[0].value.value) 457 | docstr_node = node.body[0].value 458 | visitor_within_class = attrs.VisitorWithinClass() 459 | visitor_within_class.visit(node=node) 460 | self.problems.extend( 461 | attrs.check( 462 | docstr_info=docstr_info, 463 | docstr_node=docstr_node, 464 | class_assign_nodes=visitor_within_class.class_assign_nodes, 465 | method_assign_nodes=visitor_within_class.method_assign_nodes, 466 | ) 467 | ) 468 | 469 | # Ensure recursion continues 470 | self.generic_visit(node) 471 | 472 | 473 | class Plugin: 474 | """Checks docstring include all expected descriptions. 475 | 476 | Attrs: 477 | name: The name of the plugin. 478 | """ 479 | 480 | name = __name__ 481 | _test_filename_pattern: str = TEST_FILENAME_PATTERN_DEFAULT 482 | _test_function_pattern: str = TEST_FUNCTION_PATTERN_DEFAULT 483 | _fixture_filename_pattern: str = FIXTURE_FILENAME_PATTERN_DEFAULT 484 | _fixture_decorator_pattern: str = FIXTURE_DECORATOR_PATTERN_DEFAULT 485 | _tree: ast.AST 486 | _filename: str 487 | 488 | def __init__(self, tree: ast.AST, filename: str) -> None: 489 | """Construct. 490 | 491 | Args: 492 | tree: The AST syntax tree for a file. 493 | filename: The name of the file being processed. 494 | """ 495 | self._tree = tree 496 | self._filename = Path(filename).name 497 | 498 | def _get_file_type(self) -> types_.FileType: 499 | """Get the file type from a filename. 500 | 501 | Returns: 502 | The type of file. 503 | """ 504 | if re.match(self._test_filename_pattern, self._filename) is not None: 505 | return types_.FileType.TEST 506 | 507 | if re.match(self._fixture_filename_pattern, self._filename) is not None: 508 | return types_.FileType.FIXTURE 509 | 510 | return types_.FileType.DEFAULT 511 | 512 | # No coverage since this only occurs from the command line 513 | @staticmethod 514 | def add_options(option_manager: OptionManager) -> None: # pragma: nocover 515 | """Add additional options to flake8. 516 | 517 | Args: 518 | option_manager: The flake8 OptionManager. 519 | """ 520 | option_manager.add_option( 521 | TEST_FILENAME_PATTERN_ARG_NAME, 522 | default=TEST_FILENAME_PATTERN_DEFAULT, 523 | parse_from_config=True, 524 | help=( 525 | "The pattern to identify test files. " 526 | f"(Default: {TEST_FILENAME_PATTERN_DEFAULT})" 527 | ), 528 | ) 529 | option_manager.add_option( 530 | TEST_FUNCTION_PATTERN_ARG_NAME, 531 | default=TEST_FUNCTION_PATTERN_DEFAULT, 532 | parse_from_config=True, 533 | help=( 534 | "The pattern for the name of test functions to exclude in test files. " 535 | f"(Default: {TEST_FUNCTION_PATTERN_DEFAULT})" 536 | ), 537 | ) 538 | option_manager.add_option( 539 | FIXTURE_FILENAME_PATTERN_ARG_NAME, 540 | default=FIXTURE_FILENAME_PATTERN_DEFAULT, 541 | parse_from_config=True, 542 | help=( 543 | "The pattern to identify fixture files. " 544 | f"(Default: {FIXTURE_FILENAME_PATTERN_DEFAULT})" 545 | ), 546 | ) 547 | option_manager.add_option( 548 | FIXTURE_DECORATOR_PATTERN_ARG_NAME, 549 | default=FIXTURE_DECORATOR_PATTERN_DEFAULT, 550 | parse_from_config=True, 551 | help=( 552 | "The pattern for the decorator name to exclude fixture functions. " 553 | f"(Default: {FIXTURE_DECORATOR_PATTERN_DEFAULT})" 554 | ), 555 | ) 556 | 557 | # No coverage since this only occurs from the command line 558 | @classmethod 559 | def parse_options(cls, options: argparse.Namespace) -> None: # pragma: nocover 560 | """Record the value of the options. 561 | 562 | Args: 563 | options: The options passed to flake8. 564 | """ 565 | cls._test_filename_pattern = ( 566 | getattr(options, _cli_arg_name_to_attr(TEST_FILENAME_PATTERN_ARG_NAME), None) 567 | or TEST_FILENAME_PATTERN_DEFAULT 568 | ) 569 | cls._test_function_pattern = ( 570 | getattr(options, _cli_arg_name_to_attr(TEST_FUNCTION_PATTERN_ARG_NAME), None) 571 | or TEST_FUNCTION_PATTERN_DEFAULT 572 | ) 573 | cls._fixture_filename_pattern = ( 574 | getattr(options, _cli_arg_name_to_attr(FIXTURE_FILENAME_PATTERN_ARG_NAME), None) 575 | or FIXTURE_FILENAME_PATTERN_DEFAULT 576 | ) 577 | cls._fixture_decorator_pattern = ( 578 | getattr(options, _cli_arg_name_to_attr(FIXTURE_DECORATOR_PATTERN_ARG_NAME), None) 579 | or FIXTURE_DECORATOR_PATTERN_DEFAULT 580 | ) 581 | 582 | def run(self) -> Iterator[tuple[int, int, str, type["Plugin"]]]: 583 | """Lint a file. 584 | 585 | Yields: 586 | All the problems that were found. 587 | """ 588 | file_type = self._get_file_type() 589 | visitor = Visitor( 590 | file_type=file_type, 591 | test_function_pattern=self._test_function_pattern, 592 | fixture_decorator_pattern=self._fixture_decorator_pattern, 593 | ) 594 | visitor.visit(node=self._tree) 595 | yield from ( 596 | (problem.lineno, problem.col_offset, problem.msg, type(self)) 597 | for problem in visitor.problems 598 | ) 599 | -------------------------------------------------------------------------------- /tests/unit/test___init__.py: -------------------------------------------------------------------------------- 1 | """Unit tests for plugin except for args rules.""" 2 | 3 | # The lines represent the number of test cases 4 | # pylint: disable=too-many-lines 5 | 6 | from __future__ import annotations 7 | 8 | import pytest 9 | 10 | from flake8_docstrings_complete import ( 11 | DOCSTR_MISSING_MSG, 12 | MULT_RETURNS_SECTIONS_IN_DOCSTR_MSG, 13 | MULT_YIELDS_SECTIONS_IN_DOCSTR_MSG, 14 | RETURNS_SECTION_IN_DOCSTR_MSG, 15 | RETURNS_SECTION_NOT_IN_DOCSTR_MSG, 16 | YIELDS_SECTION_IN_DOCSTR_MSG, 17 | YIELDS_SECTION_NOT_IN_DOCSTR_MSG, 18 | ) 19 | 20 | from . import result 21 | 22 | 23 | @pytest.mark.parametrize( 24 | "code, expected_result", 25 | [ 26 | pytest.param("", (), id="trivial"), 27 | pytest.param( 28 | """ 29 | def function_1(): 30 | return 31 | """, 32 | (f"2:0 {DOCSTR_MISSING_MSG}",), 33 | id="function docstring missing return", 34 | ), 35 | pytest.param( 36 | """ 37 | def _function_1(): 38 | return 39 | """, 40 | (f"2:0 {DOCSTR_MISSING_MSG}",), 41 | id="private function docstring missing return", 42 | ), 43 | pytest.param( 44 | """ 45 | @overload 46 | def function_1(): 47 | ... 48 | """, 49 | (), 50 | id="function docstring missing overload", 51 | ), 52 | pytest.param( 53 | """ 54 | @overload() 55 | def function_1(): 56 | ... 57 | """, 58 | (), 59 | id="function docstring missing overload call", 60 | ), 61 | pytest.param( 62 | """ 63 | @typing.overload 64 | def function_1(): 65 | ... 66 | """, 67 | (), 68 | id="function docstring missing overload attr", 69 | ), 70 | pytest.param( 71 | """ 72 | def function_1(): 73 | return 74 | 75 | def function_2(): 76 | return 77 | """, 78 | (f"2:0 {DOCSTR_MISSING_MSG}", f"5:0 {DOCSTR_MISSING_MSG}"), 79 | id="multiple functions docstring missing return", 80 | ), 81 | pytest.param( 82 | """ 83 | def function_1(): 84 | pass 85 | """, 86 | (f"2:0 {DOCSTR_MISSING_MSG}",), 87 | id="function docstring missing expression not constant", 88 | ), 89 | pytest.param( 90 | """ 91 | def function_1(): 92 | 1 93 | """, 94 | (f"2:0 {DOCSTR_MISSING_MSG}",), 95 | id="function docstring missing expression constnant not string", 96 | ), 97 | pytest.param( 98 | ''' 99 | def function_1(): 100 | """Docstring. 101 | 102 | Returns: 103 | """ 104 | ''', 105 | (f"3:4 {RETURNS_SECTION_IN_DOCSTR_MSG}",), 106 | id="function no return returns in docstring", 107 | ), 108 | pytest.param( 109 | ''' 110 | def _function_1(): 111 | """Docstring. 112 | 113 | Returns: 114 | """ 115 | ''', 116 | (f"3:4 {RETURNS_SECTION_IN_DOCSTR_MSG}",), 117 | id="private function no return returns in docstring", 118 | ), 119 | pytest.param( 120 | ''' 121 | class Class1: 122 | """Docstring.""" 123 | def function_1(): 124 | """Docstring. 125 | 126 | Returns: 127 | """ 128 | ''', 129 | (f"5:8 {RETURNS_SECTION_IN_DOCSTR_MSG}",), 130 | id="method no return returns in docstring", 131 | ), 132 | pytest.param( 133 | ''' 134 | def function_1(): 135 | """Docstring. 136 | 137 | Returns: 138 | """ 139 | return 140 | ''', 141 | (f"3:4 {RETURNS_SECTION_IN_DOCSTR_MSG}",), 142 | id="function return no value returns in docstring", 143 | ), 144 | pytest.param( 145 | ''' 146 | def function_1(): 147 | """Docstring. 148 | 149 | Returns: 150 | 151 | Returns: 152 | """ 153 | return 1 154 | ''', 155 | (f"3:4 {MULT_RETURNS_SECTIONS_IN_DOCSTR_MSG % 'Returns,Returns'}",), 156 | id="function return multiple returns in docstring", 157 | ), 158 | pytest.param( 159 | ''' 160 | class Class1: 161 | """Docstring.""" 162 | def function_1(): 163 | """Docstring. 164 | 165 | Returns: 166 | 167 | Returns: 168 | """ 169 | return 1 170 | ''', 171 | (f"5:8 {MULT_RETURNS_SECTIONS_IN_DOCSTR_MSG % 'Returns,Returns'}",), 172 | id="method return multiple returns in docstring", 173 | ), 174 | pytest.param( 175 | ''' 176 | def function_1(): 177 | """Docstring.""" 178 | return 1 179 | ''', 180 | (f"4:4 {RETURNS_SECTION_NOT_IN_DOCSTR_MSG}",), 181 | id="function single return value returns not in docstring", 182 | ), 183 | pytest.param( 184 | ''' 185 | def function_1(): 186 | """Docstring.""" 187 | return 0 188 | ''', 189 | (f"4:4 {RETURNS_SECTION_NOT_IN_DOCSTR_MSG}",), 190 | id="function single falsely return value returns not in docstring", 191 | ), 192 | pytest.param( 193 | ''' 194 | def function_1(): 195 | """Docstring.""" 196 | return None 197 | ''', 198 | (f"4:4 {RETURNS_SECTION_NOT_IN_DOCSTR_MSG}",), 199 | id="function single None return value returns not in docstring", 200 | ), 201 | pytest.param( 202 | ''' 203 | async def function_1(): 204 | """Docstring.""" 205 | return 1 206 | ''', 207 | (f"4:4 {RETURNS_SECTION_NOT_IN_DOCSTR_MSG}",), 208 | id="async function single return value returns not in docstring", 209 | ), 210 | pytest.param( 211 | ''' 212 | class FooClass: 213 | """Docstring.""" 214 | def function_1(self): 215 | """Docstring.""" 216 | return 1 217 | ''', 218 | (f"6:8 {RETURNS_SECTION_NOT_IN_DOCSTR_MSG}",), 219 | id="method single return value returns not in docstring", 220 | ), 221 | pytest.param( 222 | ''' 223 | def function_1(): 224 | """Docstring.""" 225 | if True: 226 | return 1 227 | ''', 228 | (f"5:8 {RETURNS_SECTION_NOT_IN_DOCSTR_MSG}",), 229 | id="function single nested return value returns not in docstring", 230 | ), 231 | pytest.param( 232 | ''' 233 | def function_1(): 234 | """Docstring.""" 235 | return 11 236 | return 12 237 | ''', 238 | ( 239 | f"4:4 {RETURNS_SECTION_NOT_IN_DOCSTR_MSG}", 240 | f"5:4 {RETURNS_SECTION_NOT_IN_DOCSTR_MSG}", 241 | ), 242 | id="function multiple return value returns not in docstring", 243 | ), 244 | pytest.param( 245 | ''' 246 | def function_1(): 247 | """Docstring.""" 248 | return 11 249 | return 250 | ''', 251 | (f"4:4 {RETURNS_SECTION_NOT_IN_DOCSTR_MSG}",), 252 | id="function multiple return first value returns not in docstring", 253 | ), 254 | pytest.param( 255 | ''' 256 | def function_1(): 257 | """Docstring.""" 258 | return 259 | return 12 260 | ''', 261 | (f"5:4 {RETURNS_SECTION_NOT_IN_DOCSTR_MSG}",), 262 | id="function multiple return second value returns not in docstring", 263 | ), 264 | pytest.param( 265 | ''' 266 | def function_1(): 267 | """Docstring.""" 268 | yield 1 269 | ''', 270 | (f"4:4 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}",), 271 | id="function single yield value yields not in docstring", 272 | ), 273 | pytest.param( 274 | ''' 275 | def _function_1(): 276 | """Docstring.""" 277 | yield 1 278 | ''', 279 | (), 280 | id="private function single yield value yields not in docstring", 281 | ), 282 | pytest.param( 283 | ''' 284 | def function_1(): 285 | """Docstring.""" 286 | yield from tuple() 287 | ''', 288 | (f"4:4 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}",), 289 | id="function single yield from value yields not in docstring", 290 | ), 291 | pytest.param( 292 | ''' 293 | def function_1(): 294 | """Docstring.""" 295 | yield 0 296 | ''', 297 | (f"4:4 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}",), 298 | id="function single falsely yield value yields not in docstring", 299 | ), 300 | pytest.param( 301 | ''' 302 | def function_1(): 303 | """Docstring.""" 304 | yield None 305 | ''', 306 | (f"4:4 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}",), 307 | id="function single None yield value yields not in docstring", 308 | ), 309 | pytest.param( 310 | ''' 311 | async def function_1(): 312 | """Docstring.""" 313 | yield 1 314 | ''', 315 | (f"4:4 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}",), 316 | id="async function single yield value yields not in docstring", 317 | ), 318 | pytest.param( 319 | ''' 320 | class FooClass: 321 | """Docstring.""" 322 | def function_1(self): 323 | """Docstring.""" 324 | yield 1 325 | ''', 326 | (f"6:8 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}",), 327 | id="method single yield value yields not in docstring", 328 | ), 329 | pytest.param( 330 | ''' 331 | def function_1(): 332 | """Docstring.""" 333 | if True: 334 | yield 1 335 | ''', 336 | (f"5:8 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}",), 337 | id="function single nested yield value yields not in docstring", 338 | ), 339 | pytest.param( 340 | ''' 341 | def function_1(): 342 | """Docstring.""" 343 | yield 11 344 | yield 12 345 | ''', 346 | ( 347 | f"4:4 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}", 348 | f"5:4 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}", 349 | ), 350 | id="function multiple yield value yields not in docstring", 351 | ), 352 | pytest.param( 353 | ''' 354 | def function_1(): 355 | """Docstring.""" 356 | yield 11 357 | yield 358 | ''', 359 | (f"4:4 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}",), 360 | id="function multiple yield first value yields not in docstring", 361 | ), 362 | pytest.param( 363 | ''' 364 | def function_1(): 365 | """Docstring.""" 366 | yield 367 | yield 12 368 | ''', 369 | (f"5:4 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}",), 370 | id="function multiple yield second value yields not in docstring", 371 | ), 372 | pytest.param( 373 | ''' 374 | def function_1(): 375 | """Docstring.""" 376 | yield from tuple() 377 | yield from list() 378 | ''', 379 | ( 380 | f"4:4 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}", 381 | f"5:4 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}", 382 | ), 383 | id="function multiple yield from value yields not in docstring", 384 | ), 385 | pytest.param( 386 | ''' 387 | def function_1(): 388 | """Docstring.""" 389 | yield from tuple() 390 | yield 391 | ''', 392 | (f"4:4 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}",), 393 | id="function multiple yield from first value yields not in docstring", 394 | ), 395 | pytest.param( 396 | ''' 397 | def function_1(): 398 | """Docstring.""" 399 | yield 400 | yield from list() 401 | ''', 402 | (f"5:4 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}",), 403 | id="function multiple yield from second value yields not in docstring", 404 | ), 405 | pytest.param( 406 | ''' 407 | def function_1(): 408 | """Docstring. 409 | 410 | Yields: 411 | """ 412 | ''', 413 | (f"3:4 {YIELDS_SECTION_IN_DOCSTR_MSG}",), 414 | id="function no yield yields in docstring", 415 | ), 416 | pytest.param( 417 | ''' 418 | def _function_1(): 419 | """Docstring. 420 | 421 | Yields: 422 | """ 423 | ''', 424 | (f"3:4 {YIELDS_SECTION_IN_DOCSTR_MSG}",), 425 | id="private function no yield yields in docstring", 426 | ), 427 | pytest.param( 428 | ''' 429 | class Class1: 430 | """Docstring.""" 431 | def function_1(): 432 | """Docstring. 433 | 434 | Yields: 435 | """ 436 | ''', 437 | (f"5:8 {YIELDS_SECTION_IN_DOCSTR_MSG}",), 438 | id="method no yield yields in docstring", 439 | ), 440 | pytest.param( 441 | ''' 442 | def function_1(): 443 | """Docstring. 444 | 445 | Yields: 446 | """ 447 | yield 448 | ''', 449 | (f"3:4 {YIELDS_SECTION_IN_DOCSTR_MSG}",), 450 | id="function yield no value yields in docstring", 451 | ), 452 | pytest.param( 453 | ''' 454 | def function_1(): 455 | """Docstring. 456 | 457 | Yields: 458 | 459 | Yields: 460 | """ 461 | yield 1 462 | ''', 463 | (f"3:4 {MULT_YIELDS_SECTIONS_IN_DOCSTR_MSG % 'Yields,Yields'}",), 464 | id="function yield multiple yields in docstring", 465 | ), 466 | pytest.param( 467 | ''' 468 | def function_1(): 469 | """Docstring. 470 | 471 | Yields: 472 | 473 | Yields: 474 | """ 475 | yield from tuple() 476 | ''', 477 | (f"3:4 {MULT_YIELDS_SECTIONS_IN_DOCSTR_MSG % 'Yields,Yields'}",), 478 | id="function yield from multiple yields in docstring", 479 | ), 480 | pytest.param( 481 | ''' 482 | class Class1: 483 | """Docstring.""" 484 | def function_1(): 485 | """Docstring. 486 | 487 | Yields: 488 | 489 | Yields: 490 | """ 491 | yield 1 492 | ''', 493 | (f"5:8 {MULT_YIELDS_SECTIONS_IN_DOCSTR_MSG % 'Yields,Yields'}",), 494 | id="method yield multiple yields in docstring", 495 | ), 496 | pytest.param( 497 | ''' 498 | async def function_1(): 499 | """Docstring 1.""" 500 | ''', 501 | (), 502 | id="function docstring", 503 | ), 504 | pytest.param( 505 | ''' 506 | async def function_1(): 507 | """Docstring 1.""" 508 | 509 | async def function_2(): 510 | """Docstring 2.""" 511 | ''', 512 | (), 513 | id="multiple functions docstring", 514 | ), 515 | pytest.param( 516 | ''' 517 | def function_1(): 518 | """Docstring 1.""" 519 | ''', 520 | (), 521 | id="async function docstring", 522 | ), 523 | pytest.param( 524 | ''' 525 | class Class1: 526 | """Docstring.""" 527 | def function_1(self): 528 | return 529 | ''', 530 | (f"4:4 {DOCSTR_MISSING_MSG}",), 531 | id="method docstring missing return", 532 | ), 533 | pytest.param( 534 | ''' 535 | def function_1(): 536 | """Docstring 1.""" 537 | return 538 | ''', 539 | (), 540 | id="function return no value docstring no returns section", 541 | ), 542 | pytest.param( 543 | ''' 544 | def function_1(): 545 | """Docstring 1. 546 | 547 | Returns: 548 | """ 549 | return 1 550 | ''', 551 | (), 552 | id="function return value docstring returns section", 553 | ), 554 | pytest.param( 555 | ''' 556 | def _function_1(): 557 | """Docstring 1. 558 | 559 | Returns: 560 | """ 561 | return 1 562 | ''', 563 | (), 564 | id="private function return value docstring returns section", 565 | ), 566 | pytest.param( 567 | ''' 568 | def function_1(): 569 | """Docstring 1.""" 570 | def function_2(): 571 | """Docstring 2. 572 | 573 | Returns: 574 | """ 575 | return 1 576 | ''', 577 | (), 578 | id="function return value in nested function docstring no returns section", 579 | ), 580 | pytest.param( 581 | ''' 582 | def function_1(): 583 | """Docstring 1.""" 584 | async def function_2(): 585 | """Docstring 2. 586 | 587 | Returns: 588 | """ 589 | return 1 590 | ''', 591 | (), 592 | id="function return value in nested async function docstring no returns section", 593 | ), 594 | pytest.param( 595 | ''' 596 | def function_1(): 597 | """Docstring 1.""" 598 | class Class1: 599 | """Docstring.""" 600 | return 1 601 | ''', 602 | (), 603 | id="function return value in class docstring no returns section", 604 | ), 605 | pytest.param( 606 | ''' 607 | def function_1(): 608 | """Docstring 1. 609 | 610 | Returns: 611 | """ 612 | return 1 613 | return 2 614 | ''', 615 | (), 616 | id="function multiple return values docstring returns section", 617 | ), 618 | pytest.param( 619 | ''' 620 | class Class1: 621 | """Docstring.""" 622 | def function_1(self): 623 | """Docstring 1. 624 | 625 | Returns: 626 | """ 627 | return 1 628 | ''', 629 | (), 630 | id="method return value docstring returns section", 631 | ), 632 | pytest.param( 633 | ''' 634 | class Class1: 635 | """Docstring. 636 | 637 | Attrs: 638 | function_1: 639 | """ 640 | @property 641 | def function_1(self): 642 | """Docstring 1.""" 643 | return 1 644 | ''', 645 | (), 646 | id="property return value docstring no returns section", 647 | ), 648 | pytest.param( 649 | ''' 650 | class Class1: 651 | """Docstring. 652 | 653 | Attrs: 654 | function_1: 655 | """ 656 | @cached_property 657 | def function_1(self): 658 | """Docstring 1.""" 659 | return 1 660 | ''', 661 | (), 662 | id="cached_property return value docstring no returns section", 663 | ), 664 | pytest.param( 665 | ''' 666 | class Class1: 667 | """Docstring. 668 | 669 | Attrs: 670 | function_1: 671 | """ 672 | @functools.cached_property 673 | def function_1(self): 674 | """Docstring 1.""" 675 | return 1 676 | ''', 677 | (), 678 | id="functools.cached_property return value docstring no returns section", 679 | ), 680 | pytest.param( 681 | ''' 682 | class Class1: 683 | """Docstring. 684 | 685 | Attrs: 686 | function_1: 687 | """ 688 | @property 689 | async def function_1(self): 690 | """Docstring 1.""" 691 | return 1 692 | ''', 693 | (), 694 | id="async property return value docstring no returns section", 695 | ), 696 | pytest.param( 697 | ''' 698 | class Class1: 699 | """Docstring. 700 | 701 | Attrs: 702 | function_1: 703 | """ 704 | @property() 705 | def function_1(self): 706 | """Docstring 1.""" 707 | return 1 708 | ''', 709 | (), 710 | id="property call return value docstring no returns section", 711 | ), 712 | pytest.param( 713 | ''' 714 | def function_1(): 715 | """Docstring 1.""" 716 | yield 717 | ''', 718 | (), 719 | id="function yield no value docstring no yields section", 720 | ), 721 | pytest.param( 722 | ''' 723 | def function_1(): 724 | """Docstring 1. 725 | 726 | Yields: 727 | """ 728 | yield 1 729 | ''', 730 | (), 731 | id="function yield value docstring yields section", 732 | ), 733 | pytest.param( 734 | ''' 735 | def _function_1(): 736 | """Docstring 1. 737 | 738 | Yields: 739 | """ 740 | yield 1 741 | ''', 742 | (), 743 | id="private function yield value docstring yields section", 744 | ), 745 | pytest.param( 746 | ''' 747 | def function_1(): 748 | """Docstring 1. 749 | 750 | Yields: 751 | """ 752 | yield from tuple() 753 | ''', 754 | (), 755 | id="function yield from docstring yields section", 756 | ), 757 | pytest.param( 758 | ''' 759 | def function_1(): 760 | """Docstring 1.""" 761 | def function_2(): 762 | """Docstring 2. 763 | 764 | Yields: 765 | """ 766 | yield 1 767 | ''', 768 | (), 769 | id="function yield value in nested function docstring no yields section", 770 | ), 771 | pytest.param( 772 | ''' 773 | def function_1(): 774 | """Docstring 1.""" 775 | async def function_2(): 776 | """Docstring 2. 777 | 778 | Yields: 779 | """ 780 | yield 1 781 | ''', 782 | (), 783 | id="function yield value in nested async function docstring no yields section", 784 | ), 785 | pytest.param( 786 | ''' 787 | def function_1(): 788 | """Docstring 1.""" 789 | class Class1: 790 | """Docstring.""" 791 | yield 1 792 | ''', 793 | (), 794 | id="function yield value in class docstring no yields section", 795 | ), 796 | pytest.param( 797 | ''' 798 | def function_1(): 799 | """Docstring 1. 800 | 801 | Yields: 802 | """ 803 | yield 1 804 | yield 2 805 | ''', 806 | (), 807 | id="function multiple yield values docstring yields section", 808 | ), 809 | pytest.param( 810 | ''' 811 | class Class1: 812 | """Docstring.""" 813 | def function_1(self): 814 | """Docstring 1. 815 | 816 | Yields: 817 | """ 818 | yield 1 819 | ''', 820 | (), 821 | id="method yield value docstring yields section", 822 | ), 823 | pytest.param( 824 | ''' 825 | class Class1: 826 | """Docstring. 827 | 828 | Attrs: 829 | function_1: 830 | """ 831 | @property 832 | def function_1(self): 833 | """Docstring 1.""" 834 | yield 1 835 | ''', 836 | (), 837 | id="property yield value docstring no yields section", 838 | ), 839 | ], 840 | ) 841 | def test_plugin(code: str, expected_result: tuple[str, ...]): 842 | """ 843 | given: code 844 | when: linting is run on the code 845 | then: the expected result is returned 846 | """ 847 | assert result.get(code) == expected_result 848 | 849 | 850 | @pytest.mark.parametrize( 851 | "code, filename, expected_result", 852 | [ 853 | pytest.param( 854 | """ 855 | def test_(): 856 | pass 857 | """, 858 | "source.py", 859 | (f"2:0 {DOCSTR_MISSING_MSG}",), 860 | id="not test file", 861 | ), 862 | pytest.param( 863 | """ 864 | def foo(): 865 | pass 866 | """, 867 | "test_.py", 868 | (f"2:0 {DOCSTR_MISSING_MSG}",), 869 | id="test file not test function", 870 | ), 871 | pytest.param( 872 | """ 873 | def test_(): 874 | pass 875 | """, 876 | "test_.py", 877 | (), 878 | id="test file test function", 879 | ), 880 | pytest.param( 881 | """ 882 | def test_(): 883 | pass 884 | """, 885 | "tests/test_.py", 886 | (), 887 | id="test file test function in directory", 888 | ), 889 | pytest.param( 890 | """ 891 | def foo(): 892 | pass 893 | """, 894 | "conftest.py", 895 | (f"2:0 {DOCSTR_MISSING_MSG}",), 896 | id="normal file not fixture function", 897 | ), 898 | pytest.param( 899 | """ 900 | @fixture 901 | def foo(): 902 | pass 903 | """, 904 | "source.py", 905 | (f"3:0 {DOCSTR_MISSING_MSG}",), 906 | id="source file fixture function", 907 | ), 908 | pytest.param( 909 | """ 910 | @fixture 911 | def foo(): 912 | pass 913 | """, 914 | "conftest.py", 915 | (), 916 | id="fixture file fixture function", 917 | ), 918 | pytest.param( 919 | """ 920 | @fixture 921 | def foo(): 922 | pass 923 | """, 924 | "test_.py", 925 | (), 926 | id="test file fixture function", 927 | ), 928 | pytest.param( 929 | """ 930 | @FIXTURE 931 | def foo(): 932 | pass 933 | """, 934 | "conftest.py", 935 | (), 936 | id="fixture file fixture function capitalised", 937 | ), 938 | pytest.param( 939 | """ 940 | @fixture 941 | @decorator 942 | def foo(): 943 | pass 944 | """, 945 | "conftest.py", 946 | (), 947 | id="fixture file fixture function multiple decorators first", 948 | ), 949 | pytest.param( 950 | """ 951 | @decorator 952 | @fixture 953 | def foo(): 954 | pass 955 | """, 956 | "conftest.py", 957 | (), 958 | id="fixture file fixture function multiple decorators second", 959 | ), 960 | pytest.param( 961 | """ 962 | @pytest.fixture 963 | def foo(): 964 | pass 965 | """, 966 | "conftest.py", 967 | (), 968 | id="fixture file fixture function prefix", 969 | ), 970 | pytest.param( 971 | """ 972 | @pytest.fixture(scope="module") 973 | def foo(): 974 | pass 975 | """, 976 | "conftest.py", 977 | (), 978 | id="fixture file fixture function prefix call", 979 | ), 980 | pytest.param( 981 | """ 982 | @additional.pytest.fixture 983 | def foo(): 984 | pass 985 | """, 986 | "conftest.py", 987 | (), 988 | id="fixture file fixture function nested prefix", 989 | ), 990 | pytest.param( 991 | """ 992 | @fixture(scope="module") 993 | def foo(): 994 | pass 995 | """, 996 | "conftest.py", 997 | (), 998 | id="fixture file fixture function arguments", 999 | ), 1000 | pytest.param( 1001 | """ 1002 | @fixture 1003 | def foo(): 1004 | pass 1005 | """, 1006 | "tests/conftest.py", 1007 | (), 1008 | id="fixture file fixture function in directory", 1009 | ), 1010 | ], 1011 | ) 1012 | def test_plugin_filename(code: str, filename: str, expected_result: tuple[str, ...]): 1013 | """ 1014 | given: code and filename 1015 | when: linting is run on the code 1016 | then: the expected result is returned 1017 | """ 1018 | assert result.get(code, filename) == expected_result 1019 | -------------------------------------------------------------------------------- /tests/unit/test___init__attrs.py: -------------------------------------------------------------------------------- 1 | """Unit tests for attrs checks in the plugin.""" 2 | 3 | # The lines represent the number of test cases 4 | # pylint: disable=too-many-lines 5 | 6 | from __future__ import annotations 7 | 8 | import pytest 9 | 10 | from flake8_docstrings_complete import DOCSTR_MISSING_MSG 11 | from flake8_docstrings_complete.attrs import ( 12 | ATTR_IN_DOCSTR_MSG, 13 | ATTR_NOT_IN_DOCSTR_MSG, 14 | ATTRS_SECTION_IN_DOCSTR_MSG, 15 | ATTRS_SECTION_NOT_IN_DOCSTR_MSG, 16 | DUPLICATE_ATTR_MSG, 17 | MULT_ATTRS_SECTIONS_IN_DOCSTR_MSG, 18 | ) 19 | 20 | from . import result 21 | 22 | 23 | @pytest.mark.parametrize( 24 | "code, expected_result", 25 | [ 26 | pytest.param( 27 | """ 28 | class Class1: 29 | pass 30 | """, 31 | (f"2:0 {DOCSTR_MISSING_MSG}",), 32 | id="class no docstring", 33 | ), 34 | pytest.param( 35 | ''' 36 | class Class1: 37 | """Docstring 1.""" 38 | attr_1 = "value 1" 39 | ''', 40 | (f"3:4 {ATTRS_SECTION_NOT_IN_DOCSTR_MSG}",), 41 | id="class has single class attr docstring no attrs section", 42 | ), 43 | pytest.param( 44 | ''' 45 | class Class1: 46 | """Docstring 1.""" 47 | attr_1 = "value 1" 48 | 49 | class Class2: 50 | """Docstring 2.""" 51 | attr_2 = "value 2" 52 | ''', 53 | ( 54 | f"3:4 {ATTRS_SECTION_NOT_IN_DOCSTR_MSG}", 55 | f"7:4 {ATTRS_SECTION_NOT_IN_DOCSTR_MSG}", 56 | ), 57 | id="multiple class has single class attr docstring no attrs section", 58 | ), 59 | pytest.param( 60 | ''' 61 | class Class1: 62 | """Docstring 1. 63 | 64 | Attrs: 65 | attr_1: 66 | """ 67 | ''', 68 | (f"3:4 {ATTRS_SECTION_IN_DOCSTR_MSG}",), 69 | id="class has no attrs docstring attrs section", 70 | ), 71 | pytest.param( 72 | ''' 73 | class Class1: 74 | """Docstring 1. 75 | 76 | Attrs: 77 | attr_1: 78 | 79 | Attrs: 80 | attr_1: 81 | """ 82 | attr_1 = "value 1" 83 | ''', 84 | (f"3:4 {MULT_ATTRS_SECTIONS_IN_DOCSTR_MSG % 'Attrs,Attrs'}",), 85 | id="class has single attrs docstring multiple attrs sections same name", 86 | ), 87 | pytest.param( 88 | ''' 89 | class Class1: 90 | """Docstring 1. 91 | 92 | Attrs: 93 | attr_1: 94 | 95 | Attributes: 96 | attr_1: 97 | """ 98 | attr_1 = "value 1" 99 | ''', 100 | (f"3:4 {MULT_ATTRS_SECTIONS_IN_DOCSTR_MSG % 'Attrs,Attributes'}",), 101 | id="class has single attrs docstring multiple attrs sections alternate name", 102 | ), 103 | pytest.param( 104 | ''' 105 | class Class1: 106 | """Docstring 1. 107 | 108 | Attrs: 109 | """ 110 | attr_1 = "value 1" 111 | ''', 112 | (f"7:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), 113 | id="class has single attr docstring no attr", 114 | ), 115 | pytest.param( 116 | ''' 117 | class Class1: 118 | """Docstring 1. 119 | 120 | Attrs: 121 | """ 122 | attr_1 = attr_2 = "value 1" 123 | ''', 124 | ( 125 | f"7:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}", 126 | f"7:13 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_2'}", 127 | ), 128 | id="class has multiple assign attr docstring no attr", 129 | ), 130 | pytest.param( 131 | ''' 132 | class Class1: 133 | """Docstring 1. 134 | 135 | Attrs: 136 | """ 137 | attr_1.nested_attr_1 = "value 1" 138 | ''', 139 | (f"7:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), 140 | id="class has single nested attr docstring no attr", 141 | ), 142 | pytest.param( 143 | ''' 144 | class Class1: 145 | """Docstring 1. 146 | 147 | Attrs: 148 | """ 149 | attr_1.nested_attr_1.nested_attr_2 = "value 1" 150 | ''', 151 | (f"7:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), 152 | id="class has single double nested attr docstring no attr", 153 | ), 154 | pytest.param( 155 | ''' 156 | class Class1: 157 | """Docstring 1. 158 | 159 | Attrs: 160 | """ 161 | _attr_1 = "value 1" 162 | ''', 163 | (f"3:4 {ATTRS_SECTION_IN_DOCSTR_MSG}",), 164 | id="class has single unused attr docstring attrs", 165 | ), 166 | pytest.param( 167 | ''' 168 | class Class1: 169 | """Docstring 1. 170 | 171 | Attrs: 172 | """ 173 | @property 174 | def attr_1(): 175 | """Docstring 2.""" 176 | return "value 1" 177 | ''', 178 | (f"8:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), 179 | id="class has single property docstring no attr", 180 | ), 181 | pytest.param( 182 | ''' 183 | class Class1: 184 | """Docstring 1. 185 | 186 | Attrs: 187 | """ 188 | @cached_property 189 | def attr_1(): 190 | """Docstring 2.""" 191 | return "value 1" 192 | ''', 193 | (f"8:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), 194 | id="class has single cached_property docstring no attr", 195 | ), 196 | pytest.param( 197 | ''' 198 | class Class1: 199 | """Docstring 1. 200 | 201 | Attrs: 202 | """ 203 | @functools.cached_property 204 | def attr_1(): 205 | """Docstring 2.""" 206 | return "value 1" 207 | ''', 208 | (f"8:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), 209 | id="class has single functools.cached_property docstring no attr", 210 | ), 211 | pytest.param( 212 | ''' 213 | class Class1: 214 | """Docstring 1. 215 | 216 | Attrs: 217 | """ 218 | @property 219 | def attr_1(self): 220 | """Docstring 2.""" 221 | self.attr_2 = "value 2" 222 | return "value 1" 223 | ''', 224 | (f"8:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), 225 | id="class has single property with assignment docstring no attr", 226 | ), 227 | pytest.param( 228 | ''' 229 | class Class1: 230 | """Docstring 1. 231 | 232 | Attrs: 233 | """ 234 | @property 235 | async def attr_1(): 236 | """Docstring 2.""" 237 | return "value 1" 238 | ''', 239 | (f"8:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), 240 | id="class has single async property docstring no attr", 241 | ), 242 | pytest.param( 243 | ''' 244 | class Class1: 245 | """Docstring 1. 246 | 247 | Attrs: 248 | """ 249 | @property() 250 | def attr_1(): 251 | """Docstring 2.""" 252 | return "value 1" 253 | ''', 254 | (f"8:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), 255 | id="class has single property call docstring no attr", 256 | ), 257 | pytest.param( 258 | ''' 259 | class Class1: 260 | """Docstring 1. 261 | 262 | Attrs: 263 | """ 264 | def __init__(self): 265 | """Docstring 2.""" 266 | attr_1 = "value 1" 267 | ''', 268 | (f"9:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), 269 | id="class has single attr after init docstring no attr", 270 | ), 271 | pytest.param( 272 | ''' 273 | class Class1: 274 | """Docstring 1. 275 | 276 | Attrs: 277 | """ 278 | @property 279 | def attr_1(): 280 | """Docstring 2.""" 281 | return "value 1" 282 | @property 283 | def attr_2(): 284 | """Docstring 3.""" 285 | return "value 3" 286 | ''', 287 | ( 288 | f"8:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}", 289 | f"12:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_2'}", 290 | ), 291 | id="class has multiple property docstring no attr", 292 | ), 293 | pytest.param( 294 | ''' 295 | class Class1: 296 | """Docstring 1. 297 | 298 | Attrs: 299 | """ 300 | attr_1 = "value 1" 301 | attr_2 = "value 2" 302 | ''', 303 | ( 304 | f"7:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}", 305 | f"8:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_2'}", 306 | ), 307 | id="class multiple attrs docstring no attr", 308 | ), 309 | pytest.param( 310 | ''' 311 | class Class1: 312 | """Docstring 1. 313 | 314 | Attrs: 315 | """ 316 | _attr_1 = "value 1" 317 | attr_2 = "value 2" 318 | ''', 319 | (f"8:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_2'}",), 320 | id="class multiple attrs first private docstring no attr", 321 | ), 322 | pytest.param( 323 | ''' 324 | class Class1: 325 | """Docstring 1. 326 | 327 | Attrs: 328 | """ 329 | attr_1 = "value 1" 330 | _attr_2 = "value 2" 331 | ''', 332 | (f"7:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), 333 | id="class multiple attrs second private docstring no attr", 334 | ), 335 | pytest.param( 336 | ''' 337 | class Class1: 338 | """Docstring 1. 339 | 340 | Attrs: 341 | attr_1: 342 | """ 343 | attr_1 = "value 1" 344 | attr_2 = "value 2" 345 | ''', 346 | (f"9:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_2'}",), 347 | id="class multiple attrs docstring single attr first", 348 | ), 349 | pytest.param( 350 | ''' 351 | class Class1: 352 | """Docstring 1. 353 | 354 | Attrs: 355 | attr_2: 356 | """ 357 | attr_1 = "value 1" 358 | attr_2 = "value 2" 359 | ''', 360 | (f"8:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), 361 | id="class multiple attrs docstring single attr second", 362 | ), 363 | pytest.param( 364 | ''' 365 | class Class1: 366 | """Docstring 1. 367 | 368 | Attrs: 369 | attr_2: 370 | """ 371 | attr_1 = "value 1" 372 | ''', 373 | ( 374 | f"8:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}", 375 | f"3:4 {ATTR_IN_DOCSTR_MSG % 'attr_2'}", 376 | ), 377 | id="class has single attr docstring attr different", 378 | ), 379 | pytest.param( 380 | ''' 381 | class Class1: 382 | """Docstring 1. 383 | 384 | Attrs: 385 | """ 386 | attr_1: str = "value 1" 387 | ''', 388 | (f"7:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), 389 | id="class has single typed attr docstring no attr", 390 | ), 391 | pytest.param( 392 | ''' 393 | class Class1: 394 | """Docstring 1. 395 | 396 | Attrs: 397 | """ 398 | attr_1 += "value 1" 399 | ''', 400 | (f"7:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), 401 | id="class has single augmented attr docstring no attr", 402 | ), 403 | pytest.param( 404 | ''' 405 | class Class1: 406 | """Docstring 1. 407 | 408 | Attrs: 409 | attr_2: 410 | attr_3: 411 | """ 412 | attr_1 = "value 1" 413 | ''', 414 | ( 415 | f"9:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}", 416 | f"3:4 {ATTR_IN_DOCSTR_MSG % 'attr_2'}", 417 | f"3:4 {ATTR_IN_DOCSTR_MSG % 'attr_3'}", 418 | ), 419 | id="class single attr docstring multiple attrs different", 420 | ), 421 | pytest.param( 422 | ''' 423 | class Class1: 424 | """Docstring 1. 425 | 426 | Attrs: 427 | attr_3: 428 | attr_4: 429 | """ 430 | attr_1 = "value 1" 431 | attr_2 = "value 2" 432 | ''', 433 | ( 434 | f"9:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}", 435 | f"10:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_2'}", 436 | f"3:4 {ATTR_IN_DOCSTR_MSG % 'attr_3'}", 437 | f"3:4 {ATTR_IN_DOCSTR_MSG % 'attr_4'}", 438 | ), 439 | id="class multiple attr docstring multiple attrs different", 440 | ), 441 | pytest.param( 442 | ''' 443 | class Class1: 444 | """Docstring 1. 445 | 446 | Attrs: 447 | attr_2: 448 | attr_3: 449 | """ 450 | attr_1 = "value 1" 451 | attr_2 = "value 2" 452 | ''', 453 | (f"9:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}", f"3:4 {ATTR_IN_DOCSTR_MSG % 'attr_3'}"), 454 | id="class multiple attr docstring multiple attrs first different", 455 | ), 456 | pytest.param( 457 | ''' 458 | class Class1: 459 | """Docstring 1. 460 | 461 | Attrs: 462 | attr_1: 463 | attr_3: 464 | """ 465 | attr_1 = "value 1" 466 | attr_2 = "value 2" 467 | ''', 468 | (f"10:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_2'}", f"3:4 {ATTR_IN_DOCSTR_MSG % 'attr_3'}"), 469 | id="class multiple attr docstring multiple attrs second different", 470 | ), 471 | pytest.param( 472 | ''' 473 | class Class1: 474 | """Docstring 1. 475 | 476 | Attrs: 477 | attr_1: 478 | attr_1: 479 | """ 480 | attr_1 = "value 1" 481 | ''', 482 | (f"3:4 {DUPLICATE_ATTR_MSG % 'attr_1'}",), 483 | id="class single attr docstring single attr duplicate", 484 | ), 485 | pytest.param( 486 | ''' 487 | class Class1: 488 | """Docstring 1. 489 | 490 | Attrs: 491 | _attr_1: 492 | _attr_1: 493 | """ 494 | _attr_1 = "value 1" 495 | ''', 496 | (f"3:4 {DUPLICATE_ATTR_MSG % '_attr_1'}",), 497 | id="class single private attr docstring single attr duplicate", 498 | ), 499 | pytest.param( 500 | ''' 501 | class Class1: 502 | """Docstring 1. 503 | 504 | Attrs: 505 | attr_1: 506 | attr_1: 507 | attr_1: 508 | """ 509 | attr_1 = "value 1" 510 | ''', 511 | (f"3:4 {DUPLICATE_ATTR_MSG % 'attr_1'}",), 512 | id="class single attr docstring single attr duplicate many", 513 | ), 514 | pytest.param( 515 | ''' 516 | class Class1: 517 | """Docstring 1. 518 | 519 | Attrs: 520 | attr_1: 521 | attr_1: 522 | attr_2: 523 | """ 524 | attr_1 = "value 1" 525 | attr_2 = "value 2" 526 | ''', 527 | (f"3:4 {DUPLICATE_ATTR_MSG % 'attr_1'}",), 528 | id="class multiple attr docstring duplicate attr first", 529 | ), 530 | pytest.param( 531 | ''' 532 | class Class1: 533 | """Docstring 1. 534 | 535 | Attrs: 536 | attr_1: 537 | attr_2: 538 | attr_2: 539 | """ 540 | attr_1 = "value 1" 541 | attr_2 = "value 2" 542 | ''', 543 | (f"3:4 {DUPLICATE_ATTR_MSG % 'attr_2'}",), 544 | id="class multiple attr docstring duplicate attr second", 545 | ), 546 | pytest.param( 547 | ''' 548 | class Class1: 549 | """Docstring 1. 550 | 551 | Attrs: 552 | attr_1: 553 | attr_1: 554 | attr_2: 555 | attr_2: 556 | """ 557 | attr_1 = "value 1" 558 | attr_2 = "value 2" 559 | ''', 560 | ( 561 | f"3:4 {DUPLICATE_ATTR_MSG % 'attr_1'}", 562 | f"3:4 {DUPLICATE_ATTR_MSG % 'attr_2'}", 563 | ), 564 | id="class multiple attr docstring duplicate attr all", 565 | ), 566 | pytest.param( 567 | ''' 568 | class Class1: 569 | """Docstring 1. 570 | 571 | Attrs: 572 | attr_1: 573 | attr_1: 574 | """ 575 | def __init__(self): 576 | """Docstring 2.""" 577 | self.attr_1 = "value 1" 578 | ''', 579 | (f"3:4 {DUPLICATE_ATTR_MSG % 'attr_1'}",), 580 | id="class single attr init docstring single attr duplicate", 581 | ), 582 | pytest.param( 583 | ''' 584 | class Class1: 585 | """Docstring 1. 586 | 587 | Attrs: 588 | attr_1: 589 | """ 590 | attr_1 = "value 1" 591 | ''', 592 | (), 593 | id="class single attr docstring single attr", 594 | ), 595 | pytest.param( 596 | ''' 597 | class Class1: 598 | """Docstring 1. 599 | 600 | Attrs: 601 | attr_1: 602 | """ 603 | @property 604 | def attr_1(): 605 | """Docstring 2.""" 606 | return "value 1" 607 | ''', 608 | (), 609 | id="class single property docstring single attr", 610 | ), 611 | pytest.param( 612 | ''' 613 | class Class1: 614 | """Docstring 1. 615 | 616 | Attrs: 617 | attr_1: 618 | """ 619 | @cached_property 620 | def attr_1(): 621 | """Docstring 2.""" 622 | return "value 1" 623 | ''', 624 | (), 625 | id="class single cached_property docstring single attr", 626 | ), 627 | pytest.param( 628 | ''' 629 | class Class1: 630 | """Docstring 1. 631 | 632 | Attrs: 633 | attr_1: 634 | """ 635 | @functools.cached_property 636 | def attr_1(): 637 | """Docstring 2.""" 638 | return "value 1" 639 | ''', 640 | (), 641 | id="class single functools.cached_property docstring single attr", 642 | ), 643 | pytest.param( 644 | ''' 645 | class Class1: 646 | """Docstring 1. 647 | 648 | Attrs: 649 | attr_1: 650 | """ 651 | attr_1: str = "value 1" 652 | ''', 653 | (), 654 | id="class single attr typed docstring single attr", 655 | ), 656 | pytest.param( 657 | ''' 658 | class Class1: 659 | """Docstring 1. 660 | 661 | Attrs: 662 | attr_1: 663 | """ 664 | attr_1 += "value 1" 665 | ''', 666 | (), 667 | id="class single attr augmented docstring single attr", 668 | ), 669 | pytest.param( 670 | ''' 671 | class Class1: 672 | """Docstring 1. 673 | 674 | Attrs: 675 | attr_1: 676 | """ 677 | def __init__(self): 678 | """Docstring 2.""" 679 | self.attr_1 = "value 1" 680 | ''', 681 | (), 682 | id="class single attr init docstring single attr", 683 | ), 684 | pytest.param( 685 | ''' 686 | class Class1: 687 | """Docstring 1. 688 | 689 | Attrs: 690 | attr_1: 691 | """ 692 | def method_1(self): 693 | """Docstring 2.""" 694 | self.attr_1 = "value 1" 695 | ''', 696 | (), 697 | id="class single attr method docstring single attr", 698 | ), 699 | pytest.param( 700 | ''' 701 | class Class1: 702 | """Docstring 1. 703 | 704 | Attrs: 705 | attr_1: 706 | attr_2: 707 | """ 708 | def method_1(self): 709 | """Docstring 2.""" 710 | self.attr_1 = "value 1" 711 | def method_2(self): 712 | """Docstring 3.""" 713 | self.attr_2 = "value 2" 714 | ''', 715 | (), 716 | id="class multiple attr method docstring single attr", 717 | ), 718 | pytest.param( 719 | ''' 720 | class Class1: 721 | """Docstring 1. 722 | 723 | Attrs: 724 | attr_1: 725 | """ 726 | @classmethod 727 | def method_1(cls): 728 | """Docstring 2.""" 729 | cls.attr_1 = "value 1" 730 | ''', 731 | (), 732 | id="class single attr classmethod docstring single attr", 733 | ), 734 | pytest.param( 735 | ''' 736 | class Class1: 737 | """Docstring 1. 738 | 739 | Attrs: 740 | _attr_1: 741 | """ 742 | _attr_1 = "value 1" 743 | ''', 744 | (), 745 | id="class single private attr docstring single attr", 746 | ), 747 | pytest.param( 748 | ''' 749 | class Class1: 750 | """Docstring 1.""" 751 | _attr_1 = "value 1" 752 | ''', 753 | (), 754 | id="class single private attr docstring single attr", 755 | ), 756 | pytest.param( 757 | ''' 758 | class Class1: 759 | """Docstring 1.""" 760 | def __init__(self): 761 | """Docstring 2.""" 762 | var_1 = "value 1" 763 | ''', 764 | (), 765 | id="class single var init docstring single attr", 766 | ), 767 | pytest.param( 768 | ''' 769 | class Class1: 770 | """Docstring 1. 771 | 772 | Attrs: 773 | attr_1: 774 | """ 775 | @property 776 | def attr_1(self): 777 | """Docstring 2.""" 778 | self.attr_2 = "value 2" 779 | return "value 1" 780 | ''', 781 | (), 782 | id="class has single property with assignment docstring single attr", 783 | ), 784 | pytest.param( 785 | ''' 786 | class Class1: 787 | """Docstring 1. 788 | 789 | Attrs: 790 | attr_1: 791 | attr_2: 792 | """ 793 | @property 794 | def attr_1(self): 795 | """Docstring 2.""" 796 | self.attr_2 = "value 2" 797 | return "value 1" 798 | ''', 799 | (), 800 | id="class has single property with assignment docstring both attr", 801 | ), 802 | pytest.param( 803 | ''' 804 | class Class1: 805 | """Docstring 1.""" 806 | def __init__(self): 807 | """Docstring 2.""" 808 | self.attr_1 = "value 1" 809 | ''', 810 | (), 811 | id="class has single attr in init docstring no attr", 812 | ), 813 | pytest.param( 814 | ''' 815 | class Class1: 816 | """Docstring 1.""" 817 | def method_1(self): 818 | """Docstring 2.""" 819 | self.attr_1 = "value 1" 820 | ''', 821 | (), 822 | id="class has single attr in method docstring no attr", 823 | ), 824 | pytest.param( 825 | ''' 826 | class Class1: 827 | """Docstring 1.""" 828 | def method_1(self): 829 | """Docstring 2.""" 830 | self.attr_1: str = "value 1" 831 | ''', 832 | (), 833 | id="class has single attr typed in method docstring no attr", 834 | ), 835 | pytest.param( 836 | ''' 837 | class Class1: 838 | """Docstring 1. 839 | 840 | Attrs: 841 | attr_1: 842 | """ 843 | def method_1(self): 844 | """Docstring 2.""" 845 | self.attr_1: str = "value 1" 846 | ''', 847 | (), 848 | id="class has single attr typed in method docstring single attr", 849 | ), 850 | pytest.param( 851 | ''' 852 | class Class1: 853 | """Docstring 1.""" 854 | def method_1(self): 855 | """Docstring 2.""" 856 | self.attr_1 += "value 1" 857 | ''', 858 | (), 859 | id="class has single attr augmented in method docstring no attr", 860 | ), 861 | pytest.param( 862 | ''' 863 | class Class1: 864 | """Docstring 1. 865 | 866 | Attrs: 867 | attr_1: 868 | """ 869 | def method_1(self): 870 | """Docstring 2.""" 871 | self.attr_1 += "value 1" 872 | ''', 873 | (), 874 | id="class has single attr augmented in method docstring single attr", 875 | ), 876 | pytest.param( 877 | ''' 878 | class Class1: 879 | """Docstring 1.""" 880 | def method_1(self): 881 | """Docstring 2.""" 882 | self.attr_1 = self.attr_2 = "value 1" 883 | ''', 884 | (), 885 | id="class has multiple attr in method docstring no attr", 886 | ), 887 | pytest.param( 888 | ''' 889 | class Class1: 890 | """Docstring 1. 891 | 892 | Attrs: 893 | attr_1: 894 | attr_2: 895 | """ 896 | def method_1(self): 897 | """Docstring 2.""" 898 | self.attr_1 = self.attr_2 = "value 1" 899 | ''', 900 | (), 901 | id="class has multiple attr in method docstring multiple attr", 902 | ), 903 | pytest.param( 904 | ''' 905 | class Class1: 906 | """Docstring 1.""" 907 | def method_1(self): 908 | """Docstring 2.""" 909 | self.attr_1.nested_attr_1 = "value 1" 910 | ''', 911 | (), 912 | id="class has single attr nested in method docstring no attr", 913 | ), 914 | pytest.param( 915 | ''' 916 | class Class1: 917 | """Docstring 1. 918 | 919 | Attrs: 920 | attr_1: 921 | """ 922 | def method_1(self): 923 | """Docstring 2.""" 924 | self.attr_1.nested_attr_1 = "value 1" 925 | ''', 926 | (), 927 | id="class has single attr nested in method docstring single attr", 928 | ), 929 | pytest.param( 930 | ''' 931 | class Class1: 932 | """Docstring 1.""" 933 | def method_1(self): 934 | """Docstring 2.""" 935 | self.attr_1.nested_attr_1.nested_attr_2 = "value 1" 936 | ''', 937 | (), 938 | id="class has single attr deep nested in method docstring no attr", 939 | ), 940 | pytest.param( 941 | ''' 942 | class Class1: 943 | """Docstring 1. 944 | 945 | Attrs: 946 | attr_1: 947 | """ 948 | def method_1(self): 949 | """Docstring 2.""" 950 | self.attr_1.nested_attr_1.nested_attr_2 = "value 1" 951 | ''', 952 | (), 953 | id="class has single attr deep nested in method docstring single attr", 954 | ), 955 | pytest.param( 956 | ''' 957 | class Class1: 958 | """Docstring 1.""" 959 | def method_1(self): 960 | """Docstring 2.""" 961 | self.attr_1 = "value 1" 962 | def method_2(self): 963 | """Docstring 3.""" 964 | self.attr_2 = "value 2" 965 | ''', 966 | (), 967 | id="class has multiple attr in multiple method docstring no attr", 968 | ), 969 | pytest.param( 970 | ''' 971 | class Class1: 972 | """Docstring 1. 973 | 974 | Attrs: 975 | attr_1: 976 | """ 977 | def method_1(self): 978 | """Docstring 2.""" 979 | self.attr_1 = "value 1" 980 | def method_2(self): 981 | """Docstring 3.""" 982 | self.attr_2 = "value 2" 983 | ''', 984 | (), 985 | id="class has multiple attr in multiple method docstring single attr first", 986 | ), 987 | pytest.param( 988 | ''' 989 | class Class1: 990 | """Docstring 1. 991 | 992 | Attrs: 993 | attr_2: 994 | """ 995 | def method_1(self): 996 | """Docstring 2.""" 997 | self.attr_1 = "value 1" 998 | def method_2(self): 999 | """Docstring 3.""" 1000 | self.attr_2 = "value 2" 1001 | ''', 1002 | (), 1003 | id="class has multiple attr in multiple method docstring single attr second", 1004 | ), 1005 | pytest.param( 1006 | ''' 1007 | class Class1: 1008 | """Docstring 1. 1009 | 1010 | Attrs: 1011 | attr_1: 1012 | attr_2: 1013 | """ 1014 | def method_1(self): 1015 | """Docstring 2.""" 1016 | self.attr_1 = "value 1" 1017 | def method_2(self): 1018 | """Docstring 3.""" 1019 | self.attr_2 = "value 2" 1020 | ''', 1021 | (), 1022 | id="class has multiple attr in multiple method docstring multiple attr", 1023 | ), 1024 | pytest.param( 1025 | ''' 1026 | class Class1: 1027 | """Docstring 1.""" 1028 | async def method_1(self): 1029 | """Docstring 2.""" 1030 | self.attr_1 = "value 1" 1031 | ''', 1032 | (), 1033 | id="class has single attr in async method docstring no attr", 1034 | ), 1035 | pytest.param( 1036 | ''' 1037 | class Class1: 1038 | """Docstring 1. 1039 | 1040 | Attrs: 1041 | attr_1: 1042 | """ 1043 | async def method_1(self): 1044 | """Docstring 2.""" 1045 | self.attr_1 = "value 1" 1046 | ''', 1047 | (), 1048 | id="class has single attr in async method docstring single attr", 1049 | ), 1050 | pytest.param( 1051 | ''' 1052 | class Class1: 1053 | """Docstring 1.""" 1054 | @classmethod 1055 | def method_1(cls): 1056 | """Docstring 2.""" 1057 | cls.attr_1 = "value 1" 1058 | ''', 1059 | (), 1060 | id="class has single attr in classmethod method docstring no attr", 1061 | ), 1062 | pytest.param( 1063 | ''' 1064 | class Class1: 1065 | """Docstring 1. 1066 | 1067 | Attrs: 1068 | attr_1: 1069 | """ 1070 | @classmethod 1071 | def method_1(cls): 1072 | """Docstring 2.""" 1073 | cls.attr_1 = "value 1" 1074 | ''', 1075 | (), 1076 | id="class has single attr in classmethod method docstring single attr", 1077 | ), 1078 | pytest.param( 1079 | ''' 1080 | class Class1: 1081 | """Docstring 1.""" 1082 | def method_1(self): 1083 | """Docstring 2.""" 1084 | var_1 = "value 1" 1085 | ''', 1086 | (), 1087 | id="class single var method docstring single attr", 1088 | ), 1089 | pytest.param( 1090 | ''' 1091 | class Class1: 1092 | """Docstring 1.""" 1093 | @classmethod 1094 | def method_1(cls): 1095 | """Docstring 2.""" 1096 | var_1 = "value 1" 1097 | ''', 1098 | (), 1099 | id="class single var classmethod docstring single attr", 1100 | ), 1101 | pytest.param( 1102 | ''' 1103 | class Class1: 1104 | """Docstring 1. 1105 | 1106 | Attrs: 1107 | attr_1: 1108 | attr_2: 1109 | """ 1110 | attr_1 = "value 1" 1111 | attr_2 = "value 2" 1112 | ''', 1113 | (), 1114 | id="class multiple attr docstring multiple attr", 1115 | ), 1116 | pytest.param( 1117 | ''' 1118 | class Class1: 1119 | """Docstring 1. 1120 | 1121 | Attrs: 1122 | attr_2: 1123 | """ 1124 | _attr_1 = "value 1" 1125 | attr_2 = "value 2" 1126 | ''', 1127 | (), 1128 | id="class multiple attr first private docstring single attr", 1129 | ), 1130 | pytest.param( 1131 | ''' 1132 | class Class1: 1133 | """Docstring 1. 1134 | 1135 | Attrs: 1136 | attr_1: 1137 | """ 1138 | attr_1 = "value 1" 1139 | _attr_2 = "value 2" 1140 | ''', 1141 | (), 1142 | id="class multiple attr second private docstring single attr", 1143 | ), 1144 | pytest.param( 1145 | ''' 1146 | class Class1: 1147 | """Docstring 1.""" 1148 | class Class2: 1149 | """Docstring 2. 1150 | 1151 | Attrs: 1152 | attr_1: 1153 | """ 1154 | attr_1 = "value 1" 1155 | ''', 1156 | (), 1157 | id="nested class single attr docstring no attrs", 1158 | ), 1159 | pytest.param( 1160 | ''' 1161 | class Class1: 1162 | """Docstring 1.""" 1163 | def method_1(self): 1164 | """Docstring 2.""" 1165 | def nested_funciont_1(self): 1166 | """Docstring 3.""" 1167 | self.attr_1 = "value 1" 1168 | ''', 1169 | (), 1170 | id="class single attr method nested method docstring no attrs", 1171 | ), 1172 | pytest.param( 1173 | ''' 1174 | class Class1: 1175 | """Docstring 1.""" 1176 | def method_1(self): 1177 | """Docstring 2.""" 1178 | async def nested_funciont_1(self): 1179 | """Docstring 3.""" 1180 | self.attr_1 = "value 1" 1181 | ''', 1182 | (), 1183 | id="class single attr method nested async method docstring no attrs", 1184 | ), 1185 | pytest.param( 1186 | ''' 1187 | class Class1: 1188 | """Docstring 1.""" 1189 | def method_1(self): 1190 | """Docstring 2.""" 1191 | def nested_funciont_1(cls): 1192 | """Docstring 3.""" 1193 | cls.attr_1 = "value 1" 1194 | ''', 1195 | (), 1196 | id="class single attr method nested classmethod docstring no attrs", 1197 | ), 1198 | ], 1199 | ) 1200 | def test_plugin(code: str, expected_result: tuple[str, ...]): 1201 | """ 1202 | given: code 1203 | when: linting is run on the code 1204 | then: the expected result is returned 1205 | """ 1206 | assert result.get(code) == expected_result 1207 | --------------------------------------------------------------------------------