diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..ce638ad --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,11 @@ +{ + "name": "Python 3", + "image": "mcr.microsoft.com/devcontainers/python:0-3.11", + "features": { + "ghcr.io/devcontainers/features/python:1": {}, + "ghcr.io/devcontainers-contrib/features/black:1": {}, + "ghcr.io/devcontainers-contrib/features/tox:1": {}, + "ghcr.io/devcontainers-contrib/features/isort:1": {}, + "ghcr.io/devcontainers-contrib/features/poetry:1": {} + } +} \ No newline at end of file diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..5e00723 --- /dev/null +++ b/.flake8 @@ -0,0 +1,8 @@ +[flake8] +max-line-length = 99 +max-doc-length = 99 +extend-ignore = E203,W503 +per-file-ignores = + tests/*:D205,D400 + flake8_docstrings_complete/*:N802 +test-docs-pattern = given/when/then diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..806c04f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/workflows/ci-cd.yaml b/.github/workflows/ci-cd.yaml new file mode 100644 index 0000000..db0aac3 --- /dev/null +++ b/.github/workflows/ci-cd.yaml @@ -0,0 +1,166 @@ +name: CI-CD + +on: + push: + branches: + - main + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + pull_request: + branches: + - main + +jobs: + constants: + name: Constants + runs-on: ubuntu-latest + outputs: + package_name: ${{ steps.output.outputs.package_name }} + package_version: ${{ steps.output.outputs.package_version }} + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + - id: output + run: | + 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 + 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 + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: Install tox + run: python -m pip install tox + - name: Run linting + run: tox -e lint + test: + name: Test + runs-on: ubuntu-latest + strategy: + matrix: + python-version: + - "3.8" + - "3.9" + - "3.10" + - "3.11" + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install tox + run: python -m pip install tox + - name: Run testing + run: tox -e test + release-test-pypi: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + needs: + - test + - lint + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: Install poetry + run: python -m pip install poetry + - name: Publish + run: | + poetry config repositories.test-pypi https://test.pypi.org/legacy/ + poetry publish --build -u __token__ -p ${{ secrets.test_pypi_password }} -r test-pypi + test-release-test-pypi: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + needs: + - release-test-pypi + - constants + strategy: + matrix: + python-version: + - "3.8" + - "3.9" + - "3.10" + - "3.11" + steps: + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Run check + run: | + 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 + echo '"""Docstring."""' > source.py + flake8 source.py + release-pypi: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + needs: + - test-release-test-pypi + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: Install poetry + run: python -m pip install poetry + - name: Publish + run: poetry publish --build -u __token__ -p ${{ secrets.pypi_password }} + release-github: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + needs: + - release-pypi + steps: + - name: Get version from tag + id: tag_name + run: | + echo current_version=${GITHUB_REF#refs/tags/v} >> $GITHUB_OUTPUT + shell: bash + - uses: actions/checkout@v3 + - name: Get latest Changelog Entry + id: changelog_reader + uses: mindsers/changelog-reader-action@v2 + with: + version: v${{ steps.tag_name.outputs.current_version }} + path: ./CHANGELOG.md + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.changelog_reader.outputs.version }} + release_name: Release ${{ steps.changelog_reader.outputs.version }} + body: ${{ steps.changelog_reader.outputs.changes }} + prerelease: ${{ steps.changelog_reader.outputs.status == 'prereleased' }} + draft: ${{ steps.changelog_reader.outputs.status == 'unreleased' }} + test-release-pypi: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + needs: + - release-pypi + - constants + strategy: + matrix: + python-version: + - "3.8" + - "3.9" + - "3.10" + - "3.11" + steps: + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Run check + run: | + 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 + echo '"""Docstring."""' > source.py + flake8 source.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f9484f0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "python.analysis.typeCheckingMode": "basic", + "python.linting.pylintEnabled": true, + "python.linting.enabled": true, + "python.formatting.provider": "black" +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9b7e494 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,65 @@ +# Changelog + +## [Unreleased] + +## [v1.0.0] - 2023-01-02 + +### Added + +#### Function/ Method Arguments + +- Lint check that ensures all function/ method arguments are documented +- Lint check that ensures docstring doesn't describe arguments the function/ + method doesn't have +- Lint check that ensures there is at most one arguments section in the + docstring +- Lint check that ensures there is no empty arguments section in the docstring +- Support for unused arguments for which descriptions are optional +- Support `*args` and `**kwargs` +- Support positional only arguments +- Support keyword only arguments +- Support ignoring `self` and `cls` arguments +- Support for skipping test functions in test files +- Support for skipping test fixtures in test and fixture files +- Support async functions/ methods + +#### Function/ Method Return Value + +- Lint check that ensures all functions/ methods that return a value have the + returns section in the docstring +- Lint check that ensures a function that does not return a value does not have + the returns section +- Lint check that ensures there is at most one returns section in the docstring + +#### Function/ Method Yield Value + +- Lint check that ensures all functions/ methods that yield a value have the + yields section in the docstring +- Lint check that ensures a function that does not yield a value does not have + the yields section +- Lint check that ensures there is at most one yields section in the docstring + +#### Function/ Method Exception Handling + +- Lint check that ensures all function/ method exceptions are documented +- Lint check that ensures docstring doesn't describe exceptions the function/ + method doesn't raise +- Lint check that ensures there is at most one raises section in the docstring +- Lint check that ensures the raises section describes at least one exception + +#### Class Attributes + +- Lint check that ensures all class attributes are documented +- Lint check that ensures docstring doesn't describe attributes the class + doesn't have +- Lint check that ensures there is at most one attributes section in the + docstring +- Support for private attributes for which descriptions are optional +- Support for class attributes defined on the class and other `classmethod` + methods +- Support for instance attributes defined in `__init__` and other non-static and + non-`classmethod` methods +- Support async functions/ methods + +[//]: # "Release links" +[v1.0.0]: https://github.com/jdkandersson/flake8-docstrings-complete/releases/v1.0.0 diff --git a/README.md b/README.md index 509838a..6addcb9 100644 --- a/README.md +++ b/README.md @@ -1 +1,1542 @@ -# flake8-docstring \ No newline at end of file +# flake8-docstrings-complete + +Linter that checks docstrings of functions, methods and classes. It should be +used in conjunction with `pydocstyle` (or `flake8-docstrings`) as the linter +assumes that the docstrings already pass `pydocstyle` checks. + +## Getting Started + +```shell +python -m venv venv +source ./venv/bin/activate +pip install flake8 flake8-docstrings flake8-docstrings-complete +flake8 source.py +``` + +On the following code where the `foo` function has the `bar` and `baz` +arguments where the `baz` argument is missing from the `Args` section in the +docstring: + +```Python +# source.py +def foo(bar, baz): + """Perform foo action on bar. + + Args: + bar: The value to perform the foo action on. + """ +``` + +This will produce warnings such as: + +```shell +flake8 test_source.py +source.py:2:14: DCO023 "baz" argument should be described in the docstring, more information: https://github.com/jdkandersson/flake8-docstrings-complete#fix-dco023 +``` + +This can be resolved by adding the `baz` argument to the `Args` section: + +```Python +# source.py +def foo(bar, baz): + """Perform foo action on bar. + + Args: + bar: The value to perform the foo action on. + baz: The modifier to the foo action. + """ +``` + +`flake8-docstrings-complete` adds the following checks to complement +`pydocstyle`: + +1. If a function/ method has arguments, that the arguments section is included. +2. If a function/ method has arguments, that all function/ method arguments are + in the argument section. +3. If an arguments section is in the function/ method docstring, the argument + section contains no arguments the function/ method doesn't have. +4. If a function/ method has a return statement with a value, the return value + section is included. +5. If a function/ method has a yield statement with a value, the yield value + section is included. +6. If a function/ method raises an exception, the raises section is included + with a description for each exception that is raised. +7. If a class has public attributes, that the attributes section is included. +8. If a class has public attributes, that all public attributes are in the + attributes section. +9. If an attributes section is in the class docstring, the attributes section + contains no attributes the class doesn't have. +10. Any of the sections being checked are not present multiple times. + +Note: + +- `self` and `cls` are not counted as arguments. +- `test_.*` methods are skipped in `test_.*\.py` files (function and file names + are configurable). +- functions with a `@fixture` et al dectorator in `conftest.py` and + `test_.*\.py` files are skipped (function and fixture file names are + configurable) + +## Configuration + +The plugin adds the following configurations to `flake8`: + +- `--docstrings-complete-test-filename-pattern`: The filename pattern for test + files. Defaults to `test_.*\.py`. +- `--docstrings-complete-test-function-pattern`: The function name pattern for + test functions. Defaults to `test_.*`. +- `--docstrings-complete-fixture-filename-pattern`: The filename pattern for + fixture files. Defaults to `conftest\.py`. +- `--docstrings-complete-fixture-decorator-pattern`: The decorator name pattern + for fixture functions. Defaults to `(^|\.)fixture$`. + +## Rules + +A few rules have been defined to allow for selective suppression: + +- `DCO010`: docstring missing on a function/ method/ class. +- `DCO020`: function/ method has one or more arguments and the docstring does + not have an arguments section. +- `DCO021`: function/ method with no arguments and the docstring has an + arguments section. +- `DCO022`: function/ method with one or more arguments and the docstring has + multiple arguments sections. +- `DCO023`: function/ method has one or more arguments not described in the + docstring. +- `DCO024`: function/ method has one or more arguments described in the + docstring which are not arguments of the function/ method. +- `DCO030`: function/ method that returns a value does not have the returns + section in the docstring. +- `DCO031`: function/ method that does not return a value has the returns + section in the docstring. +- `DCO032`: function/ method that returns a value and the docstring has + multiple returns sections. +- `DCO040`: function/ method that yields a value does not have the yields + section in the docstring. +- `DCO041`: function/ method that does not yield a value has the yields + section in the docstring. +- `DCO042`: function/ method that yields a value and the docstring has + multiple yields sections. +- `DCO050`: function/ method raises one or more exceptions and the docstring + does not have a raises section. +- `DCO051`: function/ method that raises no exceptions and the docstring has a + raises section. +- `DCO052`: function/ method that raises one or more exceptions and the + docstring has multiple raises sections. +- `DCO053`: function/ method that raises one or more exceptions where one or + more of the exceptions is not described in the docstring. +- `DCO054`: function/ method has one or more exceptions described in the + docstring which are not raised in the function/ method. +- `DCO055`: function/ method that has a raise without an exception has an empty + raises section in the docstring. +- `DCO060`: class has one or more public attributes and the docstring does not + have an attributes section. +- `DCO061`: class with no attributes and the docstring has an attributes + section. +- `DCO062`: class with one or more attributes and the docstring has multiple + attributes sections. +- `DCO063`: class has one or more public attributes not described in the + docstring. +- `DCO064`: class has one or more attributes described in the docstring which + are not attributes of the class. + +### Fix DCO010 + +This linting rule is triggered by a function/ method/ class without a +docstring. For example: + +```Python +def foo(): + pass + +class FooClass: + def foo(self): + """Perform foo action.""" + pass + +class FooClass: + """Perform foo action.""" + def foo(self): + pass +``` + +This example can be fixed by adding a docstring: + +```Python +def foo(): + """Perform foo action.""" + +class FooClass: + """Perform foo action.""" + def foo(self): + """Perform foo action.""" +``` + +### Fix DCO020 + +This linting rule is triggered by a function/ method that has one or more +arguments with a docstring that does not have an arguments section. For +example: + +```Python +def foo(bar): + """Perform foo action.""" + +class FooClass: + def foo(self, bar): + """Perform foo action.""" +``` + +These examples can be fixed by adding the arguments section and describing all +arguments in the arguments section: + +```Python +def foo(bar): + """Perform foo action. + + Args: + bar: the value to perform the foo action on. + """ + +def foo(bar): + """Perform foo action. + + Arguments: + bar: the value to perform the foo action on. + """ + +def foo(bar): + """Perform foo action. + + Parameters: + bar: the value to perform the foo action on. + """ + +class FooClass: + def foo(self, bar): + """Perform foo action. + + Args: + bar: the value to perform the foo action on. + """ +``` + +### Fix DCO021 + +This linting rule is triggered by a function/ method that has no arguments with +a docstring that has an arguments section. For example: + +```Python +def foo(): + """Perform foo action. + + Args: + bar: the value to perform the foo action on. + """ + +def foo(): + """Perform foo action. + + Arguments: + bar: the value to perform the foo action on. + """ + +def foo(): + """Perform foo action. + + Parameters: + bar: the value to perform the foo action on. + """ + +class FooClass: + def foo(self): + """Perform foo action. + + Args: + bar: the value to perform the foo action on. + """ +``` + +These examples can be fixed by removing the arguments section: + +```Python +def foo(): + """Perform foo action.""" + +class FooClass: + def foo(self): + """Perform foo action.""" +``` + +### Fix DCO022 + +This linting rule is triggered by a function/ method that has one or more +arguments and a docstring that has multiple arguments sections. For example: + +```Python +def foo(bar): + """Perform foo action. + + Args: + bar: the value to perform the foo action on. + + Args: + bar: the value to perform the foo action on. + """ + +def foo(bar): + """Perform foo action. + + Args: + bar: the value to perform the foo action on. + + Arguments: + bar: the value to perform the foo action on. + + Parameters: + bar: the value to perform the foo action on. + """ + +class FooClass: + def foo(self, bar): + """Perform foo action. + + Args: + bar: the value to perform the foo action on. + + Args: + bar: the value to perform the foo action on. + """ +``` + +These examples can be fixed by removing the additional arguments sections: + +```Python +def foo(bar): + """Perform foo action. + + Args: + bar: the value to perform the foo action on. + """ + +def foo(bar): + """Perform foo action. + + Arguments: + bar: the value to perform the foo action on. + """ + +def foo(bar): + """Perform foo action. + + Parameters: + bar: the value to perform the foo action on. + """ + +class FooClass: + def foo(self, bar): + """Perform foo action. + + Args: + bar: the value to perform the foo action on. + """ +``` + +### Fix DCO023 + +This linting rule is triggered by a function/ method that has one or more +arguments where one or more of those arguments is not described in the +docstring. For example: + +```Python +def foo(bar): + """Perform foo action. + + Args: + """ + +def foo(bar, baz): + """Perform foo action. + + Args: + bar: the value to perform the foo action on. + """ + +class FooClass: + def foo(self, bar): + """Perform foo action. + + Args: + """ +``` + +These examples can be fixed by adding the missing arguments to the arguments +section: + +```Python +def foo(bar): + """Perform foo action. + + Args: + bar: the value to perform the foo action on. + """ + +def foo(bar, baz): + """Perform foo action. + + Args: + bar: the value to perform the foo action on. + baz: the modifier to the foo action. + """ + +class FooClass: + def foo(self, bar): + """Perform foo action. + + Args: + bar: the value to perform the foo action on. + """ +``` + +### Fix DCO024 + +This linting rule is triggered by a function/ method that has one or more +arguments and a docstring that describes one or more arguments where on or more +of the described arguments are not arguments of the function/ method. For +example: + +```Python +def foo(bar): + """Perform foo action. + + Args: + bar: the value to perform the foo action on. + baz: the modifier to the foo action. + """ + +class FooClass: + def foo(self, bar): + """Perform foo action. + + Args: + bar: the value to perform the foo action on. + baz: the modifier to the foo action. + """ +``` + +These examples can be fixed by removing the arguments the function/ method +doesn't have from the docstring: + +```Python +def foo(bar): + """Perform foo action. + + Args: + bar: the value to perform the foo action on. + """ + +class FooClass: + def foo(self, bar): + """Perform foo action. + + Args: + bar: the value to perform the foo action on. + """ +``` + +### Fix DCO030 + +This linting rule is triggered by a function/ method that has at least one +return statement with a value and does not have a returns section in the +docstring. For example: + +```Python +def foo(): + """Return bar.""" + return "bar" + +class FooClass: + def foo(self): + """Return bar.""" + return "bar" +``` + +These examples can be fixed by adding the returns section: + +```Python +def foo(): + """Return bar. + + Return: + bar. + """ + return "bar" + +def foo(): + """Return bar. + + Returns: + bar. + """ + return "bar" + +class FooClass: + def foo(self): + """Return bar. + + Returns: + bar. + """ + return "bar" +``` + +### Fix DCO031 + +This linting rule is triggered by a function/ method that has no return +statement with a value and has a returns section in the +docstring. For example: + +```Python +def foo(): + """Return bar. + + Returns: + bar. + """ + pass + +class FooClass: + def foo(self): + """Return bar. + + Returns: + bar. + """ + pass +``` + +These examples can be fixed by removing the returns section: + +```Python +def foo(): + """Take foo action.""" + pass + +class FooClass: + def foo(self): + """Take foo action.""" + pass +``` + +### Fix DCO032 + +This linting rule is triggered by a function/ method that returns a value and +has a docstring that has multiple returns sections. For example: + +```Python +def foo(): + """Perform foo action. + + Returns: + bar. + + Returns: + bar. + """ + return "bar" + +def foo(): + """Perform foo action. + + Returns: + bar. + + Return: + bar. + """ + return "bar" + +class FooClass: + def foo(self): + """Perform foo action. + + Returns: + bar. + + Returns: + bar. + """ + return "bar" +``` + +These examples can be fixed by removing the additional returns sections: + +```Python +def foo(): + """Perform foo action. + + Returns: + bar. + """ + return "bar" + +def foo(): + """Perform foo action. + + Returns: + bar. + """ + return "bar" + +class FooClass: + def foo(self): + """Perform foo action. + + Returns: + bar. + """ + return "bar" +``` + +### Fix DCO040 + +This linting rule is triggered by a function/ method that has at least one +yield statement with a value or a yield from statement and does not have a +yields section in the docstring. For example: + +```Python +def foo(): + """Yield bar.""" + yield "bar" + +def foo(): + """Yield bar.""" + yield from ("bar",) + +class FooClass: + def foo(self): + """Yield bar.""" + yield "bar" +``` + +These examples can be fixed by adding the yields section: + +```Python +def foo(): + """Yield bar. + + Yield: + bar. + """ + yield "bar" + +def foo(): + """Yield bar. + + Yields: + bar. + """ + yield "bar" + +def foo(): + """Yield bar. + + Yields: + bar. + """ + yield from ("bar",) + +class FooClass: + def foo(self): + """Yield bar. + + Yields: + bar. + """ + yield "bar" +``` + +### Fix DCO041 + +This linting rule is triggered by a function/ method that has no yield +statement with a value nor a yield from statement and has a yields section +in the docstring. For example: + +```Python +def foo(): + """Yield bar. + + Yields: + bar. + """ + pass + +class FooClass: + def foo(self): + """Yield bar. + + Yields: + bar. + """ + pass +``` + +These examples can be fixed by: + +```Python +def foo(): + """Take foo action.""" + pass + +class FooClass: + def foo(self): + """Take foo action.""" + pass +``` + +### Fix DCO042 + +This linting rule is triggered by a function/ method that yields a value and +has a docstring that has multiple yields sections. For example: + +```Python +def foo(): + """Perform foo action. + + Yields: + bar. + + Yields: + bar. + """ + yield "bar" + +def foo(): + """Perform foo action. + + Yields: + bar. + + Yields: + bar. + """ + yield from ("bar",) + +def foo(): + """Perform foo action. + + Yields: + bar. + + Yield: + bar. + """ + yield "bar" + +class FooClass: + def foo(self): + """Perform foo action. + + Yields: + bar. + + Yields: + bar. + """ + yield "bar" +``` + +These examples can be fixed by removing the additional yields sections: + +```Python +def foo(): + """Perform foo action. + + Yields: + bar. + """ + yield "bar" + +def foo(): + """Perform foo action. + + Yields: + bar. + """ + yield from ("bar",) + +def foo(): + """Perform foo action. + + Yields: + bar. + """ + yield "bar" + +class FooClass: + def foo(self): + """Perform foo action. + + Yields: + bar. + """ + yield "bar" +``` + +### Fix DCO050 + +This linting rule is triggered by a function/ method that raises one or more +exceptions and a docstring that does not have a raises section. For example: + +```Python +def foo(): + """Perform foo action.""" + raise BarError + +class FooClass: + def foo(self): + """Perform foo action.""" + raise BarError +``` + +These examples can be fixed by adding the raises section and describing all +raised exceptions in it: + +```Python +def foo(): + """Perform foo action. + + Raises: + BarError: the value to perform the foo action on was wrong. + """ + raise BarError + +def foo(): + """Perform foo action. + + Raise: + BarError: the value to perform the foo action on was wrong. + """ + raise BarError + +class FooClass: + def foo(self): + """Perform foo action. + + Raises: + BarError: the value to perform the foo action on was wrong. + """ + raise BarError +``` + +### Fix DCO051 + +This linting rule is triggered by a function/ method that raises no exceptions +with a docstring that has a raises section. For example: + +```Python +def foo(): + """Perform foo action. + + Raises: + BarError: the value to perform the foo action on was wrong. + """ + +def foo(): + """Perform foo action. + + Raise: + BarError: the value to perform the foo action on was wrong. + """ + +class FooClass: + def foo(self): + """Perform foo action. + + Raises: + BarError: the value to perform the foo action on was wrong. + """ +``` + +These examples can be fixed by removing the raises section: + +```Python +def foo(): + """Perform foo action.""" + +class FooClass: + def foo(self): + """Perform foo action.""" +``` + +### Fix DCO052 + +This linting rule is triggered by a function/ method that raises one or more +exceptions with a docstring that has multiple raises sections. For example: + +```Python +def foo(): + """Perform foo action. + + Raises: + BarError: the value to perform the foo action on was wrong. + + Raises: + BarError: the value to perform the foo action on was wrong. + """ + raise BarError + +def foo(): + """Perform foo action. + + Raises: + BarError: the value to perform the foo action on was wrong. + + Raise: + BarError: the value to perform the foo action on was wrong. + """ + raise BarError + +class FooClass: + def foo(self): + """Perform foo action. + + Raises: + BarError: the value to perform the foo action on was wrong. + + Raises: + BarError: the value to perform the foo action on was wrong. + """ + raise BarError +``` + +These examples can be fixed by removing the additional raises sections: + +```Python +def foo(): + """Perform foo action. + + Raises: + BarError: the value to perform the foo action on was wrong. + """ + raise BarError + +def foo(): + """Perform foo action. + + Raise: + BarError: the value to perform the foo action on was wrong. + """ + raise BarError + +class FooClass: + def foo(self): + """Perform foo action. + + Raises: + BarError: the value to perform the foo action on was wrong. + """ + raise BarError +``` + +### Fix DCO053 + +This linting rule is triggered by a function/ method that raises one or more +exceptions where one or more of those exceptions is not described in the +docstring. For example: + +```Python +def foo(): + """Perform foo action.""" + raise BarError + +def foo(bar, baz): + """Perform foo action. + + Raises: + BarError: the value to perform the foo action on was wrong. + """ + raise BarError + raise BazError + +class FooClass: + def foo(self): + """Perform foo action.""" + raise BarError +``` + +These examples can be fixed by describing the additional exceptions in the +docstring: + +```Python +def foo(): + """Perform foo action. + + Raises: + BarError: the value to perform the foo action on was wrong. + """ + raise BarError + +def foo(bar, baz): + """Perform foo action. + + Raises: + BarError: the value to perform the foo action on was wrong. + BazError: the alternate value to perform the foo action on was wrong. + """ + raise BarError + raise BazError + +class FooClass: + def foo(self): + """Perform foo action. + + Raises: + BarError: the value to perform the foo action on was wrong. + """ + raise BarError +``` + +### Fix DCO054 + +This linting rule is triggered by a function/ method that raises one or more +exceptions and a docstring that describes one or more exceptions where on or +more of the described exceptions are not raised by the function/ method. For +example: + +```Python +def foo(): + """Perform foo action. + + Raises: + BarError: the value to perform the foo action on was wrong. + BazError: the alternate value to perform the foo action on was wrong. + """ + raise BarError + +class FooClass: + def foo(self): + """Perform foo action. + + Raises: + BarError: the value to perform the foo action on was wrong. + BazError: the alternate value to perform the foo action on was wrong. + """ + raise BarError +``` + +These examples can be fixed by removing the exception that is not raised from +the docstring: + +```Python +def foo(): + """Perform foo action. + + Raises: + BarError: the value to perform the foo action on was wrong. + """ + raise BarError + +class FooClass: + def foo(self): + """Perform foo action. + + Raises: + BarError: the value to perform the foo action on was wrong. + """ + raise BarError +``` + +### Fix DCO055 + +This linting rule is triggered by a function/ method that has a `raise` +statement without an exception (typically re-raising exceptions) and the raises +section is not included or is empty. For example: + +```Python +def foo(): + """Perform foo action.""" + try: + bar() + except BarError: + raise + +def foo(): + """Perform foo action. + + Raises: + """ + try: + bar() + except BarError: + raise + +class FooClass: + def foo(self): + """Perform foo action.""" + try: + bar() + except BarError: + raise +``` + +These examples can be fixed by describing at least one exception in the raises +section: + +```Python +def foo(): + """Perform foo action. + + Raises: + BarError: the value to perform the foo action on was wrong. + """ + try: + bar() + except BarError: + raise + +def foo(): + """Perform foo action. + + Raises: + BarError: the value to perform the foo action on was wrong. + """ + try: + bar() + except BarError: + raise + +class FooClass: + def foo(self): + """Perform foo action. + + Raises: + BarError: the value to perform the foo action on was wrong. + """ + try: + bar() + except BarError: + raise +``` + +### Fix DCO060 + +This linting rule is triggered by a class that has one or more public +attributes with a docstring that does not have an attributes section. For +example: + +```Python +class FooClass: + """Perform foo action.""" + bar = "bar" + +class FooClass: + """Perform foo action.""" + + def __init__(self): + self.bar = "bar" + +class FooClass: + """Perform foo action.""" + + def bar(self): + self.baz = "baz" + +class FooClass: + """Perform foo action. + + Attrs: + bar: The value to perform the foo action on. + """ + bar = "bar" + baz = "baz" +``` + +These examples can be fixed by adding the attributes section and describing all +attributes in the attributes section: + +```Python +class FooClass: + """Perform foo action. + + Attrs: + bar: The value to perform the foo action on. + """ + bar = "bar" + +class FooClass: + """Perform foo action. + + Attributes: + bar: The value to perform the foo action on. + """ + bar = "bar" + +class FooClass: + """Perform foo action. + + Attributes: + bar: The value to perform the foo action on. + """ + + def __init__(self): + self.bar = "bar" + +class FooClass: + """Perform foo action. + + Attributes: + baz: The value to perform the foo action on. + """ + + def bar(self): + self.baz = "baz" + +class FooClass: + """Perform foo action. + + Attrs: + bar: The value to perform the foo action on. + baz: The alternate value to perform the foo action on. + """ + bar = "bar" + baz = "baz" +``` + +### Fix DCO061 + +This linting rule is triggered by a class that has no attributes with a +docstring that has an attributes section. For example: + +```Python +class FooClass: + """Perform foo action. + + Attrs: + bar: The value to perform the foo action on. + """ + +class FooClass: + """Perform foo action. + + Attributes: + bar: The value to perform the foo action on. + """ + +class FooClass: + """Perform foo action. + + Attributes: + """ + + def __init__(self): + self._bar = "bar" +``` + +These examples can be fixed by removing the attributes section: + +```Python +class FooClass: + """Perform foo action.""" + +class FooClass: + """Perform foo action.""" + + def __init__(self): + self._bar = "bar" +``` + +### Fix DCO062 + +This linting rule is triggered by a class that has one or more attributes and +a docstring that has multiple attributes sections. For example: + +```Python +class FooClass: + """Perform foo action. + + Attrs: + bar: The value to perform the foo action on. + + Attrs: + bar: The value to perform the foo action on. + """ + bar = "bar" + +class FooClass: + """Perform foo action. + + Attrs: + bar: The value to perform the foo action on. + + Attributes: + bar: The value to perform the foo action on. + """ + bar = "bar" +``` + +These examples can be fixed by removing the additional attributes sections: + +```Python +class FooClass: + """Perform foo action. + + Attrs: + bar: The value to perform the foo action on. + """ + bar = "bar" + +class FooClass: + """Perform foo action. + + Attributes: + bar: The value to perform the foo action on. + """ + bar = "bar" +``` + +### Fix DCO063 + +This linting rule is triggered by a class that has one or more public +attributes where one or more of those public attributes is not described in the +docstring. For example: + +```Python +class FooClass: + """Perform foo action.""" + bar = "bar" + +class FooClass: + """Perform foo action. + + Attrs: + """ + + def __init__(self): + self.bar = "bar" + +class FooClass: + """Perform foo action.""" + + def bar(self): + self.baz = "baz" + +class FooClass: + """Perform foo action. + + Attrs: + bar: The value to perform the foo action on. + """ + bar = "bar" + baz = "baz" +``` + +These examples can be fixed by adding the missing attributes to the attributes +section: + +```Python +class FooClass: + """Perform foo action. + + Attrs: + bar: The value to perform the foo action on. + """ + bar = "bar" + +class FooClass: + """Perform foo action. + + Attrs: + bar: The value to perform the foo action on. + """ + bar = "bar" + +class FooClass: + """Perform foo action. + + Attributes: + bar: The value to perform the foo action on. + """ + + def __init__(self): + self.bar = "bar" + +class FooClass: + """Perform foo action. + + Attributes: + baz: The value to perform the foo action on. + """ + + def bar(self): + self.baz = "baz" + +class FooClass: + """Perform foo action. + + Attrs: + bar: The value to perform the foo action on. + baz: The alternate value to perform the foo action on. + """ + bar = "bar" + baz = "baz" +``` + +### Fix DCO064 + +This linting rule is triggered by a class that has one or more attributes and a +docstring that describes one or more attributes where on or more +of the described attributes are not attributes of the class. For example: + +```Python +class FooClass: + """Perform foo action. + + Attrs: + bar: The value to perform the foo action on. + """ + +class FooClass: + """Perform foo action. + + Attrs: + _bar: The value to perform the foo action on. + """ + +class FooClass: + """Perform foo action. + + Attrs: + bar: The value to perform the foo action on. + baz: The alternate value to perform the foo action on. + """ + bar = "bar" +``` + +These examples can be fixed by removing the attributes the class doesn't have +from the docstring: + +```Python +class FooClass: + """Perform foo action.""" + +class FooClass: + """Perform foo action.""" + +class FooClass: + """Perform foo action. + + Attrs: + bar: The value to perform the foo action on. + """ + bar = "bar" +``` + +## Docstring Examples + +Examples of function/ method and class docstrings are: + +```Python +def foo(bar): + """Perform the foo actions on bar. + + Args: + bar: the value to perform the foo actions on. + + Returns: + bar after applying to foo action to it. + + Yields: + All the foo actions that have been performed. + + Raises: + FooError: an error occurred whilst performing the foo action. + """ + +class Foo: + """Foo object. + + Attrs: + bar: the value to perform the foo actions on. + """ + + def __init__(self, bar): + """Construct. + + Args: + bar: the value to perform the foo actions on. + """ + self.bar = bar +``` + +### Fix DCO010 + +This linting rule is triggered by a function/ method that has at least one +yield statement with a value and does not have a yields section in the +docstring. For example: + +```Python +def foo(): + """Yield bar.""" + yield "bar" + +class FooClass: + def foo(self): + """Yield bar.""" + yield "bar" +``` + +These examples can be fixed by: + +```Python +def foo(): + """Yield bar. + + Yield: + bar. + """ + +def foo(): + """Yield bar. + + Yields: + bar. + """ + yield "bar" + +class FooClass: + def foo(self): + """Yield bar. + + Yields: + bar. + """ + yield "bar" +``` + +## Sections + +There are several alternative names for each of the sections which are captured +case-insensitive: + +- arguments: `Args`, `Arguments`, `Parameters` +- return value: `Return`, `Returns` +- yield value: `Yield`, `Yields` +- raise: `Raises` +- attributes: `Attrs`, `Attributes` + +Section information is extracted using the following algorithm: + +1. Look for a line that has zero or more whitespace characters, followed by a + section name (non-case-sensistive) followed by a colon. +2. Look for any sub-sections on a line which starts with zero or more + whitespace characters followed by a word, optionally followed by whitespace + and any characters within round brackets followed by a colon. +3. The section ends if any line with zero or more whitespace characters is + encountered or the end of the docstring is reached. + +## Future Ideas: + +- Check that argument, exceptions and attributes have non-empty description. +- Check that arguments, exceptions and attributes are only documented once. +- Check that arguments, exceptions and attributes have meaningful descriptions. diff --git a/flake8_docstrings_complete/__init__.py b/flake8_docstrings_complete/__init__.py new file mode 100644 index 0000000..e31877a --- /dev/null +++ b/flake8_docstrings_complete/__init__.py @@ -0,0 +1,545 @@ +"""A linter that checks docstring include all expected descriptions.""" + +from __future__ import annotations + +import argparse +import ast +import re +from pathlib import Path +from typing import Iterable, Iterator + +from flake8.options.manager import OptionManager + +from . import args, attrs, docstring, raises, types_ +from .constants import ERROR_CODE_PREFIX, MORE_INFO_BASE + +DOCSTR_MISSING_CODE = f"{ERROR_CODE_PREFIX}010" +DOCSTR_MISSING_MSG = ( + f"{DOCSTR_MISSING_CODE} docstring should be defined for a function/ method/ class" + f"{MORE_INFO_BASE}{DOCSTR_MISSING_CODE.lower()}" +) +RETURNS_SECTION_NOT_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}030" +RETURNS_SECTION_NOT_IN_DOCSTR_MSG = ( + f"{RETURNS_SECTION_NOT_IN_DOCSTR_CODE} function/ method that returns a value should have the " + f"returns section in the docstring{MORE_INFO_BASE}{RETURNS_SECTION_NOT_IN_DOCSTR_CODE.lower()}" +) +RETURNS_SECTION_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}031" +RETURNS_SECTION_IN_DOCSTR_MSG = ( + f"{RETURNS_SECTION_IN_DOCSTR_CODE} function/ method that does not return a value should not " + f"have the returns section in the docstring" + f"{MORE_INFO_BASE}{RETURNS_SECTION_IN_DOCSTR_CODE.lower()}" +) +MULT_RETURNS_SECTIONS_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}032" +MULT_RETURNS_SECTIONS_IN_DOCSTR_MSG = ( + f"{MULT_RETURNS_SECTIONS_IN_DOCSTR_CODE} a docstring should only contain a single returns " + "section, found %s" + f"{MORE_INFO_BASE}{MULT_RETURNS_SECTIONS_IN_DOCSTR_CODE.lower()}" +) +YIELDS_SECTION_NOT_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}040" +YIELDS_SECTION_NOT_IN_DOCSTR_MSG = ( + f"{YIELDS_SECTION_NOT_IN_DOCSTR_CODE} function/ method that yields a value should have the " + f"yields section in the docstring{MORE_INFO_BASE}{YIELDS_SECTION_NOT_IN_DOCSTR_CODE.lower()}" +) +YIELDS_SECTION_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}041" +YIELDS_SECTION_IN_DOCSTR_MSG = ( + f"{YIELDS_SECTION_IN_DOCSTR_CODE} function/ method that does not yield a value should not " + f"have the yields section in the docstring" + f"{MORE_INFO_BASE}{YIELDS_SECTION_IN_DOCSTR_CODE.lower()}" +) +MULT_YIELDS_SECTIONS_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}042" +MULT_YIELDS_SECTIONS_IN_DOCSTR_MSG = ( + f"{MULT_YIELDS_SECTIONS_IN_DOCSTR_CODE} a docstring should only contain a single yields " + "section, found %s" + f"{MORE_INFO_BASE}{MULT_YIELDS_SECTIONS_IN_DOCSTR_CODE.lower()}" +) + +TEST_FILENAME_PATTERN_ARG_NAME = "--docstrings-complete-test-filename-pattern" +TEST_FILENAME_PATTERN_DEFAULT = r"test_.*\.py" +TEST_FUNCTION_PATTERN_ARG_NAME = "--docstrings-complete-test-function-pattern" +TEST_FUNCTION_PATTERN_DEFAULT = r"test_.*" +FIXTURE_FILENAME_PATTERN_ARG_NAME = "--docstrings-complete-fixture-filename-pattern" +FIXTURE_FILENAME_PATTERN_DEFAULT = r"conftest\.py" +FIXTURE_DECORATOR_PATTERN_ARG_NAME = "--docstrings-complete-fixture-decorator-pattern" +FIXTURE_DECORATOR_PATTERN_DEFAULT = r"(^|\.)fixture$" + + +# Helper function for option management, tested in integration tests +def _cli_arg_name_to_attr(cli_arg_name: str) -> str: + """Transform CLI argument name to the attribute name on the namespace. + + Args: + cli_arg_name: The CLI argument name to transform. + + Returns: + The namespace name for the argument. + """ + return cli_arg_name.lstrip("-").replace("-", "_") # pragma: nocover + + +def _check_returns( + docstr_info: docstring.Docstring, docstr_node: ast.Constant, return_nodes: Iterable[ast.Return] +) -> Iterator[types_.Problem]: + """Check function/ method returns section. + + Args: + docstr_info: Information about the docstring. + docstr_node: The docstring node. + return_nodes: The return nodes of the function. + + Yields: + All the problems with the returns section. + """ + return_nodes_with_value = list(node for node in return_nodes if node.value is not None) + + # Check for return statements with value and no returns section in docstring + if return_nodes_with_value and not docstr_info.returns: + yield from ( + types_.Problem(node.lineno, node.col_offset, RETURNS_SECTION_NOT_IN_DOCSTR_MSG) + for node in return_nodes_with_value + ) + + # Check for multiple returns sections + if return_nodes_with_value and len(docstr_info.returns_sections) > 1: + yield types_.Problem( + docstr_node.lineno, + docstr_node.col_offset, + MULT_RETURNS_SECTIONS_IN_DOCSTR_MSG % ",".join(docstr_info.returns_sections), + ) + + # Check for returns section in docstring in function that does not return a value + if not return_nodes_with_value and docstr_info.returns: + yield types_.Problem( + docstr_node.lineno, docstr_node.col_offset, RETURNS_SECTION_IN_DOCSTR_MSG + ) + + +def _check_yields( + docstr_info: docstring.Docstring, + docstr_node: ast.Constant, + yield_nodes: Iterable[ast.Yield | ast.YieldFrom], +) -> Iterator[types_.Problem]: + """Check function/ method yields section. + + Args: + docstr_info: Information about the docstring. + docstr_node: The docstring node. + yield_nodes: The yield and yield from nodes of the function. + + Yields: + All the problems with the yields section. + """ + yield_nodes_with_value = list(node for node in yield_nodes if node.value is not None) + + # Check for yield statements with value and no yields section in docstring + if yield_nodes_with_value and not docstr_info.yields: + yield from ( + types_.Problem(node.lineno, node.col_offset, YIELDS_SECTION_NOT_IN_DOCSTR_MSG) + for node in yield_nodes_with_value + ) + + # Check for multiple yields sections + if yield_nodes_with_value and len(docstr_info.yields_sections) > 1: + yield types_.Problem( + docstr_node.lineno, + docstr_node.col_offset, + MULT_YIELDS_SECTIONS_IN_DOCSTR_MSG % ",".join(docstr_info.yields_sections), + ) + + # Check for yields section in docstring in function that does not yield a value + if not yield_nodes_with_value and docstr_info.yields: + yield types_.Problem( + docstr_node.lineno, docstr_node.col_offset, YIELDS_SECTION_IN_DOCSTR_MSG + ) + + +class VisitorWithinFunction(ast.NodeVisitor): + """Visits AST nodes within a functions but not nested functions or classes. + + Attrs: + return_nodes: All the return nodes encountered within the function. + yield_nodes: All the yield nodes encountered within the function. + raise_nodes: All the raise nodes encountered within the function. + """ + + return_nodes: list[ast.Return] + yield_nodes: list[ast.Yield | ast.YieldFrom] + raise_nodes: list[ast.Raise] + _visited_once: bool + + def __init__(self) -> None: + """Construct.""" + self.return_nodes = [] + self.yield_nodes = [] + self.raise_nodes = [] + self._visited_once = False + + # The function must be called the same as the name of the node + def visit_Return(self, node: ast.Return) -> None: # pylint: disable=invalid-name + """Record return node. + + Args: + node: The return node to record. + """ + self.return_nodes.append(node) + + # Ensure recursion continues + self.generic_visit(node) + + # The function must be called the same as the name of the node + def visit_Yield(self, node: ast.Yield) -> None: # pylint: disable=invalid-name + """Record yield node. + + Args: + node: The yield node to record. + """ + self.yield_nodes.append(node) + + # Ensure recursion continues + self.generic_visit(node) + + # The function must be called the same as the name of the node + def visit_YieldFrom(self, node: ast.YieldFrom) -> None: # pylint: disable=invalid-name + """Record yield from node. + + Args: + node: The yield from node to record. + """ + self.yield_nodes.append(node) + + # Ensure recursion continues + self.generic_visit(node) + + # The function must be called the same as the name of the node + def visit_Raise(self, node: ast.Raise) -> None: # pylint: disable=invalid-name + """Record raise node. + + Args: + node: The raise node to record. + """ + self.raise_nodes.append(node) + + # Ensure recursion continues + self.generic_visit(node) + + def visit_once(self, node: ast.AST) -> None: + """Visit the node once and then skip. + + Args: + node: The node being visited. + """ + if not self._visited_once: + self._visited_once = True + self.generic_visit(node=node) + + # Ensure that nested functions and classes are not iterated over + # The functions must be called the same as the name of the node + visit_FunctionDef = visit_once # noqa: N815,DCO063 + visit_AsyncFunctionDef = visit_once # noqa: N815,DCO063 + visit_ClassDef = visit_once # noqa: N815,DCO063 + + +class Visitor(ast.NodeVisitor): + """Visits AST nodes and check docstrings of functions and classes. + + Attrs: + problems: All the problems that were encountered. + """ + + problems: list[types_.Problem] + _file_type: types_.FileType + _test_function_pattern: str + _fixture_decorator_pattern: str + + def __init__( + self, + file_type: types_.FileType, + test_function_pattern: str, + fixture_decorator_pattern: str, + ) -> None: + """Construct. + + Args: + file_type: The type of file being processed. + test_function_pattern: The pattern to match test functions with. + fixture_decorator_pattern: The pattern to match decorators of fixture function with. + """ + self.problems = [] + self._file_type = file_type + self._test_function_pattern = test_function_pattern + self._fixture_decorator_pattern = fixture_decorator_pattern + + def _is_fixture_decorator(self, node: ast.expr) -> bool: + """Determine whether an expression is a fixture decorator. + + Args: + node: The node to check. + + Returns: + Whether the node is a decorator fixture. + """ + # Handle variable + fixture_name: str | None = None + if isinstance(node, ast.Name): + fixture_name = node.id + if isinstance(node, ast.Attribute): + fixture_name = node.attr + if fixture_name is not None: + return ( + re.search(self._fixture_decorator_pattern, fixture_name, re.IGNORECASE) is not None + ) + + # Handle call + if isinstance(node, ast.Call): + return self._is_fixture_decorator(node=node.func) + + # No valid syntax can reach here + return False # pragma: nocover + + def _skip_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool: + """Check whether to skip a function. + + Args: + node: The function to check + + Returns: + Whether to skip the function. + """ + if self._file_type == types_.FileType.TEST and re.match( + self._test_function_pattern, node.name + ): + return True + + if self._file_type in {types_.FileType.TEST, types_.FileType.FIXTURE}: + return any(self._is_fixture_decorator(decorator) for decorator in node.decorator_list) + + return False + + def visit_any_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None: + """Check a function definition node. + + Args: + node: The function definition to check. + """ + if not self._skip_function(node=node): + # Check docstring is defined + if ast.get_docstring(node) is None: + self.problems.append( + types_.Problem( + lineno=node.lineno, col_offset=node.col_offset, msg=DOCSTR_MISSING_MSG + ) + ) + + if ( + node.body + and isinstance(node.body[0], ast.Expr) + and isinstance(node.body[0].value, ast.Constant) + and isinstance(node.body[0].value.value, str) + ): + # Check args + docstr_info = docstring.parse(value=node.body[0].value.value) + docstr_node = node.body[0].value + self.problems.extend( + args.check(docstr_info=docstr_info, docstr_node=docstr_node, args=node.args) + ) + + # Check returns + visitor_within_function = VisitorWithinFunction() + visitor_within_function.visit(node=node) + self.problems.extend( + _check_returns( + docstr_info=docstr_info, + docstr_node=docstr_node, + return_nodes=visitor_within_function.return_nodes, + ) + ) + + # Check yields + self.problems.extend( + _check_yields( + docstr_info=docstr_info, + docstr_node=docstr_node, + yield_nodes=visitor_within_function.yield_nodes, + ) + ) + + # Check raises + self.problems.extend( + raises.check( + docstr_info=docstr_info, + docstr_node=docstr_node, + raise_nodes=visitor_within_function.raise_nodes, + ) + ) + + # Ensure recursion continues + self.generic_visit(node) + + # The functions must be called the same as the name of the node + visit_FunctionDef = visit_any_function # noqa: N815,DCO063 + visit_AsyncFunctionDef = visit_any_function # noqa: N815,DCO063 + + # The function must be called the same as the name of the node + def visit_ClassDef(self, node: ast.ClassDef) -> None: # pylint: disable=invalid-name + """Check a class definition node. + + Args: + node: The class definition to check. + """ + # Check docstring is defined + if ast.get_docstring(node) is None: + self.problems.append( + types_.Problem( + lineno=node.lineno, col_offset=node.col_offset, msg=DOCSTR_MISSING_MSG + ) + ) + + if ( + node.body + and isinstance(node.body[0], ast.Expr) + and isinstance(node.body[0].value, ast.Constant) + and isinstance(node.body[0].value.value, str) + ): + # Check attrs + docstr_info = docstring.parse(value=node.body[0].value.value) + docstr_node = node.body[0].value + visitor_within_class = attrs.VisitorWithinClass() + visitor_within_class.visit(node=node) + self.problems.extend( + attrs.check( + docstr_info=docstr_info, + docstr_node=docstr_node, + class_assign_nodes=visitor_within_class.class_assign_nodes, + method_assign_nodes=visitor_within_class.method_assign_nodes, + ) + ) + + # Ensure recursion continues + self.generic_visit(node) + + +class Plugin: + """Checks docstring include all expected descriptions. + + Attrs: + name: The name of the plugin. + """ + + name = __name__ + _test_filename_pattern: str = TEST_FILENAME_PATTERN_DEFAULT + _test_function_pattern: str = TEST_FUNCTION_PATTERN_DEFAULT + _fixture_filename_pattern: str = FIXTURE_FILENAME_PATTERN_DEFAULT + _fixture_decorator_pattern: str = FIXTURE_DECORATOR_PATTERN_DEFAULT + _tree: ast.AST + _filename: str + + def __init__(self, tree: ast.AST, filename: str) -> None: + """Construct. + + Args: + tree: The AST syntax tree for a file. + filename: The name of the file being processed. + """ + self._tree = tree + self._filename = Path(filename).name + + def _get_file_type(self) -> types_.FileType: + """Get the file type from a filename. + + Returns: + The type of file. + """ + if re.match(self._test_filename_pattern, self._filename) is not None: + return types_.FileType.TEST + + if re.match(self._fixture_filename_pattern, self._filename) is not None: + return types_.FileType.FIXTURE + + return types_.FileType.DEFAULT + + # No coverage since this only occurs from the command line + @staticmethod + def add_options(option_manager: OptionManager) -> None: # pragma: nocover + """Add additional options to flake8. + + Args: + option_manager: The flake8 OptionManager. + """ + option_manager.add_option( + TEST_FILENAME_PATTERN_ARG_NAME, + default=TEST_FILENAME_PATTERN_DEFAULT, + parse_from_config=True, + help=( + "The pattern to identify test files. " + f"(Default: {TEST_FILENAME_PATTERN_DEFAULT})" + ), + ) + option_manager.add_option( + TEST_FUNCTION_PATTERN_ARG_NAME, + default=TEST_FUNCTION_PATTERN_DEFAULT, + parse_from_config=True, + help=( + "The pattern for the name of test functions to exclude in test files. " + f"(Default: {TEST_FUNCTION_PATTERN_DEFAULT})" + ), + ) + option_manager.add_option( + FIXTURE_FILENAME_PATTERN_ARG_NAME, + default=FIXTURE_FILENAME_PATTERN_DEFAULT, + parse_from_config=True, + help=( + "The pattern to identify fixture files. " + f"(Default: {FIXTURE_FILENAME_PATTERN_DEFAULT})" + ), + ) + option_manager.add_option( + FIXTURE_DECORATOR_PATTERN_ARG_NAME, + default=FIXTURE_DECORATOR_PATTERN_DEFAULT, + parse_from_config=True, + help=( + "The pattern for the decorator name to exclude fixture functions. " + f"(Default: {FIXTURE_DECORATOR_PATTERN_DEFAULT})" + ), + ) + + # No coverage since this only occurs from the command line + @classmethod + def parse_options(cls, options: argparse.Namespace) -> None: # pragma: nocover + """Record the value of the options. + + Args: + options: The options passed to flake8. + """ + cls._test_filename_pattern = ( + getattr(options, _cli_arg_name_to_attr(TEST_FILENAME_PATTERN_ARG_NAME), None) + or TEST_FILENAME_PATTERN_DEFAULT + ) + cls._test_function_pattern = ( + getattr(options, _cli_arg_name_to_attr(TEST_FUNCTION_PATTERN_ARG_NAME), None) + or TEST_FUNCTION_PATTERN_DEFAULT + ) + cls._fixture_filename_pattern = ( + getattr(options, _cli_arg_name_to_attr(FIXTURE_FILENAME_PATTERN_ARG_NAME), None) + or FIXTURE_FILENAME_PATTERN_DEFAULT + ) + cls._fixture_decorator_pattern = ( + getattr(options, _cli_arg_name_to_attr(FIXTURE_DECORATOR_PATTERN_ARG_NAME), None) + or FIXTURE_DECORATOR_PATTERN_DEFAULT + ) + + def run(self) -> Iterator[tuple[int, int, str, type["Plugin"]]]: + """Lint a file. + + Yields: + All the problems that were found. + """ + file_type = self._get_file_type() + visitor = Visitor( + file_type=file_type, + test_function_pattern=self._test_function_pattern, + fixture_decorator_pattern=self._fixture_decorator_pattern, + ) + visitor.visit(node=self._tree) + yield from ( + (problem.lineno, problem.col_offset, problem.msg, type(self)) + for problem in visitor.problems + ) diff --git a/flake8_docstrings_complete/args.py b/flake8_docstrings_complete/args.py new file mode 100644 index 0000000..d84b35c --- /dev/null +++ b/flake8_docstrings_complete/args.py @@ -0,0 +1,123 @@ +"""The arguments section checks.""" + +from __future__ import annotations + +import ast +from typing import Iterator + +from . import docstring, types_ +from .constants import ERROR_CODE_PREFIX, MORE_INFO_BASE + +ARGS_SECTION_NOT_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}020" +ARGS_SECTION_NOT_IN_DOCSTR_MSG = ( + f"{ARGS_SECTION_NOT_IN_DOCSTR_CODE} a function/ method with arguments should have the " + "arguments section in the docstring" + f"{MORE_INFO_BASE}{ARGS_SECTION_NOT_IN_DOCSTR_CODE.lower()}" +) +ARGS_SECTION_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}021" +ARGS_SECTION_IN_DOCSTR_MSG = ( + f"{ARGS_SECTION_IN_DOCSTR_CODE} a function/ method without arguments should not have the " + "arguments section in the docstring" + f"{MORE_INFO_BASE}{ARGS_SECTION_IN_DOCSTR_CODE.lower()}" +) +MULT_ARGS_SECTIONS_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}022" +MULT_ARGS_SECTIONS_IN_DOCSTR_MSG = ( + f"{MULT_ARGS_SECTIONS_IN_DOCSTR_CODE} a docstring should only contain a single arguments " + f"section, found %s{MORE_INFO_BASE}{MULT_ARGS_SECTIONS_IN_DOCSTR_CODE.lower()}" +) +ARG_NOT_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}023" +ARG_NOT_IN_DOCSTR_MSG = ( + f'{ARG_NOT_IN_DOCSTR_CODE} "%s" argument should be described in the docstring{MORE_INFO_BASE}' + f"{ARG_NOT_IN_DOCSTR_CODE.lower()}" +) +ARG_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}024" +ARG_IN_DOCSTR_MSG = ( + f'{ARG_IN_DOCSTR_CODE} "%s" argument should not be described in the docstring{MORE_INFO_BASE}' + f"{ARG_IN_DOCSTR_CODE.lower()}" +) + +SKIP_ARGS = {"self", "cls"} +UNUSED_ARGS_PREFIX = "_" + + +def _iter_args(args: ast.arguments) -> Iterator[ast.arg]: + """Iterate over all arguments. + + Adds vararg and kwarg to the args. + + Args: + args: The arguments to iter over. + + Yields: + All the arguments. + """ + yield from (arg for arg in args.args if arg.arg not in SKIP_ARGS) + yield from (arg for arg in args.posonlyargs if arg.arg not in SKIP_ARGS) + yield from (arg for arg in args.kwonlyargs) + if args.vararg: + yield args.vararg + if args.kwarg: + yield args.kwarg + + +def check( + docstr_info: docstring.Docstring, docstr_node: ast.Constant, args: ast.arguments +) -> Iterator[types_.Problem]: + """Check that all function/ method arguments are described in the docstring. + + Check the function/ method has at most one args section. + Check that all arguments of the function/ method are documented except certain arguments (like + self). + Check that a function/ method without arguments does not have an args section. + + Args: + docstr_info: Information about the docstring. + docstr_node: The docstring node. + args: The arguments of the function. + + Yields: + All the problems with the arguments. + """ + all_args = list(_iter_args(args)) + all_used_args = list(arg for arg in all_args if not arg.arg.startswith(UNUSED_ARGS_PREFIX)) + + # Check that args section is in docstring if function/ method has used arguments + if all_used_args and docstr_info.args is None: + yield types_.Problem( + docstr_node.lineno, docstr_node.col_offset, ARGS_SECTION_NOT_IN_DOCSTR_MSG + ) + # Check that args section is not in docstring if function/ method has no arguments + if not all_args and docstr_info.args is not None: + yield types_.Problem( + docstr_node.lineno, docstr_node.col_offset, ARGS_SECTION_IN_DOCSTR_MSG + ) + elif all_args and docstr_info.args is not None: + docstr_args = set(docstr_info.args) + + # Check for multiple args sections + if len(docstr_info.args_sections) > 1: + yield types_.Problem( + docstr_node.lineno, + docstr_node.col_offset, + MULT_ARGS_SECTIONS_IN_DOCSTR_MSG % ",".join(docstr_info.args_sections), + ) + + # Check for function arguments that are not in the docstring + yield from ( + types_.Problem(arg.lineno, arg.col_offset, ARG_NOT_IN_DOCSTR_MSG % arg.arg) + for arg in all_used_args + if arg.arg not in docstr_args + ) + + # Check for arguments in the docstring that are not function arguments + func_args = set(arg.arg for arg in all_args) + yield from ( + types_.Problem(docstr_node.lineno, docstr_node.col_offset, ARG_IN_DOCSTR_MSG % arg) + for arg in sorted(docstr_args - func_args) + ) + + # Check for empty args section + if not all_used_args and len(docstr_info.args) == 0: + yield types_.Problem( + docstr_node.lineno, docstr_node.col_offset, ARGS_SECTION_IN_DOCSTR_MSG + ) diff --git a/flake8_docstrings_complete/attrs.py b/flake8_docstrings_complete/attrs.py new file mode 100644 index 0000000..70172d1 --- /dev/null +++ b/flake8_docstrings_complete/attrs.py @@ -0,0 +1,269 @@ +"""The arguments section checks.""" + +from __future__ import annotations + +import ast +from itertools import chain +from typing import Iterable, Iterator + +from . import docstring, types_ +from .constants import ERROR_CODE_PREFIX, MORE_INFO_BASE + +ATTRS_SECTION_NOT_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}060" +ATTRS_SECTION_NOT_IN_DOCSTR_MSG = ( + f"{ATTRS_SECTION_NOT_IN_DOCSTR_CODE} a class with attributes should have the attributes " + f"section in the docstring{MORE_INFO_BASE}{ATTRS_SECTION_NOT_IN_DOCSTR_CODE.lower()}" +) +ATTRS_SECTION_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}061" +ATTRS_SECTION_IN_DOCSTR_MSG = ( + f"{ATTRS_SECTION_IN_DOCSTR_CODE} a function/ method without attributes should not have the " + f"attributes section in the docstring{MORE_INFO_BASE}{ATTRS_SECTION_IN_DOCSTR_CODE.lower()}" +) +MULT_ATTRS_SECTIONS_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}062" +MULT_ATTRS_SECTIONS_IN_DOCSTR_MSG = ( + f"{MULT_ATTRS_SECTIONS_IN_DOCSTR_CODE} a docstring should only contain a single attributes " + f"section, found %s{MORE_INFO_BASE}{MULT_ATTRS_SECTIONS_IN_DOCSTR_CODE.lower()}" +) +ATTR_NOT_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}063" +ATTR_NOT_IN_DOCSTR_MSG = ( + f'{ATTR_NOT_IN_DOCSTR_CODE} "%s" attribute should be described in the docstring' + f"{MORE_INFO_BASE}{ATTR_NOT_IN_DOCSTR_CODE.lower()}" +) +ATTR_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}064" +ATTR_IN_DOCSTR_MSG = ( + f'{ATTR_IN_DOCSTR_CODE} "%s" attribute should not be described in the docstring' + f"{MORE_INFO_BASE}{ATTR_IN_DOCSTR_CODE.lower()}" +) + +CLASS_SELF_CLS = {"self", "cls"} +PRIVATE_ATTR_PREFIX = "_" + + +def _get_class_target_name(target: ast.expr) -> ast.Name | None: + """Get the name of the target for an assignment on the class. + + Args: + target: The target node of an assignment expression. + + Returns: + The Name node of the target. + """ + if isinstance(target, ast.Name): + return target + if isinstance(target, ast.Attribute): + if isinstance(target.value, ast.Name): + return target.value + if isinstance(target.value, ast.Attribute): + return _get_class_target_name(target=target.value) + + # There is no valid syntax that gets to here + return None # pragma: nocover + + +def _iter_class_attrs( + nodes: Iterable[ast.Assign | ast.AnnAssign | ast.AugAssign], +) -> Iterator[types_.Node]: + """Get the node of the variable being assigned at the class level if the target is a Name. + + Args: + nodes: The assign nodes. + + Yields: + All the nodes of name targets of the assignment expressions. + """ + for node in nodes: + if isinstance(node, ast.Assign): + target_names = filter( + None, (_get_class_target_name(target) for target in node.targets) + ) + yield from ( + types_.Node(lineno=name.lineno, col_offset=name.col_offset, name=name.id) + for name in target_names + ) + else: + target_name = _get_class_target_name(target=node.target) + # No valid syntax reaches else + if target_name is not None: # pragma: nobranch + yield types_.Node( + lineno=target_name.lineno, + col_offset=target_name.col_offset, + name=target_name.id, + ) + + +def _get_method_target_node(target: ast.expr) -> types_.Node | None: + """Get the node of the target for an assignment in a method. + + Args: + target: The target node of an assignment expression. + + Returns: + The Name node of the target. + """ + if isinstance(target, ast.Attribute): + if isinstance(target.value, ast.Name) and target.value.id in CLASS_SELF_CLS: + return types_.Node( + lineno=target.lineno, col_offset=target.col_offset, name=target.attr + ) + # No valid syntax reaches else + if isinstance(target.value, ast.Attribute): # pragma: nobranch + return _get_method_target_node(target=target.value) + + return None + + +def _iter_method_attrs( + nodes: Iterable[ast.Assign | ast.AnnAssign | ast.AugAssign], +) -> Iterator[types_.Node]: + """Get the node of the class or instance variable being assigned in methods. + + Args: + nodes: The assign nodes. + + Yields: + All the nodes of name targets of the assignment expressions in methods. + """ + for node in nodes: + if isinstance(node, ast.Assign): + yield from filter(None, (_get_method_target_node(target) for target in node.targets)) + else: + target_node = _get_method_target_node(node.target) + # No valid syntax reaches else + if target_node is not None: # pragma: nobranch + yield target_node + + +def check( + docstr_info: docstring.Docstring, + docstr_node: ast.Constant, + class_assign_nodes: Iterable[ast.Assign | ast.AnnAssign | ast.AugAssign], + method_assign_nodes: Iterable[ast.Assign | ast.AnnAssign | ast.AugAssign], +) -> Iterator[types_.Problem]: + """Check that all class attributes are described in the docstring. + + Check the class has at most one attrs section. + Check that all attributes of the class are documented. + Check that a class without attributes does not have an attrs section. + + Args: + docstr_info: Information about the docstring. + docstr_node: The docstring node. + class_assign_nodes: The attributes of the class assigned in the class. + method_assign_nodes: The attributes of the class assigned in the methods. + + Yields: + All the problems with the attributes. + """ + all_targets = list( + chain(_iter_class_attrs(class_assign_nodes), _iter_method_attrs(method_assign_nodes)) + ) + all_public_targets = list( + target for target in all_targets if not target.name.startswith(PRIVATE_ATTR_PREFIX) + ) + + # Check that attrs section is in docstring if function/ method has public attributes + if all_public_targets and docstr_info.attrs is None: + yield types_.Problem( + docstr_node.lineno, docstr_node.col_offset, ATTRS_SECTION_NOT_IN_DOCSTR_MSG + ) + # Check that attrs section is not in docstring if class has no attributes + if not all_targets and docstr_info.attrs is not None: + yield types_.Problem( + docstr_node.lineno, docstr_node.col_offset, ATTRS_SECTION_IN_DOCSTR_MSG + ) + elif all_targets and docstr_info.attrs is not None: + docstr_attrs = set(docstr_info.attrs) + + # Check for multiple attrs sections + if len(docstr_info.attrs_sections) > 1: + yield types_.Problem( + docstr_node.lineno, + docstr_node.col_offset, + MULT_ATTRS_SECTIONS_IN_DOCSTR_MSG % ",".join(docstr_info.attrs_sections), + ) + + # Check for class attributes that are not in the docstring + yield from ( + types_.Problem(target.lineno, target.col_offset, ATTR_NOT_IN_DOCSTR_MSG % target.name) + for target in all_public_targets + if target.name not in docstr_attrs + ) + + # Check for attributes in the docstring that are not class attributes + class_attrs = set(target.name for target in all_targets) + yield from ( + types_.Problem(docstr_node.lineno, docstr_node.col_offset, ATTR_IN_DOCSTR_MSG % attr) + for attr in sorted(docstr_attrs - class_attrs) + ) + + # Check for empty attrs section + if not all_public_targets and len(docstr_info.attrs) == 0: + yield types_.Problem( + docstr_node.lineno, docstr_node.col_offset, ATTRS_SECTION_IN_DOCSTR_MSG + ) + + +class VisitorWithinClass(ast.NodeVisitor): + """Visits AST nodes within a class but not nested class and functions nested within functions. + + Attrs: + class_assign_nodes: All the return nodes encountered within the class. + method_assign_nodes: All the return nodes encountered within the class methods. + """ + + class_assign_nodes: list[ast.Assign | ast.AnnAssign | ast.AugAssign] + method_assign_nodes: list[ast.Assign | ast.AnnAssign | ast.AugAssign] + _visited_once: bool + _visited_top_level: bool + + def __init__(self) -> None: + """Construct.""" + self.class_assign_nodes = [] + self.method_assign_nodes = [] + self._visited_once = False + self._visited_top_level = False + + def visit_assign(self, node: ast.Assign | ast.AnnAssign | ast.AugAssign) -> None: + """Record assign node. + + Args: + node: The assign node to record. + """ + if not self._visited_top_level: + self.class_assign_nodes.append(node) + else: + self.method_assign_nodes.append(node) + + # Ensure recursion continues + self.generic_visit(node) + + def visit_once(self, node: ast.AST) -> None: + """Visit the node once and then skip. + + Args: + node: The node being visited. + """ + if not self._visited_once: + self._visited_once = True + self.generic_visit(node=node) + + def visit_top_level(self, node: ast.AST) -> None: + """Visit the top level node but skip any nested nodes. + + Args: + node: The node being visited. + """ + if not self._visited_top_level: + self._visited_top_level = True + self.generic_visit(node=node) + self._visited_top_level = False + + # The functions must be called the same as the name of the node + # Visit assign nodes + visit_Assign = visit_assign # noqa: N815,DCO063 + visit_AnnAssign = visit_assign # noqa: N815,DCO063 + visit_AugAssign = visit_assign # noqa: N815,DCO063 + # Ensure that nested functions and classes are not iterated over + visit_FunctionDef = visit_top_level # noqa: N815,DCO063 + visit_AsyncFunctionDef = visit_top_level # noqa: N815,DCO063 + visit_ClassDef = visit_once # noqa: N815,DCO063 diff --git a/flake8_docstrings_complete/constants.py b/flake8_docstrings_complete/constants.py new file mode 100644 index 0000000..8f579c5 --- /dev/null +++ b/flake8_docstrings_complete/constants.py @@ -0,0 +1,6 @@ +"""Shared constants for the linter.""" + +ERROR_CODE_PREFIX = "DCO" +MORE_INFO_BASE = ( + ", more information: https://github.com/jdkandersson/flake8-docstrings-complete#fix-" +) diff --git a/flake8_docstrings_complete/docstring.py b/flake8_docstrings_complete/docstring.py new file mode 100644 index 0000000..8aba35b --- /dev/null +++ b/flake8_docstrings_complete/docstring.py @@ -0,0 +1,161 @@ +"""Parse a docstring to retrieve the sections and sub-sections.""" + +from __future__ import annotations + +import contextlib +import itertools +import re +from typing import Iterable, Iterator, NamedTuple + + +class _Section(NamedTuple): + """Represents a docstring section. + + Attrs: + name: Short description of the section. + subs: The names of the sub-sections included in the section. None if the section has no + sub-sections. + """ + + name: str + subs: tuple[str, ...] + + +class Docstring(NamedTuple): + """Represents a docstring. + + Attrs: + args: The arguments described in the docstring. None if the docstring doesn't have the args + section. + args_sections: All the arguments sections. + attrs: The attributes described in the docstring. None if the docstring doesn't have the + attrs section. + attrs_sections: All the attributes sections. + returns: Whether the docstring has the returns section. + returns_sections: All the returns sections. + yields: Whether the docstring has the yields section. + yields_sections: All the yields sections. + raises: The exceptions described in the docstring. None if the docstring doesn't have the + raises section. + raises_sections: All the raises sections. + """ + + args: tuple[str, ...] | None = None + args_sections: tuple[str, ...] = () + attrs: tuple[str, ...] | None = None + attrs_sections: tuple[str, ...] = () + returns: bool = False + returns_sections: tuple[str, ...] = () + yields: bool = False + yields_sections: tuple[str, ...] = () + raises: tuple[str, ...] | None = None + raises_sections: tuple[str, ...] = () + + +_SECTION_NAMES = { + "args": {"args", "arguments", "parameters"}, + "attrs": {"attributes", "attrs"}, + "returns": {"return", "returns"}, + "yields": {"yield", "yields"}, + "raises": {"raises", "raise"}, +} +_WHITESPACE_REGEX = r"\s*" +_SECTION_START_PATTERN = re.compile(rf"{_WHITESPACE_REGEX}(\w+):") +_SUB_SECTION_PATTERN = re.compile(rf"{_WHITESPACE_REGEX}(\w+)( \(.*\))?:") +_SECTION_END_PATTERN = re.compile(rf"{_WHITESPACE_REGEX}$") + + +def _get_sections(lines: Iterable[str]) -> Iterator[_Section]: + """Retrieve all the sectiond from the docstring. + + A section start is indicated by a line that starts with zero or more whitespace followed by a + word and then a colon. + A section end is indicated by a line with just whitespace or that there a no more lines in the + docstring. + A sub-section is indicated by a line with zero or more whitespace characters, followed by a + word, optionally followed by arbitrary characters enclosed in brackets followed by a colon. + + Args: + lines: The lines of the docstring. + + Yields: + All the sections in the docstring. + """ + lines = iter(lines) + + with contextlib.suppress(StopIteration): + while True: + # Find the start of the next section + section_name = next( + filter(None, (_SECTION_START_PATTERN.match(line) for line in lines)) + ).group(1) + # Get all the lines of the section + section_lines = itertools.takewhile( + lambda line: _SECTION_END_PATTERN.match(line) is None, lines + ) + # Retrieve sub section from section lines + sub_section_matches = (_SUB_SECTION_PATTERN.match(line) for line in section_lines) + sub_sections = (match.group(1) for match in sub_section_matches if match is not None) + yield _Section(name=section_name, subs=tuple(sub_sections)) + + +def _get_section_by_name(name: str, sections: Iterable[_Section]) -> _Section | None: + """Get the section by name. + + Args: + name: The name of the section. + sections: The sections to retrieve from. + + Returns: + The section or None if it wasn't found. + """ + sections = iter(sections) + return next( + (section for section in sections if section.name.lower() in _SECTION_NAMES[name]), + None, + ) + + +def _get_all_section_names_by_name(name: str, sections: Iterable[_Section]) -> Iterator[str]: + """Get all the section names in a docstring by name. + + Args: + name: The name of the section. + sections: The sections to retrieve from. + + Yields: + The names of the sections that match the name. + """ + sections = iter(sections) + yield from ( + section.name for section in sections if section.name.lower() in _SECTION_NAMES[name] + ) + + +def parse(value: str) -> Docstring: + """Parse a docstring. + + Args: + value: The docstring to parse. + + Returns: + The parsed docstring. + """ + sections = list(_get_sections(lines=value.splitlines())) + + args_section = _get_section_by_name("args", sections) + attrs_section = _get_section_by_name("attrs", sections) + raises_section = _get_section_by_name("raises", sections) + + return Docstring( + args=args_section.subs if args_section is not None else None, + args_sections=tuple(_get_all_section_names_by_name(name="args", sections=sections)), + attrs=attrs_section.subs if attrs_section is not None else None, + attrs_sections=tuple(_get_all_section_names_by_name(name="attrs", sections=sections)), + returns=_get_section_by_name("returns", sections) is not None, + returns_sections=tuple(_get_all_section_names_by_name(name="returns", sections=sections)), + yields=_get_section_by_name("yields", sections) is not None, + yields_sections=tuple(_get_all_section_names_by_name(name="yields", sections=sections)), + raises=raises_section.subs if raises_section is not None else None, + raises_sections=tuple(_get_all_section_names_by_name(name="raises", sections=sections)), + ) diff --git a/flake8_docstrings_complete/raises.py b/flake8_docstrings_complete/raises.py new file mode 100644 index 0000000..92668f5 --- /dev/null +++ b/flake8_docstrings_complete/raises.py @@ -0,0 +1,144 @@ +"""The raises section checks.""" + +from __future__ import annotations + +import ast +from typing import Iterable, Iterator + +from . import docstring, types_ +from .constants import ERROR_CODE_PREFIX, MORE_INFO_BASE + +RAISES_SECTION_NOT_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}050" +RAISES_SECTION_NOT_IN_DOCSTR_MSG = ( + f"{RAISES_SECTION_NOT_IN_DOCSTR_CODE} a function/ method that raises an exception should have " + "the raises section in the docstring" + f"{MORE_INFO_BASE}{RAISES_SECTION_NOT_IN_DOCSTR_CODE.lower()}" +) +RAISES_SECTION_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}051" +RAISES_SECTION_IN_DOCSTR_MSG = ( + f"{RAISES_SECTION_IN_DOCSTR_CODE} a function/ method that does not raise an exception should " + "not have the raises section in the docstring" + f"{MORE_INFO_BASE}{RAISES_SECTION_IN_DOCSTR_CODE.lower()}" +) +MULT_RAISES_SECTIONS_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}052" +MULT_RAISES_SECTIONS_IN_DOCSTR_MSG = ( + f"{MULT_RAISES_SECTIONS_IN_DOCSTR_CODE} a docstring should only contain a single raises " + "section, found %s" + f"{MORE_INFO_BASE}{MULT_RAISES_SECTIONS_IN_DOCSTR_CODE.lower()}" +) +EXC_NOT_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}053" +EXC_NOT_IN_DOCSTR_MSG = ( + f'{EXC_NOT_IN_DOCSTR_CODE} "%s" exception should be described in the docstring{MORE_INFO_BASE}' + f"{EXC_NOT_IN_DOCSTR_CODE.lower()}" +) +EXC_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}054" +EXC_IN_DOCSTR_MSG = ( + f'{EXC_IN_DOCSTR_CODE} "%s" exception should not be described in the docstring{MORE_INFO_BASE}' + f"{EXC_IN_DOCSTR_CODE.lower()}" +) +RE_RAISE_NO_EXC_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}055" +RE_RAISE_NO_EXC_IN_DOCSTR_MSG = ( + f"{RE_RAISE_NO_EXC_IN_DOCSTR_CODE} a function/ method that re-raises exceptions should " + "describe at least one exception in the raises section of the docstring" + f"{MORE_INFO_BASE}{RE_RAISE_NO_EXC_IN_DOCSTR_CODE.lower()}" +) + + +def _get_exc_node(node: ast.Raise) -> types_.Node | None: + """Get the exception value from raise. + + Args: + node: The raise node. + + Returns: + The exception node. + """ + if isinstance(node.exc, ast.Name): + return types_.Node( + name=node.exc.id, lineno=node.exc.lineno, col_offset=node.exc.col_offset + ) + if isinstance(node.exc, ast.Attribute): + return types_.Node( + name=node.exc.attr, lineno=node.exc.lineno, col_offset=node.exc.col_offset + ) + if isinstance(node.exc, ast.Call): + if isinstance(node.exc.func, ast.Name): + return types_.Node( + name=node.exc.func.id, + lineno=node.exc.func.lineno, + col_offset=node.exc.func.col_offset, + ) + if isinstance(node.exc.func, ast.Attribute): + return types_.Node( + name=node.exc.func.attr, + lineno=node.exc.func.lineno, + col_offset=node.exc.func.col_offset, + ) + + return None + + +def check( + docstr_info: docstring.Docstring, docstr_node: ast.Constant, raise_nodes: Iterable[ast.Raise] +) -> Iterator[types_.Problem]: + """Check that all raised exceptions arguments are described in the docstring. + + Check the function/ method has at most one raises section. + Check that all raised exceptions of the function/ method are documented. + Check that a function/ method that doesn't raise exceptions does not have a raises section. + + Args: + docstr_info: Information about the docstring. + docstr_node: The docstring node. + raise_nodes: The raise nodes. + + Yields: + All the problems with exceptions. + """ + all_excs = list(_get_exc_node(node) for node in raise_nodes) + has_raise_no_value = any(exc is None for exc in all_excs) + all_raise_no_value = all(exc is None for exc in all_excs) + + # Check that raises section is in docstring if function/ method raises exceptions + if all_excs and docstr_info.raises is None: + yield types_.Problem( + docstr_node.lineno, docstr_node.col_offset, RAISES_SECTION_NOT_IN_DOCSTR_MSG + ) + # Check that raises section is not in docstring if function/ method raises no exceptions + if not all_excs and docstr_info.raises is not None: + yield types_.Problem( + docstr_node.lineno, docstr_node.col_offset, RAISES_SECTION_IN_DOCSTR_MSG + ) + # Check for empty raises section + if (all_excs and all_raise_no_value) and ( + docstr_info.raises is None or len(docstr_info.raises) == 0 + ): + yield types_.Problem( + docstr_node.lineno, docstr_node.col_offset, RE_RAISE_NO_EXC_IN_DOCSTR_MSG + ) + elif all_excs and docstr_info.raises is not None: + docstr_raises = set(docstr_info.raises) + + # Check for multiple raises sections + if len(docstr_info.raises_sections) > 1: + yield types_.Problem( + docstr_node.lineno, + docstr_node.col_offset, + MULT_RAISES_SECTIONS_IN_DOCSTR_MSG % ",".join(docstr_info.raises_sections), + ) + + # Check for exceptions that are not raised + yield from ( + types_.Problem(exc.lineno, exc.col_offset, EXC_NOT_IN_DOCSTR_MSG % exc.name) + for exc in all_excs + if exc and exc.name not in docstr_raises + ) + + # Check for exceptions in the docstring that are not raised unless function has a raises + # without an exception + if not has_raise_no_value: + func_exc = set(exc.name for exc in all_excs if exc is not None) + yield from ( + types_.Problem(docstr_node.lineno, docstr_node.col_offset, EXC_IN_DOCSTR_MSG % exc) + for exc in sorted(docstr_raises - func_exc) + ) diff --git a/flake8_docstrings_complete/types_.py b/flake8_docstrings_complete/types_.py new file mode 100644 index 0000000..7266db0 --- /dev/null +++ b/flake8_docstrings_complete/types_.py @@ -0,0 +1,48 @@ +"""Types that support execution.""" + +from __future__ import annotations + +import enum +from typing import NamedTuple + + +class Problem(NamedTuple): + """Represents a problem within the code. + + Attrs: + lineno: The line number the problem occurred on + col_offset: The column the problem occurred on + msg: The message explaining the problem + """ + + lineno: int + col_offset: int + msg: str + + +class FileType(str, enum.Enum): + """The type of file being processed. + + Attrs: + TEST: A file with tests. + FIXTURE: A file with fixtures. + DEFAULT: All other files. + """ + + TEST = "test" + FIXTURE = "fixture" + DEFAULT = "default" + + +class Node(NamedTuple): + """Information about a node. + + Attrs: + name: Short description of the node. + lineno: The line number the node is on. + col_offset: The column of the node. + """ + + name: str + lineno: int + col_offset: int diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..c70799b --- /dev/null +++ b/poetry.lock @@ -0,0 +1,754 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + +[[package]] +name = "astpretty" +version = "3.0.0" +description = "Pretty print the output of python stdlib `ast.parse`." +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "astpretty-3.0.0-py2.py3-none-any.whl", hash = "sha256:15bfd47593667169485a1fa7938b8de9445b11057d6f2b6e214b2f70667f94b6"}, + {file = "astpretty-3.0.0.tar.gz", hash = "sha256:b08c95f32e5994454ea99882ff3c4a0afc8254c38998a0ed4b479dba448dc581"}, +] + +[[package]] +name = "astroid" +version = "2.12.13" +description = "An abstract syntax tree for Python with inference support." +category = "dev" +optional = false +python-versions = ">=3.7.2" +files = [ + {file = "astroid-2.12.13-py3-none-any.whl", hash = "sha256:10e0ad5f7b79c435179d0d0f0df69998c4eef4597534aae44910db060baeb907"}, + {file = "astroid-2.12.13.tar.gz", hash = "sha256:1493fe8bd3dfd73dc35bd53c9d5b6e49ead98497c47b2307662556a5692d29d7"}, +] + +[package.dependencies] +lazy-object-proxy = ">=1.4.0" +typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} +wrapt = [ + {version = ">=1.11,<2", markers = "python_version < \"3.11\""}, + {version = ">=1.14,<2", markers = "python_version >= \"3.11\""}, +] + +[[package]] +name = "attrs" +version = "22.2.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, + {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] +tests = ["attrs[tests-no-zope]", "zope.interface"] +tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] + +[[package]] +name = "black" +version = "22.12.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, + {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, + {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, + {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, + {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, + {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, + {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, + {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, + {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, + {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, + {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, + {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "codespell" +version = "2.2.2" +description = "Codespell" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "codespell-2.2.2-py3-none-any.whl", hash = "sha256:87dfcd9bdc9b3cb8b067b37f0af22044d7a84e28174adfc8eaa203056b7f9ecc"}, + {file = "codespell-2.2.2.tar.gz", hash = "sha256:c4d00c02b5a2a55661f00d5b4b3b5a710fa803ced9a9d7e45438268b099c319c"}, +] + +[package.extras] +dev = ["check-manifest", "flake8", "pytest", "pytest-cov", "pytest-dependency", "tomli"] +hard-encoding-detection = ["chardet"] +toml = ["tomli"] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "6.5.0" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, + {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, + {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, + {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, + {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, + {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, + {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, + {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, + {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, + {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, + {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, + {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, + {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, + {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, + {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, + {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, + {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, + {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, + {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, + {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "dill" +version = "0.3.6" +description = "serialize all of python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "dill-0.3.6-py3-none-any.whl", hash = "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0"}, + {file = "dill-0.3.6.tar.gz", hash = "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"}, +] + +[package.extras] +graph = ["objgraph (>=1.7.2)"] + +[[package]] +name = "exceptiongroup" +version = "1.1.0" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, + {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "flake8" +version = "6.0.0" +description = "the modular source code checker: pep8 pyflakes and co" +category = "main" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "flake8-6.0.0-py2.py3-none-any.whl", hash = "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7"}, + {file = "flake8-6.0.0.tar.gz", hash = "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.10.0,<2.11.0" +pyflakes = ">=3.0.0,<3.1.0" + +[[package]] +name = "flake8-builtins" +version = "2.1.0" +description = "Check for python builtins being used as variables or parameters." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "flake8-builtins-2.1.0.tar.gz", hash = "sha256:12ff1ee96dd4e1f3141141ee6c45a5c7d3b3c440d0949e9b8d345c42b39c51d4"}, + {file = "flake8_builtins-2.1.0-py3-none-any.whl", hash = "sha256:469e8f03d6d0edf4b1e62b6d5a97dce4598592c8a13ec8f0952e7a185eba50a1"}, +] + +[package.dependencies] +flake8 = "*" + +[package.extras] +test = ["pytest"] + +[[package]] +name = "flake8-docstrings" +version = "1.6.0" +description = "Extension for flake8 which uses pydocstyle to check docstrings" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "flake8-docstrings-1.6.0.tar.gz", hash = "sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b"}, + {file = "flake8_docstrings-1.6.0-py2.py3-none-any.whl", hash = "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde"}, +] + +[package.dependencies] +flake8 = ">=3" +pydocstyle = ">=2.1" + +[[package]] +name = "flake8-test-docs" +version = "1.0.7" +description = "A linter that checks test docstrings for the arrange/act/assert structure" +category = "dev" +optional = false +python-versions = ">=3.8.1,<4.0.0" +files = [ + {file = "flake8_test_docs-1.0.7-py3-none-any.whl", hash = "sha256:f5ad2814414c535bdcd32c9cc9a78147bc697fcb70a10d4a2dee5ab28ec194a6"}, + {file = "flake8_test_docs-1.0.7.tar.gz", hash = "sha256:a217d870a5b8045e38333e322cde68ef076f4e962ddc8e32abe6c676066dde07"}, +] + +[package.dependencies] +flake8 = ">=6,<7" +typing_extensions = {version = ">=4,<5", markers = "python_version < \"3.10\""} + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] + +[[package]] +name = "isort" +version = "5.11.4" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "isort-5.11.4-py3-none-any.whl", hash = "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"}, + {file = "isort-5.11.4.tar.gz", hash = "sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6"}, +] + +[package.extras] +colors = ["colorama (>=0.4.3,<0.5.0)"] +pipfile-deprecated-finder = ["pipreqs", "requirementslib"] +plugins = ["setuptools"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] + +[[package]] +name = "lazy-object-proxy" +version = "1.8.0" +description = "A fast and thorough lazy object proxy." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "lazy-object-proxy-1.8.0.tar.gz", hash = "sha256:c219a00245af0f6fa4e95901ed28044544f50152840c5b6a3e7b2568db34d156"}, + {file = "lazy_object_proxy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4fd031589121ad46e293629b39604031d354043bb5cdf83da4e93c2d7f3389fe"}, + {file = "lazy_object_proxy-1.8.0-cp310-cp310-win32.whl", hash = "sha256:b70d6e7a332eb0217e7872a73926ad4fdc14f846e85ad6749ad111084e76df25"}, + {file = "lazy_object_proxy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:eb329f8d8145379bf5dbe722182410fe8863d186e51bf034d2075eb8d85ee25b"}, + {file = "lazy_object_proxy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4e2d9f764f1befd8bdc97673261b8bb888764dfdbd7a4d8f55e4fbcabb8c3fb7"}, + {file = "lazy_object_proxy-1.8.0-cp311-cp311-win32.whl", hash = "sha256:e20bfa6db17a39c706d24f82df8352488d2943a3b7ce7d4c22579cb89ca8896e"}, + {file = "lazy_object_proxy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:14010b49a2f56ec4943b6cf925f597b534ee2fe1f0738c84b3bce0c1a11ff10d"}, + {file = "lazy_object_proxy-1.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6850e4aeca6d0df35bb06e05c8b934ff7c533734eb51d0ceb2d63696f1e6030c"}, + {file = "lazy_object_proxy-1.8.0-cp37-cp37m-win32.whl", hash = "sha256:5b51d6f3bfeb289dfd4e95de2ecd464cd51982fe6f00e2be1d0bf94864d58acd"}, + {file = "lazy_object_proxy-1.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:6f593f26c470a379cf7f5bc6db6b5f1722353e7bf937b8d0d0b3fba911998858"}, + {file = "lazy_object_proxy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c1c7c0433154bb7c54185714c6929acc0ba04ee1b167314a779b9025517eada"}, + {file = "lazy_object_proxy-1.8.0-cp38-cp38-win32.whl", hash = "sha256:d176f392dbbdaacccf15919c77f526edf11a34aece58b55ab58539807b85436f"}, + {file = "lazy_object_proxy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:afcaa24e48bb23b3be31e329deb3f1858f1f1df86aea3d70cb5c8578bfe5261c"}, + {file = "lazy_object_proxy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:71d9ae8a82203511a6f60ca5a1b9f8ad201cac0fc75038b2dc5fa519589c9288"}, + {file = "lazy_object_proxy-1.8.0-cp39-cp39-win32.whl", hash = "sha256:8f6ce2118a90efa7f62dd38c7dbfffd42f468b180287b748626293bf12ed468f"}, + {file = "lazy_object_proxy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:eac3a9a5ef13b332c059772fd40b4b1c3d45a3a2b05e33a361dee48e54a4dad0"}, + {file = "lazy_object_proxy-1.8.0-pp37-pypy37_pp73-any.whl", hash = "sha256:ae032743794fba4d171b5b67310d69176287b5bf82a21f588282406a79498891"}, + {file = "lazy_object_proxy-1.8.0-pp38-pypy38_pp73-any.whl", hash = "sha256:7e1561626c49cb394268edd00501b289053a652ed762c58e1081224c8d881cec"}, + {file = "lazy_object_proxy-1.8.0-pp39-pypy39_pp73-any.whl", hash = "sha256:ce58b2b3734c73e68f0e30e4e725264d4d6be95818ec0a0be4bb6bf9a7e79aa8"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mypy" +version = "0.991" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mypy-0.991-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab"}, + {file = "mypy-0.991-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d"}, + {file = "mypy-0.991-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6"}, + {file = "mypy-0.991-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb"}, + {file = "mypy-0.991-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305"}, + {file = "mypy-0.991-cp310-cp310-win_amd64.whl", hash = "sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c"}, + {file = "mypy-0.991-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372"}, + {file = "mypy-0.991-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f"}, + {file = "mypy-0.991-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33"}, + {file = "mypy-0.991-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05"}, + {file = "mypy-0.991-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad"}, + {file = "mypy-0.991-cp311-cp311-win_amd64.whl", hash = "sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297"}, + {file = "mypy-0.991-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813"}, + {file = "mypy-0.991-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711"}, + {file = "mypy-0.991-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd"}, + {file = "mypy-0.991-cp37-cp37m-win_amd64.whl", hash = "sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef"}, + {file = "mypy-0.991-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a"}, + {file = "mypy-0.991-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93"}, + {file = "mypy-0.991-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf"}, + {file = "mypy-0.991-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135"}, + {file = "mypy-0.991-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70"}, + {file = "mypy-0.991-cp38-cp38-win_amd64.whl", hash = "sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243"}, + {file = "mypy-0.991-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d"}, + {file = "mypy-0.991-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5"}, + {file = "mypy-0.991-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3"}, + {file = "mypy-0.991-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648"}, + {file = "mypy-0.991-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476"}, + {file = "mypy-0.991-cp39-cp39-win_amd64.whl", hash = "sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461"}, + {file = "mypy-0.991-py3-none-any.whl", hash = "sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb"}, + {file = "mypy-0.991.tar.gz", hash = "sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06"}, +] + +[package.dependencies] +mypy-extensions = ">=0.4.3" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=3.10" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] + +[[package]] +name = "packaging" +version = "22.0" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-22.0-py3-none-any.whl", hash = "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"}, + {file = "packaging-22.0.tar.gz", hash = "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3"}, +] + +[[package]] +name = "pathspec" +version = "0.10.3" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"}, + {file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"}, +] + +[[package]] +name = "pep8-naming" +version = "0.13.3" +description = "Check PEP-8 naming conventions, plugin for flake8" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pep8-naming-0.13.3.tar.gz", hash = "sha256:1705f046dfcd851378aac3be1cd1551c7c1e5ff363bacad707d43007877fa971"}, + {file = "pep8_naming-0.13.3-py3-none-any.whl", hash = "sha256:1a86b8c71a03337c97181917e2b472f0f5e4ccb06844a0d6f0a33522549e7a80"}, +] + +[package.dependencies] +flake8 = ">=5.0.0" + +[[package]] +name = "platformdirs" +version = "2.6.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, + {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, +] + +[package.extras] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pycodestyle" +version = "2.10.0" +description = "Python style guide checker" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pycodestyle-2.10.0-py2.py3-none-any.whl", hash = "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"}, + {file = "pycodestyle-2.10.0.tar.gz", hash = "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053"}, +] + +[[package]] +name = "pydocstyle" +version = "6.1.1" +description = "Python docstring style checker" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, + {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, +] + +[package.dependencies] +snowballstemmer = "*" + +[package.extras] +toml = ["toml"] + +[[package]] +name = "pyflakes" +version = "3.0.1" +description = "passive checker of Python programs" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyflakes-3.0.1-py2.py3-none-any.whl", hash = "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf"}, + {file = "pyflakes-3.0.1.tar.gz", hash = "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"}, +] + +[[package]] +name = "pylint" +version = "2.15.9" +description = "python code static checker" +category = "dev" +optional = false +python-versions = ">=3.7.2" +files = [ + {file = "pylint-2.15.9-py3-none-any.whl", hash = "sha256:349c8cd36aede4d50a0754a8c0218b43323d13d5d88f4b2952ddfe3e169681eb"}, + {file = "pylint-2.15.9.tar.gz", hash = "sha256:18783cca3cfee5b83c6c5d10b3cdb66c6594520ffae61890858fe8d932e1c6b4"}, +] + +[package.dependencies] +astroid = ">=2.12.13,<=2.14.0-dev0" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +dill = [ + {version = ">=0.2", markers = "python_version < \"3.11\""}, + {version = ">=0.3.6", markers = "python_version >= \"3.11\""}, +] +isort = ">=4.2.5,<6" +mccabe = ">=0.6,<0.8" +platformdirs = ">=2.2.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +tomlkit = ">=0.10.1" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} + +[package.extras] +spelling = ["pyenchant (>=3.2,<4.0)"] +testutils = ["gitpython (>3)"] + +[[package]] +name = "pytest" +version = "7.2.0" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, + {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, +] + +[package.dependencies] +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "4.0.0" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, + {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "tomlkit" +version = "0.11.6" +description = "Style preserving TOML library" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"}, + {file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"}, +] + +[[package]] +name = "typing-extensions" +version = "4.4.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, + {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, +] + +[[package]] +name = "wrapt" +version = "1.14.1" +description = "Module for decorators, wrappers and monkey patching." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +files = [ + {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, + {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, + {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, + {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, + {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, + {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, + {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, + {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, + {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, + {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, + {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, + {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, + {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, + {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, + {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, + {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, + {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, + {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, + {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, + {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, + {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, + {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.8.1" +content-hash = "b857748b061316e5ec7f97f274f39c14094c131c16213b070f9cb63e174d4b6b" diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 0000000..ab1033b --- /dev/null +++ b/poetry.toml @@ -0,0 +1,2 @@ +[virtualenvs] +in-project = true diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a118f77 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,82 @@ +[tool.poetry] +name = "flake8-docstrings-complete" +version = "1.0.0" +description = "A linter that checks docstrings are complete" +authors = ["David Andersson "] +license = "Apache 2.0" +readme = "README.md" +packages = [{include = "flake8_docstrings_complete"}] +classifiers = [ + "Framework :: Flake8", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.8", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Software Development :: Quality Assurance", +] + +[tool.poetry.dependencies] +python = "^3.8.1" +flake8 = "^6" + +[tool.poetry.group.dev.dependencies] +pytest = "^7" +mypy = "^0.991" +isort = "^5" +black = "^22" +coverage = {extras = ["toml"], version = "^6"} +pytest-cov = "^4" +flake8-docstrings = "^1" +flake8-builtins = "^2" +pep8-naming = "^0" +codespell = "^2" +pylint = "^2" +pydocstyle = "^6" +toml = "^0" +astpretty = "^3" +flake8-test-docs = "^1" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry.plugins."flake8.extension"] +DCO = "flake8_docstrings_complete:Plugin" + +[tool.black] +line-length = 99 +target-version = ["py38"] + +[tool.isort] +line_length = 99 +profile = "black" + +[tool.coverage.run] +branch = true + +[tool.coverage.report] +fail_under = 100 +show_missing = true + +[tool.mypy] +ignore_missing_imports = true +check_untyped_defs = true +disallow_untyped_defs = true + +[[tool.mypy.overrides]] +module = "tests.*" +disallow_untyped_defs = false + +[tool.pylint.messages_control] +enable = [ + "useless-suppression" +] +disable = [ + "wrong-import-position" +] \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..d420712 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Tests.""" diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..c210fac --- /dev/null +++ b/tests/integration/__init__.py @@ -0,0 +1 @@ +"""Integration tests.""" diff --git a/tests/integration/test___init__.py b/tests/integration/test___init__.py new file mode 100644 index 0000000..b0cff61 --- /dev/null +++ b/tests/integration/test___init__.py @@ -0,0 +1,559 @@ +"""Integration tests for plugin.""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +import pytest + +from flake8_docstrings_complete import ( + DOCSTR_MISSING_CODE, + FIXTURE_DECORATOR_PATTERN_ARG_NAME, + FIXTURE_DECORATOR_PATTERN_DEFAULT, + FIXTURE_FILENAME_PATTERN_ARG_NAME, + FIXTURE_FILENAME_PATTERN_DEFAULT, + MULT_RETURNS_SECTIONS_IN_DOCSTR_CODE, + MULT_YIELDS_SECTIONS_IN_DOCSTR_CODE, + RETURNS_SECTION_IN_DOCSTR_CODE, + RETURNS_SECTION_NOT_IN_DOCSTR_CODE, + TEST_FILENAME_PATTERN_ARG_NAME, + TEST_FILENAME_PATTERN_DEFAULT, + TEST_FUNCTION_PATTERN_ARG_NAME, + TEST_FUNCTION_PATTERN_DEFAULT, + YIELDS_SECTION_IN_DOCSTR_CODE, + YIELDS_SECTION_NOT_IN_DOCSTR_CODE, +) +from flake8_docstrings_complete.args import ( + ARG_IN_DOCSTR_CODE, + ARG_NOT_IN_DOCSTR_CODE, + ARGS_SECTION_IN_DOCSTR_CODE, + ARGS_SECTION_NOT_IN_DOCSTR_CODE, + ARGS_SECTION_NOT_IN_DOCSTR_MSG, + MULT_ARGS_SECTIONS_IN_DOCSTR_CODE, +) +from flake8_docstrings_complete.attrs import ( + ATTR_IN_DOCSTR_CODE, + ATTR_NOT_IN_DOCSTR_CODE, + ATTRS_SECTION_IN_DOCSTR_CODE, + ATTRS_SECTION_NOT_IN_DOCSTR_CODE, + MULT_ATTRS_SECTIONS_IN_DOCSTR_CODE, +) +from flake8_docstrings_complete.raises import ( + EXC_IN_DOCSTR_CODE, + EXC_NOT_IN_DOCSTR_CODE, + MULT_RAISES_SECTIONS_IN_DOCSTR_CODE, + RAISES_SECTION_IN_DOCSTR_CODE, + RAISES_SECTION_NOT_IN_DOCSTR_CODE, + RE_RAISE_NO_EXC_IN_DOCSTR_CODE, +) + + +def test_help(): + """ + given: linter + when: the flake8 help message is generated + then: plugin is registered with flake8 + """ + with subprocess.Popen( + f"{sys.executable} -m flake8 --help", + stdout=subprocess.PIPE, + shell=True, + ) as proc: + stdout = proc.communicate()[0].decode(encoding="utf-8") + + assert "flake8-docstrings-complete" in stdout + assert TEST_FILENAME_PATTERN_ARG_NAME in stdout + assert TEST_FILENAME_PATTERN_DEFAULT in stdout + assert TEST_FUNCTION_PATTERN_ARG_NAME in stdout + assert TEST_FUNCTION_PATTERN_DEFAULT in stdout + assert FIXTURE_FILENAME_PATTERN_ARG_NAME in stdout + assert FIXTURE_FILENAME_PATTERN_DEFAULT in stdout + assert FIXTURE_DECORATOR_PATTERN_ARG_NAME in stdout + assert FIXTURE_DECORATOR_PATTERN_DEFAULT in stdout + + +def create_code_file(code: str, filename: str, base_path: Path) -> Path: + """Create the code file with the given code. + + Args: + code: The code to write to the file. + filename: The name of the file to create. + base_path: The path to create the file within + + Returns: + The path to the code file. + """ + (code_file := base_path / filename).write_text(f'"""Docstring."""\n\n{code}') + return code_file + + +def test_fail(tmp_path: Path): + """ + given: file with Python code that fails the linting + when: flake8 is run against the code + then: the process exits with non-zero code and includes the error message + """ + code_file = create_code_file( + '\ndef foo(arg_1):\n """Docstring."""\n', "source.py", tmp_path + ) + + with subprocess.Popen( + f"{sys.executable} -m flake8 {code_file}", + stdout=subprocess.PIPE, + shell=True, + ) as proc: + stdout = proc.communicate()[0].decode(encoding="utf-8") + + assert ARGS_SECTION_NOT_IN_DOCSTR_MSG in stdout + assert proc.returncode + + +@pytest.mark.parametrize( + "code, filename, extra_args", + [ + pytest.param( + ''' +def foo(): + """Docstring.""" +''', + "source.py", + "", + id="default", + ), + pytest.param( + ''' +def _test(arg_1): + """ + arrange: line 1 + act: line 2 + assert: line 3 + """ +''', + "_test.py", + ( + f"{TEST_FILENAME_PATTERN_ARG_NAME} .*_test\\.py " + f"{TEST_FUNCTION_PATTERN_ARG_NAME} _test" + ), + id="custom test filename and function pattern", + ), + pytest.param( + ''' +def custom(): + """Docstring.""" + + +@custom +def fixture(arg_1): + """Docstring.""" +''', + "fixture.py", + ( + f"{FIXTURE_FILENAME_PATTERN_ARG_NAME} fixture\\.py " + f"{FIXTURE_DECORATOR_PATTERN_ARG_NAME} custom" + ), + id="custom fixture filename and function pattern", + ), + pytest.param( + f""" +def foo(): # noqa: {DOCSTR_MISSING_CODE} + pass +""", + "source.py", + "", + id=f"{DOCSTR_MISSING_CODE} disabled", + ), + pytest.param( + f''' +def foo(arg_1): + """Docstring.""" # noqa: {ARGS_SECTION_NOT_IN_DOCSTR_CODE} +''', + "source.py", + "", + id=f"{ARGS_SECTION_NOT_IN_DOCSTR_CODE} disabled", + ), + pytest.param( + f''' +def foo(): + """Docstring. + + Args: + Arguments. + """ # noqa: {ARGS_SECTION_IN_DOCSTR_CODE} +''', + "source.py", + "", + id=f"{ARGS_SECTION_IN_DOCSTR_CODE} disabled", + ), + pytest.param( + f''' +def foo(arg_1): + """Docstring. + + Args: + arg_1: + + Parameters: + arg_1: + """ # noqa: {MULT_ARGS_SECTIONS_IN_DOCSTR_CODE} +''', + "source.py", + "", + id=f"{MULT_ARGS_SECTIONS_IN_DOCSTR_CODE} disabled", + ), + pytest.param( + f''' +def foo(arg_1): # noqa: {ARG_NOT_IN_DOCSTR_CODE} + """Docstring. + + Args: + Arguments. + """ +''', + "source.py", + "", + id=f"{ARG_NOT_IN_DOCSTR_CODE} disabled", + ), + pytest.param( + f''' +def foo( + arg_1, + arg2, # noqa: {ARG_NOT_IN_DOCSTR_CODE} +): + """Docstring. + + Args: + arg_1: + """ +''', + "source.py", + "", + id=f"{ARG_NOT_IN_DOCSTR_CODE} disabled specific arg", + ), + pytest.param( + f''' +def foo(arg_1): + """Docstring. + + Args: + arg_1: + arg_2: + """ # noqa: {ARG_IN_DOCSTR_CODE} +''', + "source.py", + "", + id=f"{ARG_IN_DOCSTR_CODE} disabled", + ), + pytest.param( + f''' +def foo(): + """Docstring.""" + return 1 # noqa: {RETURNS_SECTION_NOT_IN_DOCSTR_CODE} +''', + "source.py", + "", + id=f"{RETURNS_SECTION_NOT_IN_DOCSTR_CODE} disabled", + ), + pytest.param( + f''' +def foo(): + """Docstring. + + Returns: + A value. + """ # noqa: {RETURNS_SECTION_IN_DOCSTR_CODE} +''', + "source.py", + "", + id=f"{RETURNS_SECTION_IN_DOCSTR_CODE} disabled", + ), + pytest.param( + f''' +def foo(): + """Docstring. + + Returns: + A value. + + Return: + A value. + """ # noqa: {MULT_RETURNS_SECTIONS_IN_DOCSTR_CODE} + return 1 +''', + "source.py", + "", + id=f"{MULT_RETURNS_SECTIONS_IN_DOCSTR_CODE} disabled", + ), + pytest.param( + f''' +def foo(): + """Docstring.""" + yield 1 # noqa: {YIELDS_SECTION_NOT_IN_DOCSTR_CODE} +''', + "source.py", + "", + id=f"{YIELDS_SECTION_NOT_IN_DOCSTR_CODE} disabled", + ), + pytest.param( + f''' +def foo(): + """Docstring. + + Yields: + A value. + """ # noqa: {YIELDS_SECTION_IN_DOCSTR_CODE} +''', + "source.py", + "", + id=f"{YIELDS_SECTION_IN_DOCSTR_CODE} disabled", + ), + pytest.param( + f''' +def foo(): + """Docstring. + + Yields: + A value. + + Yield: + A value. + """ # noqa: {MULT_YIELDS_SECTIONS_IN_DOCSTR_CODE} + yield 1 +''', + "source.py", + "", + id=f"{MULT_YIELDS_SECTIONS_IN_DOCSTR_CODE} disabled", + ), + pytest.param( + f''' +class Exc1Error(Exception): + """Docstring.""" + + +def foo(): + """Docstring.""" # noqa: {RAISES_SECTION_NOT_IN_DOCSTR_CODE} + raise Exc1Error +''', + "source.py", + "", + id=f"{RAISES_SECTION_NOT_IN_DOCSTR_CODE} disabled", + ), + pytest.param( + f''' +def foo(): + """Docstring. + + Raises: + Exc1:. + """ # noqa: {RAISES_SECTION_IN_DOCSTR_CODE} +''', + "source.py", + "", + id=f"{RAISES_SECTION_IN_DOCSTR_CODE} disabled", + ), + pytest.param( + f''' +class Exc1Error(Exception): + """Docstring.""" + + +def foo(): + """Docstring. + + Raises: + Exc1Error: + + Raise: + Exc1Error: + """ # noqa: {MULT_RAISES_SECTIONS_IN_DOCSTR_CODE} + raise Exc1Error +''', + "source.py", + "", + id=f"{MULT_RAISES_SECTIONS_IN_DOCSTR_CODE} disabled", + ), + pytest.param( + f''' +class Exc1Error(Exception): + """Docstring.""" + + +class Exc2Error(Exception): + """Docstring.""" + + +def foo(): + """Docstring. + + Raises: + Exc1Error:. + """ + raise Exc1Error + raise Exc2Error # noqa: {EXC_NOT_IN_DOCSTR_CODE} +''', + "source.py", + "", + id=f"{EXC_NOT_IN_DOCSTR_CODE} disabled", + ), + pytest.param( + f''' +class Exc1Error(Exception): + """Docstring.""" + + +def foo(): + """Docstring. + + Raises: + Exc1Error: + Exc2Error: + """ # noqa: {EXC_IN_DOCSTR_CODE} + raise Exc1Error +''', + "source.py", + "", + id=f"{EXC_IN_DOCSTR_CODE} disabled", + ), + pytest.param( + f''' +def foo(): + """Docstring. + + Raises: + """ # noqa: {RE_RAISE_NO_EXC_IN_DOCSTR_CODE},D414 + raise +''', + "source.py", + "", + id=f"{RE_RAISE_NO_EXC_IN_DOCSTR_CODE} disabled", + ), + pytest.param( + f''' +class Class1: + """Docstring.""" # noqa: {ATTRS_SECTION_NOT_IN_DOCSTR_CODE} + + attr_1 = "value 1" +''', + "source.py", + "", + id=f"{ATTRS_SECTION_NOT_IN_DOCSTR_CODE} disabled", + ), + pytest.param( + f''' +class Class1: + """Docstring. + + Attrs: + Attributes. + """ # noqa: {ATTRS_SECTION_IN_DOCSTR_CODE} +''', + "source.py", + "", + id=f"{ATTRS_SECTION_IN_DOCSTR_CODE} disabled", + ), + pytest.param( + f''' +class Class1: + """Docstring. + + Attrs: + attr_1: + + Attributes: + attr_1: + """ # noqa: {MULT_ATTRS_SECTIONS_IN_DOCSTR_CODE} + + attr_1 = "value 1" +''', + "source.py", + "", + id=f"{MULT_ATTRS_SECTIONS_IN_DOCSTR_CODE} disabled", + ), + pytest.param( + f''' +class Class1: + """Docstring. + + Attrs: + Attributes. + """ + + attr_1 = "value 1" # noqa: {ATTR_NOT_IN_DOCSTR_CODE} +''', + "source.py", + "", + id=f"{ATTR_NOT_IN_DOCSTR_CODE} disabled", + ), + pytest.param( + f''' +class Class1: + """Docstring. + + Attrs: + attr_1: + """ + + attr_1 = "value 1" + attr_2 = "value 2" # noqa: {ATTR_NOT_IN_DOCSTR_CODE} +''', + "source.py", + "", + id=f"{ATTR_NOT_IN_DOCSTR_CODE} disabled specific arg", + ), + pytest.param( + f''' +class Class1: + """Docstring. + + Attrs: + attr_1: + attr_2: + """ # noqa: {ATTR_IN_DOCSTR_CODE} + + attr_1 = "value 1" +''', + "source.py", + "", + id=f"{ATTR_IN_DOCSTR_CODE} disabled", + ), + ], +) +def test_pass(code: str, filename: str, extra_args: str, tmp_path: Path): + """ + given: file with Python code that passes the linting + when: flake8 is run against the code + then: the process exits with zero code and empty stdout + """ + code_file = create_code_file(code, filename, tmp_path) + (config_file := tmp_path / ".flake8").touch() + + with subprocess.Popen( + ( + f"{sys.executable} -m flake8 {code_file} {extra_args} --ignore D205,D400,D103 " + f"--config {config_file}" + ), + stdout=subprocess.PIPE, + shell=True, + ) as proc: + stdout = proc.communicate()[0].decode(encoding="utf-8") + + assert not stdout, stdout + assert not proc.returncode + + +def test_self(): + """ + given: working linter + when: flake8 is run against the source and tests of the linter + then: the process exits with zero code and empty stdout + """ + with subprocess.Popen( + f"{sys.executable} -m flake8 flake8_docstrings_complete/ tests/ --ignore D205,D400,D103", + stdout=subprocess.PIPE, + shell=True, + ) as proc: + stdout = proc.communicate()[0].decode(encoding="utf-8") + + assert not stdout, stdout + assert not proc.returncode diff --git a/tests/test_docstring.py b/tests/test_docstring.py new file mode 100644 index 0000000..637e4a5 --- /dev/null +++ b/tests/test_docstring.py @@ -0,0 +1,501 @@ +"""Tests for docstring module.""" + +from __future__ import annotations + +import pytest + +from flake8_docstrings_complete import docstring + +# Need access to protected functions for testing +# pylint: disable=protected-access + + +@pytest.mark.parametrize( + "lines, expected_sections", + [ + pytest.param((), (), id="empty"), + pytest.param(("",), (), id="single not a section"), + pytest.param(("not a section",), (), id="single not a section no colon"), + pytest.param(("not a section:",), (), id="single not a section not after first word"), + pytest.param(("name_1:",), (docstring._Section("name_1", ()),), id="single section"), + pytest.param( + (" name_1:",), + (docstring._Section("name_1", ()),), + id="single section leading whitespace single space", + ), + pytest.param( + ("\tname_1:",), + (docstring._Section("name_1", ()),), + id="single section leading whitespace single tab", + ), + pytest.param( + (" name_1:",), + (docstring._Section("name_1", ()),), + id="single section leading whitespace multiple", + ), + pytest.param( + ("name_1: ",), + (docstring._Section("name_1", ()),), + id="single section trailing whitespace", + ), + pytest.param( + ("name_1: description",), + (docstring._Section("name_1", ()),), + id="single section trailing characters", + ), + pytest.param( + ("name_1:", "description 1"), + (docstring._Section("name_1", ()),), + id="single section multiple lines", + ), + pytest.param( + ("name_1:", "sub_name_1:"), + (docstring._Section("name_1", ("sub_name_1",)),), + id="single section single sub-section", + ), + pytest.param( + ("name_1:", "sub_name_1 (text 1):"), + (docstring._Section("name_1", ("sub_name_1",)),), + id="single section single sub-section brackets", + ), + pytest.param( + ("name_1:", " sub_name_1:"), + (docstring._Section("name_1", ("sub_name_1",)),), + id="single section single sub-section leading whitespace", + ), + pytest.param( + ("name_1:", "sub_name_1: "), + (docstring._Section("name_1", ("sub_name_1",)),), + id="single section single sub-section trailing whitespace", + ), + pytest.param( + ("name_1:", "sub_name_1: description 1"), + (docstring._Section("name_1", ("sub_name_1",)),), + id="single section single sub-section trailing characters", + ), + pytest.param( + ("name_1:", "sub_name_1:", "description 1"), + (docstring._Section("name_1", ("sub_name_1",)),), + id="single section single sub-section other sub first", + ), + pytest.param( + ("name_1:", "description 1", "sub_name_1:"), + (docstring._Section("name_1", ("sub_name_1",)),), + id="single section single sub-section other sub last", + ), + pytest.param( + ("name_1:", "sub_name_1:", "sub_name_2:"), + (docstring._Section("name_1", ("sub_name_1", "sub_name_2")),), + id="single section multiple sub-sections", + ), + pytest.param( + ("name_1:", "sub_name_1:", "sub_name_2:", "sub_name_3:"), + (docstring._Section("name_1", ("sub_name_1", "sub_name_2", "sub_name_3")),), + id="single section many sub-sections", + ), + pytest.param( + ("name_1:", "description 1", "description 2"), + (docstring._Section("name_1", ()),), + id="single section many lines", + ), + pytest.param( + ("name_1:", ""), (docstring._Section("name_1", ()),), id="single section separator" + ), + pytest.param( + ("name_1:", "", "name_2:"), + (docstring._Section("name_1", ()), docstring._Section("name_2", ())), + id="multiple sections separator empty", + ), + pytest.param( + ("name_1:", " ", "name_2:"), + (docstring._Section("name_1", ()), docstring._Section("name_2", ())), + id="multiple sections separator single space", + ), + pytest.param( + ("name_1:", "\t", "name_2:"), + (docstring._Section("name_1", ()), docstring._Section("name_2", ())), + id="multiple sections separator single tab", + ), + pytest.param( + ("name_1:", " ", "name_2:"), + (docstring._Section("name_1", ()), docstring._Section("name_2", ())), + id="multiple sections separator multiple whitespace", + ), + pytest.param( + ("name_1:", "sub_name_1:", "", "name_2:"), + (docstring._Section("name_1", ("sub_name_1",)), docstring._Section("name_2", ())), + id="multiple sections first has sub-section", + ), + pytest.param( + ("name_1:", "", "name_2:", "sub_name_1:"), + (docstring._Section("name_1", ()), docstring._Section("name_2", ("sub_name_1",))), + id="multiple sections last has sub-section", + ), + pytest.param( + ("name_1:", "", "name_2:", "", "name_3:"), + ( + docstring._Section("name_1", ()), + docstring._Section("name_2", ()), + docstring._Section("name_3", ()), + ), + id="many sections", + ), + ], +) +def test__get_sections( + lines: tuple[()] | tuple[str, ...], + expected_sections: tuple[()] | tuple[docstring._Section, ...], +): + """ + given: lines of a docstring + when: _get_sections is called with the lines + then: the expected sections are returned. + """ + assert isinstance(lines, tuple) + assert isinstance(expected_sections, tuple) + + returned_sections = tuple(docstring._get_sections(lines=lines)) + + assert returned_sections == expected_sections + + +@pytest.mark.parametrize( + "value, expected_docstring", + [ + pytest.param("", docstring.Docstring(), id="empty"), + pytest.param("short description", docstring.Docstring(), id="short description"), + pytest.param( + """short description + +long description""", + docstring.Docstring(), + id="short and long description", + ), + pytest.param( + """short description + +Args: + """, + docstring.Docstring(args=(), args_sections=("Args",)), + id="args empty", + ), + pytest.param( + """short description + +Args: + arg_1: + """, + docstring.Docstring(args=("arg_1",), args_sections=("Args",)), + id="args single", + ), + pytest.param( + """short description + +Args: + arg_1: + arg_2: + """, + docstring.Docstring(args=("arg_1", "arg_2"), args_sections=("Args",)), + id="args multiple", + ), + pytest.param( + """short description + +args: + arg_1: + """, + docstring.Docstring(args=("arg_1",), args_sections=("args",)), + id="args lower case", + ), + pytest.param( + """short description + +Arguments: + arg_1: + """, + docstring.Docstring(args=("arg_1",), args_sections=("Arguments",)), + id="args alternate Arguments", + ), + pytest.param( + """short description + +Parameters: + arg_1: + """, + docstring.Docstring(args=("arg_1",), args_sections=("Parameters",)), + id="args alternate Parameters", + ), + pytest.param( + """short description + +Args: + arg_1: + +Parameters: + arg_2: + """, + docstring.Docstring(args=("arg_1",), args_sections=("Args", "Parameters")), + id="args multiple sections", + ), + pytest.param( + """short description + +Attrs: + """, + docstring.Docstring(attrs=(), attrs_sections=("Attrs",)), + id="attrs empty", + ), + pytest.param( + """short description + +Attrs: + +Attributes: + """, + docstring.Docstring(attrs=(), attrs_sections=("Attrs", "Attributes")), + id="multiple attrs empty", + ), + pytest.param( + """short description + +Attrs: + +Attrs: + """, + docstring.Docstring(attrs=(), attrs_sections=("Attrs", "Attrs")), + id="multiple attrs alternate empty", + ), + pytest.param( + """short description + +Attrs: + attr_1: + """, + docstring.Docstring(attrs=("attr_1",), attrs_sections=("Attrs",)), + id="attrs single", + ), + pytest.param( + """short description + +Attrs: + attr_1: + attr_2: + """, + docstring.Docstring(attrs=("attr_1", "attr_2"), attrs_sections=("Attrs",)), + id="attrs multiple", + ), + pytest.param( + """short description + +attrs: + attr_1: + """, + docstring.Docstring(attrs=("attr_1",), attrs_sections=("attrs",)), + id="attrs lower case", + ), + pytest.param( + """short description + +Attributes: + attr_1: + """, + docstring.Docstring(attrs=("attr_1",), attrs_sections=("Attributes",)), + id="attrs alternate Attributes", + ), + pytest.param( + """short description + +Returns: + """, + docstring.Docstring(returns=True, returns_sections=("Returns",)), + id="returns empty", + ), + pytest.param( + """short description + +Returns: + The return value. + """, + docstring.Docstring(returns=True, returns_sections=("Returns",)), + id="returns single line", + ), + pytest.param( + """short description + +Return: + """, + docstring.Docstring(returns=True, returns_sections=("Return",)), + id="returns alternate", + ), + pytest.param( + """short description + +Returns: + +Returns: + """, + docstring.Docstring(returns=True, returns_sections=("Returns", "Returns")), + id="multiple returns", + ), + pytest.param( + """short description + +Returns: + +Return: + """, + docstring.Docstring(returns=True, returns_sections=("Returns", "Return")), + id="multiple returns alternate", + ), + pytest.param( + """short description + +Yields: + """, + docstring.Docstring(yields=True, yields_sections=("Yields",)), + id="yields empty", + ), + pytest.param( + """short description + +Yields: + The return value. + """, + docstring.Docstring(yields=True, yields_sections=("Yields",)), + id="yields single line", + ), + pytest.param( + """short description + +Yield: + """, + docstring.Docstring(yields=True, yields_sections=("Yield",)), + id="yields alternate", + ), + pytest.param( + """short description + +Yields: + +Yields: + """, + docstring.Docstring(yields=True, yields_sections=("Yields", "Yields")), + id="multiple yields", + ), + pytest.param( + """short description + +Yields: + +Yield: + """, + docstring.Docstring(yields=True, yields_sections=("Yields", "Yield")), + id="multiple yields alternate", + ), + pytest.param( + """short description + +Raises: + """, + docstring.Docstring(raises=(), raises_sections=("Raises",)), + id="raises empty", + ), + pytest.param( + """short description + +Raises: + +Raises: + """, + docstring.Docstring(raises=(), raises_sections=("Raises", "Raises")), + id="raises empty multiple", + ), + pytest.param( + """short description + +Raises: + +Raise: + """, + docstring.Docstring(raises=(), raises_sections=("Raises", "Raise")), + id="raises empty multiple alternate", + ), + pytest.param( + """short description + +Raises: + """, + docstring.Docstring(raises=(), raises_sections=("Raises",)), + id="raises empty multiple", + ), + pytest.param( + """short description + +Raises: + exc_1: + """, + docstring.Docstring(raises=("exc_1",), raises_sections=("Raises",)), + id="raises single", + ), + pytest.param( + """short description + +Raises: + exc_1: + exc_2: + """, + docstring.Docstring(raises=("exc_1", "exc_2"), raises_sections=("Raises",)), + id="raises multiple", + ), + pytest.param( + """short description + +raises: + exc_1: + """, + docstring.Docstring(raises=("exc_1",), raises_sections=("raises",)), + id="raises lower case", + ), + pytest.param( + """short description + +Attrs: + attr_1: + +Args: + arg_1: + +Returns: + The return value. + +Yields: + The yield value. + +Raises: + exc_1: + """, + docstring.Docstring( + args=("arg_1",), + args_sections=("Args",), + attrs=("attr_1",), + attrs_sections=("Attrs",), + returns=True, + returns_sections=("Returns",), + yields=True, + yields_sections=("Yields",), + raises=("exc_1",), + raises_sections=("Raises",), + ), + id="all defined", + ), + ], +) +def test_parse(value: str, expected_docstring: docstring.Docstring): + """ + given: docstring value + when: parse is called with the docstring + then: the expected docstring information is returned. + """ + returned_docstring = docstring.parse(value=value) + + assert returned_docstring == expected_docstring diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e0310a0 --- /dev/null +++ b/tests/unit/__init__.py @@ -0,0 +1 @@ +"""Unit tests.""" diff --git a/tests/unit/result.py b/tests/unit/result.py new file mode 100644 index 0000000..3b80a39 --- /dev/null +++ b/tests/unit/result.py @@ -0,0 +1,22 @@ +"""Get the linting result.""" + +from __future__ import annotations + +import ast + +from flake8_docstrings_complete import Plugin + + +def get(code: str, filename: str = "source.py") -> tuple[str, ...]: + """Generate linting results. + + Args: + code: The code to check. + filename: The name of the file the code is in. + + Returns: + The linting result. + """ + tree = ast.parse(code) + plugin = Plugin(tree, filename) + return tuple(f"{line}:{col} {msg}" for line, col, msg, _ in plugin.run()) diff --git a/tests/unit/test___init__.py b/tests/unit/test___init__.py new file mode 100644 index 0000000..2827b0c --- /dev/null +++ b/tests/unit/test___init__.py @@ -0,0 +1,829 @@ +"""Unit tests for plugin except for args rules.""" + +from __future__ import annotations + +import pytest + +from flake8_docstrings_complete import ( + DOCSTR_MISSING_MSG, + MULT_RETURNS_SECTIONS_IN_DOCSTR_MSG, + MULT_YIELDS_SECTIONS_IN_DOCSTR_MSG, + RETURNS_SECTION_IN_DOCSTR_MSG, + RETURNS_SECTION_NOT_IN_DOCSTR_MSG, + YIELDS_SECTION_IN_DOCSTR_MSG, + YIELDS_SECTION_NOT_IN_DOCSTR_MSG, +) + +from . import result + + +@pytest.mark.parametrize( + "code, expected_result", + [ + pytest.param("", (), id="trivial"), + pytest.param( + """ +def function_1(): + return +""", + (f"2:0 {DOCSTR_MISSING_MSG}",), + id="function docstring missing return", + ), + pytest.param( + """ +def function_1(): + return + +def function_2(): + return +""", + (f"2:0 {DOCSTR_MISSING_MSG}", f"5:0 {DOCSTR_MISSING_MSG}"), + id="multiple functions docstring missing return", + ), + pytest.param( + """ +def function_1(): + pass +""", + (f"2:0 {DOCSTR_MISSING_MSG}",), + id="function docstring missing expression not constant", + ), + pytest.param( + """ +def function_1(): + 1 +""", + (f"2:0 {DOCSTR_MISSING_MSG}",), + id="function docstring missing expression constnant not string", + ), + pytest.param( + ''' +def function_1(): + """Docstring. + + Returns: + """ +''', + (f"3:4 {RETURNS_SECTION_IN_DOCSTR_MSG}",), + id="function no return returns in docstring", + ), + pytest.param( + ''' +class Class1: + """Docstring.""" + def function_1(): + """Docstring. + + Returns: + """ +''', + (f"5:8 {RETURNS_SECTION_IN_DOCSTR_MSG}",), + id="method no return returns in docstring", + ), + pytest.param( + ''' +def function_1(): + """Docstring. + + Returns: + """ + return +''', + (f"3:4 {RETURNS_SECTION_IN_DOCSTR_MSG}",), + id="function return no value returns in docstring", + ), + pytest.param( + ''' +def function_1(): + """Docstring. + + Returns: + + Returns: + """ + return 1 +''', + (f"3:4 {MULT_RETURNS_SECTIONS_IN_DOCSTR_MSG % 'Returns,Returns'}",), + id="function return multiple returns in docstring", + ), + pytest.param( + ''' +class Class1: + """Docstring.""" + def function_1(): + """Docstring. + + Returns: + + Returns: + """ + return 1 +''', + (f"5:8 {MULT_RETURNS_SECTIONS_IN_DOCSTR_MSG % 'Returns,Returns'}",), + id="method return multiple returns in docstring", + ), + pytest.param( + ''' +def function_1(): + """Docstring.""" + return 1 +''', + (f"4:4 {RETURNS_SECTION_NOT_IN_DOCSTR_MSG}",), + id="function single return value returns not in docstring", + ), + pytest.param( + ''' +def function_1(): + """Docstring.""" + return 0 +''', + (f"4:4 {RETURNS_SECTION_NOT_IN_DOCSTR_MSG}",), + id="function single falsely return value returns not in docstring", + ), + pytest.param( + ''' +def function_1(): + """Docstring.""" + return None +''', + (f"4:4 {RETURNS_SECTION_NOT_IN_DOCSTR_MSG}",), + id="function single None return value returns not in docstring", + ), + pytest.param( + ''' +async def function_1(): + """Docstring.""" + return 1 +''', + (f"4:4 {RETURNS_SECTION_NOT_IN_DOCSTR_MSG}",), + id="async function single return value returns not in docstring", + ), + pytest.param( + ''' +class FooClass: + """Docstring.""" + def function_1(self): + """Docstring.""" + return 1 +''', + (f"6:8 {RETURNS_SECTION_NOT_IN_DOCSTR_MSG}",), + id="method single return value returns not in docstring", + ), + pytest.param( + ''' +def function_1(): + """Docstring.""" + if True: + return 1 +''', + (f"5:8 {RETURNS_SECTION_NOT_IN_DOCSTR_MSG}",), + id="function single nested return value returns not in docstring", + ), + pytest.param( + ''' +def function_1(): + """Docstring.""" + return 11 + return 12 +''', + ( + f"4:4 {RETURNS_SECTION_NOT_IN_DOCSTR_MSG}", + f"5:4 {RETURNS_SECTION_NOT_IN_DOCSTR_MSG}", + ), + id="function multiple return value returns not in docstring", + ), + pytest.param( + ''' +def function_1(): + """Docstring.""" + return 11 + return +''', + (f"4:4 {RETURNS_SECTION_NOT_IN_DOCSTR_MSG}",), + id="function multiple return first value returns not in docstring", + ), + pytest.param( + ''' +def function_1(): + """Docstring.""" + return + return 12 +''', + (f"5:4 {RETURNS_SECTION_NOT_IN_DOCSTR_MSG}",), + id="function multiple return second value returns not in docstring", + ), + pytest.param( + ''' +def function_1(): + """Docstring.""" + yield 1 +''', + (f"4:4 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}",), + id="function single yield value yields not in docstring", + ), + pytest.param( + ''' +def function_1(): + """Docstring.""" + yield from tuple() +''', + (f"4:4 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}",), + id="function single yield from value yields not in docstring", + ), + pytest.param( + ''' +def function_1(): + """Docstring.""" + yield 0 +''', + (f"4:4 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}",), + id="function single falsely yield value yields not in docstring", + ), + pytest.param( + ''' +def function_1(): + """Docstring.""" + yield None +''', + (f"4:4 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}",), + id="function single None yield value yields not in docstring", + ), + pytest.param( + ''' +async def function_1(): + """Docstring.""" + yield 1 +''', + (f"4:4 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}",), + id="async function single yield value yields not in docstring", + ), + pytest.param( + ''' +class FooClass: + """Docstring.""" + def function_1(self): + """Docstring.""" + yield 1 +''', + (f"6:8 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}",), + id="method single yield value yields not in docstring", + ), + pytest.param( + ''' +def function_1(): + """Docstring.""" + if True: + yield 1 +''', + (f"5:8 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}",), + id="function single nested yield value yields not in docstring", + ), + pytest.param( + ''' +def function_1(): + """Docstring.""" + yield 11 + yield 12 +''', + ( + f"4:4 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}", + f"5:4 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}", + ), + id="function multiple yield value yields not in docstring", + ), + pytest.param( + ''' +def function_1(): + """Docstring.""" + yield 11 + yield +''', + (f"4:4 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}",), + id="function multiple yield first value yields not in docstring", + ), + pytest.param( + ''' +def function_1(): + """Docstring.""" + yield + yield 12 +''', + (f"5:4 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}",), + id="function multiple yield second value yields not in docstring", + ), + pytest.param( + ''' +def function_1(): + """Docstring.""" + yield from tuple() + yield from list() +''', + ( + f"4:4 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}", + f"5:4 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}", + ), + id="function multiple yield from value yields not in docstring", + ), + pytest.param( + ''' +def function_1(): + """Docstring.""" + yield from tuple() + yield +''', + (f"4:4 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}",), + id="function multiple yield from first value yields not in docstring", + ), + pytest.param( + ''' +def function_1(): + """Docstring.""" + yield + yield from list() +''', + (f"5:4 {YIELDS_SECTION_NOT_IN_DOCSTR_MSG}",), + id="function multiple yield from second value yields not in docstring", + ), + pytest.param( + ''' +def function_1(): + """Docstring. + + Yields: + """ +''', + (f"3:4 {YIELDS_SECTION_IN_DOCSTR_MSG}",), + id="function no yield yields in docstring", + ), + pytest.param( + ''' +class Class1: + """Docstring.""" + def function_1(): + """Docstring. + + Yields: + """ +''', + (f"5:8 {YIELDS_SECTION_IN_DOCSTR_MSG}",), + id="method no yield yields in docstring", + ), + pytest.param( + ''' +def function_1(): + """Docstring. + + Yields: + """ + yield +''', + (f"3:4 {YIELDS_SECTION_IN_DOCSTR_MSG}",), + id="function yield no value yields in docstring", + ), + pytest.param( + ''' +def function_1(): + """Docstring. + + Yields: + + Yields: + """ + yield 1 +''', + (f"3:4 {MULT_YIELDS_SECTIONS_IN_DOCSTR_MSG % 'Yields,Yields'}",), + id="function yield multiple yields in docstring", + ), + pytest.param( + ''' +def function_1(): + """Docstring. + + Yields: + + Yields: + """ + yield from tuple() +''', + (f"3:4 {MULT_YIELDS_SECTIONS_IN_DOCSTR_MSG % 'Yields,Yields'}",), + id="function yield from multiple yields in docstring", + ), + pytest.param( + ''' +class Class1: + """Docstring.""" + def function_1(): + """Docstring. + + Yields: + + Yields: + """ + yield 1 +''', + (f"5:8 {MULT_YIELDS_SECTIONS_IN_DOCSTR_MSG % 'Yields,Yields'}",), + id="method yield multiple yields in docstring", + ), + pytest.param( + ''' +async def function_1(): + """Docstring 1.""" +''', + (), + id="function docstring", + ), + pytest.param( + ''' +async def function_1(): + """Docstring 1.""" + +async def function_2(): + """Docstring 2.""" +''', + (), + id="multiple functions docstring", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1.""" +''', + (), + id="async function docstring", + ), + pytest.param( + ''' +class Class1: + """Docstring.""" + def function_1(self): + return +''', + (f"4:4 {DOCSTR_MISSING_MSG}",), + id="method docstring missing return", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1.""" + return +''', + (), + id="function return no value docstring no returns section", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Returns: + """ + return 1 +''', + (), + id="function return value docstring returns section", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1.""" + def function_2(): + """Docstring 2. + + Returns: + """ + return 1 +''', + (), + id="function return value in nested function docstring no returns section", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1.""" + async def function_2(): + """Docstring 2. + + Returns: + """ + return 1 +''', + (), + id="function return value in nested async function docstring no returns section", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1.""" + class Class1: + """Docstring.""" + return 1 +''', + (), + id="function return value in class docstring no returns section", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Returns: + """ + return 1 + return 2 +''', + (), + id="function multiple return values docstring returns section", + ), + pytest.param( + ''' +class Class1: + """Docstring.""" + def function_1(self): + """Docstring 1. + + Returns: + """ + return 1 +''', + (), + id="method return value docstring returns section", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1.""" + yield +''', + (), + id="function yield no value docstring no yields section", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Yields: + """ + yield 1 +''', + (), + id="function yield value docstring yields section", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Yields: + """ + yield from tuple() +''', + (), + id="function yield from docstring yields section", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1.""" + def function_2(): + """Docstring 2. + + Yields: + """ + yield 1 +''', + (), + id="function yield value in nested function docstring no yields section", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1.""" + async def function_2(): + """Docstring 2. + + Yields: + """ + yield 1 +''', + (), + id="function yield value in nested async function docstring no yields section", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1.""" + class Class1: + """Docstring.""" + yield 1 +''', + (), + id="function yield value in class docstring no yields section", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Yields: + """ + yield 1 + yield 2 +''', + (), + id="function multiple yield values docstring yields section", + ), + pytest.param( + ''' +class Class1: + """Docstring.""" + def function_1(self): + """Docstring 1. + + Yields: + """ + yield 1 +''', + (), + id="method yield value docstring yields section", + ), + ], +) +def test_plugin(code: str, expected_result: tuple[str, ...]): + """ + given: code + when: linting is run on the code + then: the expected result is returned + """ + assert result.get(code) == expected_result + + +@pytest.mark.parametrize( + "code, filename, expected_result", + [ + pytest.param( + """ +def test_(): + pass +""", + "source.py", + (f"2:0 {DOCSTR_MISSING_MSG}",), + id="not test file", + ), + pytest.param( + """ +def foo(): + pass +""", + "test_.py", + (f"2:0 {DOCSTR_MISSING_MSG}",), + id="test file not test function", + ), + pytest.param( + """ +def test_(): + pass +""", + "test_.py", + (), + id="test file test function", + ), + pytest.param( + """ +def test_(): + pass +""", + "tests/test_.py", + (), + id="test file test function in directory", + ), + pytest.param( + """ +def foo(): + pass +""", + "conftest.py", + (f"2:0 {DOCSTR_MISSING_MSG}",), + id="normal file not fixture function", + ), + pytest.param( + """ +@fixture +def foo(): + pass +""", + "source.py", + (f"3:0 {DOCSTR_MISSING_MSG}",), + id="source file fixture function", + ), + pytest.param( + """ +@fixture +def foo(): + pass +""", + "conftest.py", + (), + id="fixture file fixture function", + ), + pytest.param( + """ +@fixture +def foo(): + pass +""", + "test_.py", + (), + id="test file fixture function", + ), + pytest.param( + """ +@FIXTURE +def foo(): + pass +""", + "conftest.py", + (), + id="fixture file fixture function capitalised", + ), + pytest.param( + """ +@fixture +@decorator +def foo(): + pass +""", + "conftest.py", + (), + id="fixture file fixture function multiple decorators first", + ), + pytest.param( + """ +@decorator +@fixture +def foo(): + pass +""", + "conftest.py", + (), + id="fixture file fixture function multiple decorators second", + ), + pytest.param( + """ +@pytest.fixture +def foo(): + pass +""", + "conftest.py", + (), + id="fixture file fixture function prefix", + ), + pytest.param( + """ +@pytest.fixture(scope="module") +def foo(): + pass +""", + "conftest.py", + (), + id="fixture file fixture function prefix call", + ), + pytest.param( + """ +@additional.pytest.fixture +def foo(): + pass +""", + "conftest.py", + (), + id="fixture file fixture function nested prefix", + ), + pytest.param( + """ +@fixture(scope="module") +def foo(): + pass +""", + "conftest.py", + (), + id="fixture file fixture function arguments", + ), + pytest.param( + """ +@fixture +def foo(): + pass +""", + "tests/conftest.py", + (), + id="fixture file fixture function in directory", + ), + ], +) +def test_plugin_filename(code: str, filename: str, expected_result: tuple[str, ...]): + """ + given: code and filename + when: linting is run on the code + then: the expected result is returned + """ + assert result.get(code, filename) == expected_result diff --git a/tests/unit/test___init__args.py b/tests/unit/test___init__args.py new file mode 100644 index 0000000..485169e --- /dev/null +++ b/tests/unit/test___init__args.py @@ -0,0 +1,629 @@ +"""Unit tests for args checks in the plugin.""" + +from __future__ import annotations + +import pytest + +from flake8_docstrings_complete.args import ( + ARG_IN_DOCSTR_MSG, + ARG_NOT_IN_DOCSTR_MSG, + ARGS_SECTION_IN_DOCSTR_MSG, + ARGS_SECTION_NOT_IN_DOCSTR_MSG, + MULT_ARGS_SECTIONS_IN_DOCSTR_MSG, +) + +from . import result + + +@pytest.mark.parametrize( + "code, expected_result", + [ + pytest.param( + ''' +def function_1(arg_1): + """Docstring 1.""" +''', + (f"3:4 {ARGS_SECTION_NOT_IN_DOCSTR_MSG}",), + id="function has single arg docstring no args section", + ), + pytest.param( + ''' +def function_1(arg_1): + """Docstring 1.""" + +def function_2(arg_2): + """Docstring 2.""" +''', + ( + f"3:4 {ARGS_SECTION_NOT_IN_DOCSTR_MSG}", + f"6:4 {ARGS_SECTION_NOT_IN_DOCSTR_MSG}", + ), + id="multiple function has single arg docstring no args section", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Args: + """ +''', + (f"3:4 {ARGS_SECTION_IN_DOCSTR_MSG}",), + id="function has no args docstring args section", + ), + pytest.param( + ''' +def function_1(arg_1): + """Docstring 1. + + Args: + arg_1: + + Args: + arg_1: + """ +''', + (f"3:4 {MULT_ARGS_SECTIONS_IN_DOCSTR_MSG % 'Args,Args'}",), + id="function has single args docstring multiple args sections same name", + ), + pytest.param( + ''' +def function_1(arg_1): + """Docstring 1. + + Args: + arg_1: + + Arguments: + arg_1: + """ +''', + (f"3:4 {MULT_ARGS_SECTIONS_IN_DOCSTR_MSG % 'Args,Arguments'}",), + id="function has single args docstring multiple args sections different name", + ), + pytest.param( + ''' +def function_1(arg_1): + """Docstring 1. + + Args: + """ +''', + (f"2:15 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}",), + id="function has single arg docstring no arg", + ), + pytest.param( + ''' +def function_1(_arg_1): + """Docstring 1. + + Args: + """ +''', + (f"3:4 {ARGS_SECTION_IN_DOCSTR_MSG}",), + id="function has single unused arg docstring args", + ), + pytest.param( + ''' +async def function_1(arg_1): + """Docstring 1. + + Args: + """ +''', + (f"2:21 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}",), + id="async function has single arg docstring no arg", + ), + pytest.param( + ''' +def function_1(arg_1, /): + """Docstring 1. + + Args: + """ +''', + (f"2:15 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}",), + id="function has single positional only arg docstring no arg", + ), + pytest.param( + ''' +def function_1(arg_1, arg_2, /): + """Docstring 1. + + Args: + """ +''', + (f"2:15 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}", f"2:22 {ARG_NOT_IN_DOCSTR_MSG % 'arg_2'}"), + id="function has multiple positional only arg docstring no arg", + ), + pytest.param( + ''' +class Class1: + """Docstring.""" + def function_1(self, arg_1, /): + """Docstring 1. + + Args: + """ +''', + (f"4:25 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}",), + id="method has single positional only arg docstring no arg", + ), + pytest.param( + ''' +def function_1(*, arg_1): + """Docstring 1. + + Args: + """ +''', + (f"2:18 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}",), + id="function has single keyword only arg docstring no arg", + ), + pytest.param( + ''' +def function_1(*, arg_1, arg_2): + """Docstring 1. + + Args: + """ +''', + (f"2:18 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}", f"2:25 {ARG_NOT_IN_DOCSTR_MSG % 'arg_2'}"), + id="function has multiple keyword only arg docstring no arg", + ), + pytest.param( + ''' +class Class1: + """Docstring.""" + def function_1(self, *, arg_1): + """Docstring 1. + + Args: + """ +''', + (f"4:28 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}",), + id="method has single keyword only arg docstring no arg", + ), + pytest.param( + ''' +def function_1(*args): + """Docstring 1. + + Args: + """ +''', + (f"2:16 {ARG_NOT_IN_DOCSTR_MSG % 'args'}",), + id="function has *args docstring no arg", + ), + pytest.param( + ''' +def function_1(**kwargs): + """Docstring 1. + + Args: + """ +''', + (f"2:17 {ARG_NOT_IN_DOCSTR_MSG % 'kwargs'}",), + id="function has **kwargs docstring no arg", + ), + pytest.param( + ''' +def function_1(*args, **kwargs): + """Docstring 1. + + Args: + """ +''', + ( + f"2:16 {ARG_NOT_IN_DOCSTR_MSG % 'args'}", + f"2:24 {ARG_NOT_IN_DOCSTR_MSG % 'kwargs'}", + ), + id="function has *args and **kwargs docstring no arg", + ), + pytest.param( + ''' +def function_1(*args, arg_1): + """Docstring 1. + + Args: + """ +''', + (f"2:22 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}", f"2:16 {ARG_NOT_IN_DOCSTR_MSG % 'args'}"), + id="function has *args docstring no arg", + ), + pytest.param( + ''' +def function_1(arg_1, arg_2): + """Docstring 1. + + Args: + """ + ''', + (f"2:15 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}", f"2:22 {ARG_NOT_IN_DOCSTR_MSG % 'arg_2'}"), + id="function multiple args docstring no arg", + ), + pytest.param( + ''' +def function_1(_arg_1, arg_2): + """Docstring 1. + + Args: + """ + ''', + (f"2:23 {ARG_NOT_IN_DOCSTR_MSG % 'arg_2'}",), + id="function multiple args first unused docstring no arg", + ), + pytest.param( + ''' +def function_1(arg_1, _arg_2): + """Docstring 1. + + Args: + """ + ''', + (f"2:15 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}",), + id="function multiple args second unused docstring no arg", + ), + pytest.param( + ''' +def function_1(arg_1, arg_2): + """Docstring 1. + + Args: + arg_1: + """ +''', + (f"2:22 {ARG_NOT_IN_DOCSTR_MSG % 'arg_2'}",), + id="function multiple args docstring single arg first", + ), + pytest.param( + ''' +def function_1(arg_1, arg_2): + """Docstring 1. + + Args: + arg_2: + """ +''', + (f"2:15 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}",), + id="function multiple args docstring single arg second", + ), + pytest.param( + ''' +def function_1(arg_1): + """Docstring 1. + + Args: + arg_2: + """ +''', + ( + f"2:15 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}", + f"3:4 {ARG_IN_DOCSTR_MSG % 'arg_2'}", + ), + id="function has single arg docstring arg different", + ), + pytest.param( + ''' +def function_1(arg_1): + """Docstring 1. + + Args: + arg_2: + arg_3: + """ + ''', + ( + f"2:15 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}", + f"3:4 {ARG_IN_DOCSTR_MSG % 'arg_2'}", + f"3:4 {ARG_IN_DOCSTR_MSG % 'arg_3'}", + ), + id="function single arg docstring multiple args different", + ), + pytest.param( + ''' +def function_1(arg_1, arg_2): + """Docstring 1. + + Args: + arg_3: + arg_4: + """ + ''', + ( + f"2:15 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}", + f"2:22 {ARG_NOT_IN_DOCSTR_MSG % 'arg_2'}", + f"3:4 {ARG_IN_DOCSTR_MSG % 'arg_3'}", + f"3:4 {ARG_IN_DOCSTR_MSG % 'arg_4'}", + ), + id="function multiple arg docstring multiple args different", + ), + pytest.param( + ''' +def function_1(arg_1, arg_2): + """Docstring 1. + + Args: + arg_3: + arg_2: + """ + ''', + (f"2:15 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}", f"3:4 {ARG_IN_DOCSTR_MSG % 'arg_3'}"), + id="function multiple arg docstring multiple args first different", + ), + pytest.param( + ''' +def function_1(arg_1, arg_2): + """Docstring 1. + + Args: + arg_1: + arg_3: + """ + ''', + (f"2:22 {ARG_NOT_IN_DOCSTR_MSG % 'arg_2'}", f"3:4 {ARG_IN_DOCSTR_MSG % 'arg_3'}"), + id="function multiple arg docstring multiple args last different", + ), + pytest.param( + ''' +def function_1(arg_1): + """Docstring 1. + + Args: + arg_1: + """ +''', + (), + id="function single arg docstring single arg", + ), + pytest.param( + ''' +def function_1(_arg_1): + """Docstring 1. + + Args: + _arg_1: + """ +''', + (), + id="function single unused arg docstring single arg", + ), + pytest.param( + ''' +def function_1(_arg_1): + """Docstring 1.""" +''', + (), + id="function single unused arg docstring no args", + ), + pytest.param( + ''' +def function_1(*_args): + """Docstring 1. + + Args: + _args: + """ +''', + (), + id="function single unused *args docstring single arg", + ), + pytest.param( + ''' +def function_1(*_args): + """Docstring 1.""" +''', + (), + id="function single unused *args docstring no args", + ), + pytest.param( + ''' +def function_1(**_kwargs): + """Docstring 1. + + Args: + _kwargs: + """ +''', + (), + id="function single unused **kwargs docstring single arg", + ), + pytest.param( + ''' +def function_1(**_kwargs): + """Docstring 1.""" +''', + (), + id="function single unused **kwargs docstring no args", + ), + pytest.param( + ''' +def function_1(*args): + """Docstring 1. + + Args: + args: + """ +''', + (), + id="function single arg docstring *args", + ), + pytest.param( + ''' +def function_1(**kwargs): + """Docstring 1. + + Args: + kwargs: + """ +''', + (), + id="function single arg docstring **kwargs", + ), + pytest.param( + ''' +def function_1(*args, **kwargs): + """Docstring 1. + + Args: + args: + kwargs: + """ +''', + (), + id="function single arg docstring *args and **kwargs", + ), + pytest.param( + ''' +def function_1(arg_1, arg_2): + """Docstring 1. + + Args: + arg_1: + arg_2: + """ +''', + (), + id="function multiple arg docstring multiple arg", + ), + pytest.param( + ''' +def function_1(_arg_1, arg_2): + """Docstring 1. + + Args: + arg_2: + """ +''', + (), + id="function multiple arg first unused docstring single arg", + ), + pytest.param( + ''' +def function_1(arg_1, _arg_2): + """Docstring 1. + + Args: + arg_1: + """ +''', + (), + id="function multiple arg first unused docstring single arg", + ), + pytest.param( + ''' +class Class1: + """Docstring.""" + def function_1(self, arg_1): + """Docstring 1.""" +''', + (f"5:8 {ARGS_SECTION_NOT_IN_DOCSTR_MSG}",), + id="method has single arg docstring no args section", + ), + pytest.param( + ''' +class Class1: + """Docstring.""" + def function_1(self): + """Docstring 1. + + Args: + """ +''', + (f"5:8 {ARGS_SECTION_IN_DOCSTR_MSG}",), + id="method has no args docstring args section", + ), + pytest.param( + ''' +class Class1: + """Docstring.""" + def function_1(self, arg_1): + """Docstring 1. + + Args: + """ +''', + (f"4:25 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}",), + id="method has single arg docstring no arg", + ), + pytest.param( + ''' +class Class1: + """Docstring.""" + @staticmethod + def function_1(arg_1): + """Docstring 1. + + Args: + """ +''', + (f"5:19 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}",), + id="method has single arg docstring no arg staticmethod", + ), + pytest.param( + ''' +class Class1: + """Docstring.""" + @classmethod + def function_1(cls, arg_1): + """Docstring 1. + + Args: + """ +''', + (f"5:24 {ARG_NOT_IN_DOCSTR_MSG % 'arg_1'}",), + id="method has single arg docstring no arg classmethod", + ), + pytest.param( + ''' +class Class1: + """Docstring.""" + def function_1(self, arg_1): + """Docstring 1. + + Args: + arg_1: + """ +''', + (), + id="method single arg docstring single arg", + ), + pytest.param( + ''' +class Class1: + """Docstring.""" + @staticmethod + def function_1(arg_1): + """Docstring 1. + + Args: + arg_1: + """ +''', + (), + id="method single arg docstring single arg staticmethod", + ), + pytest.param( + ''' +class Class1: + """Docstring.""" + @classmethod + def function_1(cls, arg_1): + """Docstring 1. + + Args: + arg_1: + """ +''', + (), + id="method single arg docstring single arg classmethod", + ), + ], +) +def test_plugin(code: str, expected_result: tuple[str, ...]): + """ + given: code + when: linting is run on the code + then: the expected result is returned + """ + assert result.get(code) == expected_result diff --git a/tests/unit/test___init__attrs.py b/tests/unit/test___init__attrs.py new file mode 100644 index 0000000..29b8320 --- /dev/null +++ b/tests/unit/test___init__attrs.py @@ -0,0 +1,791 @@ +"""Unit tests for attrs checks in the plugin.""" + +from __future__ import annotations + +import pytest + +from flake8_docstrings_complete import DOCSTR_MISSING_MSG +from flake8_docstrings_complete.attrs import ( + ATTR_IN_DOCSTR_MSG, + ATTR_NOT_IN_DOCSTR_MSG, + ATTRS_SECTION_IN_DOCSTR_MSG, + ATTRS_SECTION_NOT_IN_DOCSTR_MSG, + MULT_ATTRS_SECTIONS_IN_DOCSTR_MSG, +) + +from . import result + + +@pytest.mark.parametrize( + "code, expected_result", + [ + pytest.param( + """ +class Class1: + pass +""", + (f"2:0 {DOCSTR_MISSING_MSG}",), + id="class no docstring", + ), + pytest.param( + ''' +class Class1: + """Docstring 1.""" + attr_1 = "value 1" +''', + (f"3:4 {ATTRS_SECTION_NOT_IN_DOCSTR_MSG}",), + id="class has single class attr docstring no attrs section", + ), + pytest.param( + ''' +class Class1: + """Docstring 1.""" + attr_1 = "value 1" + +class Class2: + """Docstring 2.""" + attr_2 = "value 2" +''', + ( + f"3:4 {ATTRS_SECTION_NOT_IN_DOCSTR_MSG}", + f"7:4 {ATTRS_SECTION_NOT_IN_DOCSTR_MSG}", + ), + id="multiple class has single class attr docstring no attrs section", + ), + pytest.param( + ''' +class Class1: + """Docstring 1.""" + def __init__(self): + """Docstring 2.""" + self.attr_1 = "value 1" +''', + (f"3:4 {ATTRS_SECTION_NOT_IN_DOCSTR_MSG}",), + id="class has single __init__ attr docstring no attrs section", + ), + pytest.param( + ''' +class Class1: + """Docstring 1.""" + def method_1(self): + """Docstring 2.""" + self.attr_1 = "value 1" +''', + (f"3:4 {ATTRS_SECTION_NOT_IN_DOCSTR_MSG}",), + id="class has single method attr docstring no attrs section", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + attr_1: + """ +''', + (f"3:4 {ATTRS_SECTION_IN_DOCSTR_MSG}",), + id="class has no attrs docstring attrs section", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + attr_1: + + Attrs: + attr_1: + """ + attr_1 = "value 1" +''', + (f"3:4 {MULT_ATTRS_SECTIONS_IN_DOCSTR_MSG % 'Attrs,Attrs'}",), + id="class has single attrs docstring multiple attrs sections same name", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + attr_1: + + Attributes: + attr_1: + """ + attr_1 = "value 1" +''', + (f"3:4 {MULT_ATTRS_SECTIONS_IN_DOCSTR_MSG % 'Attrs,Attributes'}",), + id="class has single attrs docstring multiple attrs sections alternate name", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + """ + attr_1 = "value 1" +''', + (f"7:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), + id="class has single attr docstring no attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + """ + attr_1 = attr_2 = "value 1" +''', + ( + f"7:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}", + f"7:13 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_2'}", + ), + id="class has multiple assign attr docstring no attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + """ + attr_1.nested_attr_1 = "value 1" +''', + (f"7:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), + id="class has single nested attr docstring no attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + """ + attr_1.nested_attr_1.nested_attr_2 = "value 1" +''', + (f"7:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), + id="class has single double nested attr docstring no attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + """ + _attr_1 = "value 1" +''', + (f"3:4 {ATTRS_SECTION_IN_DOCSTR_MSG}",), + id="class has single unused attr docstring attrs", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + """ + def __init__(self): + """Docstring 2.""" + attr_1 = "value 1" +''', + (f"9:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), + id="class has single attr after init docstring no attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + """ + def __init__(self): + """Docstring 2.""" + self.attr_1 = "value 1" +''', + (f"9:8 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), + id="class has single attr in init docstring no attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + """ + def method_1(self): + """Docstring 2.""" + self.attr_1 = "value 1" +''', + (f"9:8 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), + id="class has single attr in method docstring no attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + """ + def method_1(self): + """Docstring 2.""" + self.attr_1: str = "value 1" +''', + (f"9:8 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), + id="class has single attr typed in method docstring no attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + """ + def method_1(self): + """Docstring 2.""" + self.attr_1 += "value 1" +''', + (f"9:8 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), + id="class has single attr augmented in method docstring no attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + """ + def method_1(self): + """Docstring 2.""" + self.attr_1 = self.attr_2 = "value 1" +''', + ( + f"9:8 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}", + f"9:22 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_2'}", + ), + id="class has multiple attr in method docstring no attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + """ + def method_1(self): + """Docstring 2.""" + self.attr_1.nested_attr_1 = "value 1" +''', + (f"9:8 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), + id="class has single attr nested in method docstring no attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + """ + def method_1(self): + """Docstring 2.""" + self.attr_1.nested_attr_1.nested_attr_2 = "value 1" +''', + (f"9:8 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), + id="class has single attr deep nested in method docstring no attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + """ + def method_1(self): + """Docstring 2.""" + self.attr_1 = "value 1" + def method_2(self): + """Docstring 3.""" + self.attr_2 = "value 2" +''', + ( + f"9:8 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}", + f"12:8 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_2'}", + ), + id="class has multiple attr in multiple method docstring no attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + """ + async def method_1(self): + """Docstring 2.""" + self.attr_1 = "value 1" +''', + (f"9:8 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), + id="class has single attr in async method docstring no attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + """ + @classmethod + def method_1(cls): + """Docstring 2.""" + cls.attr_1 = "value 1" +''', + (f"10:8 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), + id="class has single attr in classmethod method docstring no attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + """ + attr_1 = "value 1" + attr_2 = "value 2" + ''', + ( + f"7:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}", + f"8:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_2'}", + ), + id="class multiple attrs docstring no attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + """ + _attr_1 = "value 1" + attr_2 = "value 2" + ''', + (f"8:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_2'}",), + id="class multiple attrs first private docstring no attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + """ + attr_1 = "value 1" + _attr_2 = "value 2" + ''', + (f"7:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), + id="class multiple attrs second private docstring no attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + attr_1: + """ + attr_1 = "value 1" + attr_2 = "value 2" +''', + (f"9:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_2'}",), + id="class multiple attrs docstring single attr first", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + attr_2: + """ + attr_1 = "value 1" + attr_2 = "value 2" +''', + (f"8:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), + id="class multiple attrs docstring single attr second", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + attr_2: + """ + attr_1 = "value 1" +''', + ( + f"8:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}", + f"3:4 {ATTR_IN_DOCSTR_MSG % 'attr_2'}", + ), + id="class has single attr docstring attr different", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + """ + attr_1: str = "value 1" +''', + (f"7:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), + id="class has single typed attr docstring no attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + """ + attr_1 += "value 1" +''', + (f"7:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",), + id="class has single augmented attr docstring no attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + attr_2: + attr_3: + """ + attr_1 = "value 1" + ''', + ( + f"9:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}", + f"3:4 {ATTR_IN_DOCSTR_MSG % 'attr_2'}", + f"3:4 {ATTR_IN_DOCSTR_MSG % 'attr_3'}", + ), + id="class single attr docstring multiple attrs different", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + attr_3: + attr_4: + """ + attr_1 = "value 1" + attr_2 = "value 2" + ''', + ( + f"9:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}", + f"10:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_2'}", + f"3:4 {ATTR_IN_DOCSTR_MSG % 'attr_3'}", + f"3:4 {ATTR_IN_DOCSTR_MSG % 'attr_4'}", + ), + id="class multiple attr docstring multiple attrs different", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + attr_2: + attr_3: + """ + attr_1 = "value 1" + attr_2 = "value 2" + ''', + (f"9:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}", f"3:4 {ATTR_IN_DOCSTR_MSG % 'attr_3'}"), + id="class multiple attr docstring multiple attrs first different", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + attr_1: + attr_3: + """ + attr_1 = "value 1" + attr_2 = "value 2" + ''', + (f"10:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_2'}", f"3:4 {ATTR_IN_DOCSTR_MSG % 'attr_3'}"), + id="class multiple attr docstring multiple attrs second different", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + attr_1: + """ + attr_1 = "value 1" +''', + (), + id="class single attr docstring single attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + attr_1: + """ + attr_1: str = "value 1" +''', + (), + id="class single attr typed docstring single attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + attr_1: + """ + attr_1 += "value 1" +''', + (), + id="class single attr augmented docstring single attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + attr_1: + """ + def __init__(self): + """Docstring 2.""" + self.attr_1 = "value 1" +''', + (), + id="class single attr init docstring single attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + attr_1: + """ + def method_1(self): + """Docstring 2.""" + self.attr_1 = "value 1" +''', + (), + id="class single attr method docstring single attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + attr_1: + attr_2: + """ + def method_1(self): + """Docstring 2.""" + self.attr_1 = "value 1" + def method_2(self): + """Docstring 3.""" + self.attr_2 = "value 2" +''', + (), + id="class multiple attr method docstring single attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + attr_1: + """ + @classmethod + def method_1(cls): + """Docstring 2.""" + cls.attr_1 = "value 1" +''', + (), + id="class single attr classmethod docstring single attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + _attr_1: + """ + _attr_1 = "value 1" +''', + (), + id="class single private attr docstring single attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1.""" + _attr_1 = "value 1" +''', + (), + id="class single private attr docstring single attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1.""" + def __init__(self): + """Docstring 2.""" + var_1 = "value 1" +''', + (), + id="class single var init docstring single attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1.""" + def method_1(self): + """Docstring 2.""" + var_1 = "value 1" +''', + (), + id="class single var method docstring single attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1.""" + @classmethod + def method_1(cls): + """Docstring 2.""" + var_1 = "value 1" +''', + (), + id="class single var classmethod docstring single attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + attr_1: + attr_2: + """ + attr_1 = "value 1" + attr_2 = "value 2" +''', + (), + id="class multiple attr docstring multiple attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + attr_2: + """ + _attr_1 = "value 1" + attr_2 = "value 2" +''', + (), + id="class multiple attr first private docstring single attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1. + + Attrs: + attr_1: + """ + attr_1 = "value 1" + _attr_2 = "value 2" +''', + (), + id="class multiple attr second private docstring single attr", + ), + pytest.param( + ''' +class Class1: + """Docstring 1.""" + class Class2: + """Docstring 2. + + Attrs: + attr_1: + """ + attr_1 = "value 1" +''', + (), + id="nested class single attr docstring no attrs", + ), + pytest.param( + ''' +class Class1: + """Docstring 1.""" + def method_1(self): + """Docstring 2.""" + def nested_funciont_1(self): + """Docstring 3.""" + self.attr_1 = "value 1" +''', + (), + id="class single attr method nested method docstring no attrs", + ), + pytest.param( + ''' +class Class1: + """Docstring 1.""" + def method_1(self): + """Docstring 2.""" + async def nested_funciont_1(self): + """Docstring 3.""" + self.attr_1 = "value 1" +''', + (), + id="class single attr method nested async method docstring no attrs", + ), + pytest.param( + ''' +class Class1: + """Docstring 1.""" + def method_1(self): + """Docstring 2.""" + def nested_funciont_1(cls): + """Docstring 3.""" + cls.attr_1 = "value 1" +''', + (), + id="class single attr method nested classmethod docstring no attrs", + ), + ], +) +def test_plugin(code: str, expected_result: tuple[str, ...]): + """ + given: code + when: linting is run on the code + then: the expected result is returned + """ + assert result.get(code) == expected_result diff --git a/tests/unit/test___init__raises.py b/tests/unit/test___init__raises.py new file mode 100644 index 0000000..95d33c6 --- /dev/null +++ b/tests/unit/test___init__raises.py @@ -0,0 +1,643 @@ +"""Unit tests for raises checks in the plugin.""" + +from __future__ import annotations + +import pytest + +from flake8_docstrings_complete.raises import ( + EXC_IN_DOCSTR_MSG, + EXC_NOT_IN_DOCSTR_MSG, + MULT_RAISES_SECTIONS_IN_DOCSTR_MSG, + RAISES_SECTION_IN_DOCSTR_MSG, + RAISES_SECTION_NOT_IN_DOCSTR_MSG, + RE_RAISE_NO_EXC_IN_DOCSTR_MSG, +) + +from . import result + + +@pytest.mark.parametrize( + "code, expected_result", + [ + pytest.param( + ''' +def function_1(): + """Docstring 1.""" + raise Exc1 +''', + (f"3:4 {RAISES_SECTION_NOT_IN_DOCSTR_MSG}",), + id="function raises single exc docstring no raises section", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1.""" + raise Exc1 + +def function_2(): + """Docstring 2.""" + raise Exc2 +''', + ( + f"3:4 {RAISES_SECTION_NOT_IN_DOCSTR_MSG}", + f"7:4 {RAISES_SECTION_NOT_IN_DOCSTR_MSG}", + ), + id="multiple function raises single exc docstring no raises section", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Raises: + """ +''', + (f"3:4 {RAISES_SECTION_IN_DOCSTR_MSG}",), + id="function raises no exc docstring raises section", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Raises: + Exc1: + + Raises: + Exc1: + """ + raise Exc1 +''', + (f"3:4 {MULT_RAISES_SECTIONS_IN_DOCSTR_MSG % 'Raises,Raises'}",), + id="function raises single excs docstring multiple raises sections same name", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Raises: + Exc1: + + Raise: + Exc1: + """ + raise Exc1 +''', + (f"3:4 {MULT_RAISES_SECTIONS_IN_DOCSTR_MSG % 'Raises,Raise'}",), + id="function raises single excs docstring multiple raises sections different name", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Raises: + """ + raise Exc1 +''', + (f"7:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}",), + id="function raises single exc docstring no exc", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Raises: + """ + raise Exc1 + raise +''', + (f"7:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}",), + id="function raises single exc and single no exc docstring no exc", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Raises: + """ + raise Exc1() +''', + (f"7:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}",), + id="function raises single exc call docstring no exc", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Raises: + """ + raise module.Exc1 +''', + (f"7:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}",), + id="function raises single nested exc docstring no exc", + ), + pytest.param( + ''' +async def function_1(): + """Docstring 1. + + Raises: + """ + raise Exc1 +''', + (f"7:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}",), + id="async function raises single exc docstring no exc", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Raises: + """ + raise Exc1 + raise Exc2 + ''', + (f"7:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}", f"8:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc2'}"), + id="function multiple excs docstring no exc", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Raises: + """ + def function_2(): + """Docstring 2. + + Raises: + Exc1: + """ + raise Exc1 + raise Exc2 + ''', + (f"14:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc2'}",), + id="function multiple excs first nested function", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Raises: + """ + async def function_2(): + """Docstring 2. + + Raises: + Exc1: + """ + raise Exc1 + raise Exc2 + ''', + (f"14:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc2'}",), + id="function multiple excs first nested async function", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Raises: + """ + class Class1: + """Docstring 2.""" + raise Exc1 + raise Exc2 + ''', + (f"10:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc2'}",), + id="function multiple excs first nested class", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Raises: + """ + raise Exc1 + def function_2(): + """Docstring 2. + + Raises: + Exc2: + """ + raise Exc2 + ''', + (f"7:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}",), + id="function multiple excs second nested function", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Raises: + Exc1: + """ + raise Exc1 + raise Exc2 +''', + (f"9:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc2'}",), + id="function multiple excs docstring single exc first", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Raises: + Exc2: + """ + raise Exc1 + raise Exc2 +''', + (f"8:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}",), + id="function multiple excs docstring single exc second", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Raises: + Exc2: + """ + raise Exc1 +''', + ( + f"8:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}", + f"3:4 {EXC_IN_DOCSTR_MSG % 'Exc2'}", + ), + id="function raises single exc docstring exc different", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Raises: + Exc2: + Exc3: + """ + raise Exc1 + ''', + ( + f"9:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}", + f"3:4 {EXC_IN_DOCSTR_MSG % 'Exc2'}", + f"3:4 {EXC_IN_DOCSTR_MSG % 'Exc3'}", + ), + id="function single exc docstring multiple exc different", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Raises: + Exc3: + Exc4: + """ + raise Exc1 + raise Exc2 + ''', + ( + f"9:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}", + f"10:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc2'}", + f"3:4 {EXC_IN_DOCSTR_MSG % 'Exc3'}", + f"3:4 {EXC_IN_DOCSTR_MSG % 'Exc4'}", + ), + id="function multiple exc docstring multiple exc different", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Raises: + Exc3: + Exc2: + """ + raise Exc1 + raise Exc2 + ''', + (f"9:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}", f"3:4 {EXC_IN_DOCSTR_MSG % 'Exc3'}"), + id="function multiple exc docstring multiple exc first different", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Raises: + Exc1: + Exc3: + """ + raise Exc1 + raise Exc2 + ''', + (f"10:10 {EXC_NOT_IN_DOCSTR_MSG % 'Exc2'}", f"3:4 {EXC_IN_DOCSTR_MSG % 'Exc3'}"), + id="function multiple exc docstring multiple exc last different", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1.""" + raise +''', + ( + f"3:4 {RAISES_SECTION_NOT_IN_DOCSTR_MSG}", + f"3:4 {RE_RAISE_NO_EXC_IN_DOCSTR_MSG}", + ), + id="function single raise no exc docstring no raises exc", + ), + pytest.param( + ''' +class Class1: + """Docstring.""" + def function_1(self): + """Docstring 1.""" + raise +''', + ( + f"5:8 {RAISES_SECTION_NOT_IN_DOCSTR_MSG}", + f"5:8 {RE_RAISE_NO_EXC_IN_DOCSTR_MSG}", + ), + id="method raise no exc docstring no raises", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Raises: + """ + raise +''', + (f"3:4 {RE_RAISE_NO_EXC_IN_DOCSTR_MSG}",), + id="function raise no exc docstring raises empty", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Raises: + Exc1: + """ + raise +''', + (), + id="function single raise no exc docstring raises exc", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Raises: + Exc1: + """ + raise Exc1 +''', + (), + id="function single raise no exc docstring raises", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1.""" + def function_2(): + """Docstring 2. + + Raises: + Exc1: + """ + raise Exc1 +''', + (), + id="function single nested function exc docstring no raises", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1.""" + async def function_2(): + """Docstring 2. + + Raises: + Exc1: + """ + raise Exc1 +''', + (), + id="function single nested async function exc docstring no raises", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1.""" + class Class1: + """Docstring 2.""" + raise Exc1 +''', + (), + id="function single nested class exc docstring no raises", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Raises: + Exc1: + """ + raise Exc1() +''', + (), + id="function single exc call docstring single exc", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Raises: + Exc1: + """ + raise (lambda: True)() +''', + (), + id="function single exc lambda docstring single exc", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Raises: + Exc1: + """ + raise module.Exc1 +''', + (), + id="function single exc attribute docstring single exc", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Raises: + Exc1: + """ + raise module.Exc1() +''', + (), + id="function single exc attribute call docstring single exc", + ), + pytest.param( + ''' +def function_1(): + """Docstring 1. + + Raises: + Exc1: + Exc2: + """ + raise Exc1 + raise Exc2 +''', + (), + id="function multiple exc docstring multiple exc", + ), + pytest.param( + ''' +class Class1: + """Docstring.""" + def function_1(self): + """Docstring 1.""" + raise Exc1 +''', + (f"5:8 {RAISES_SECTION_NOT_IN_DOCSTR_MSG}",), + id="method raises single exc docstring no raises section", + ), + pytest.param( + ''' +class Class1: + """Docstring.""" + def function_1(self): + """Docstring 1. + + Raises: + """ +''', + (f"5:8 {RAISES_SECTION_IN_DOCSTR_MSG}",), + id="method raises no exc docstring raises section", + ), + pytest.param( + ''' +class Class1: + """Docstring.""" + def function_1(self): + """Docstring 1. + + Raises: + """ + raise Exc1 +''', + (f"9:14 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}",), + id="method raises single exc docstring no exc", + ), + pytest.param( + ''' +class Class1: + """Docstring.""" + @staticmethod + def function_1(): + """Docstring 1. + + Raises: + """ + raise Exc1 +''', + (f"10:14 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}",), + id="method raises single exc docstring no exc staticmethod", + ), + pytest.param( + ''' +class Class1: + """Docstring.""" + @classmethod + def function_1(cls): + """Docstring 1. + + Raises: + """ + raise Exc1 +''', + (f"10:14 {EXC_NOT_IN_DOCSTR_MSG % 'Exc1'}",), + id="method raises single exc docstring no exc classmethod", + ), + pytest.param( + ''' +class Class1: + """Docstring.""" + def function_1(self): + """Docstring 1. + + Raises: + Exc1: + """ + raise Exc1 +''', + (), + id="method single exc docstring single exc", + ), + pytest.param( + ''' +class Class1: + """Docstring.""" + @staticmethod + def function_1(): + """Docstring 1. + + Raises: + Exc1: + """ + raise Exc1 +''', + (), + id="method single exc docstring single exc staticmethod", + ), + pytest.param( + ''' +class Class1: + """Docstring.""" + @classmethod + def function_1(cls): + """Docstring 1. + + Raises: + Exc1: + """ + raise Exc1 +''', + (), + id="method single exc docstring single exc classmethod", + ), + ], +) +def test_plugin(code: str, expected_result: tuple[str, ...]): + """ + given: code + when: linting is run on the code + then: the expected result is returned + """ + assert result.get(code) == expected_result diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..df14c16 --- /dev/null +++ b/tox.ini @@ -0,0 +1,60 @@ +[tox] +skipsdist=True +envlist = lint, test, coverage-report + +[vars] +src_path = {toxinidir}/flake8_docstrings_complete +tst_path = {toxinidir}/tests/ +all_path = {[vars]src_path} {[vars]tst_path} + +[testenv] +allowlist_externals=python,poetry +setenv = + PYTHONPATH = {toxinidir}:{toxinidir}/lib:{[vars]src_path} + PYTHONBREAKPOINT=ipdb.set_trace + PY_COLORS=1 +passenv = + PYTHONPATH + +[testenv:fmt] +description = Apply coding style standards to code +deps = + poetry +commands = + poetry install + poetry run isort {[vars]all_path} + poetry run black {[vars]all_path} + +[testenv:lint] +description = Check code against coding style standards +deps = + poetry +commands = + poetry install + poetry run pydocstyle {[vars]src_path} + poetry run codespell {toxinidir} --skip {toxinidir}/.git --skip {toxinidir}/.tox \ + --skip {toxinidir}/.venv --skip {toxinidir}/.mypy_cache + poetry run flake8 {[vars]all_path} + poetry run isort --check-only --diff {[vars]all_path} + poetry run black --check --diff {[vars]all_path} + poetry run mypy {[vars]all_path} + poetry run pylint {[vars]all_path} + poetry run pydocstyle {[vars]src_path} + +[testenv:test] +description = Run tests +deps = + poetry +commands = + poetry install + poetry run coverage run \ + -m pytest -v --tb native -s {posargs} + poetry run coverage report + +[testenv:coverage-report] +description = Create test coverage report +deps = + poetry +commands = + poetry install + poetry run coverage report