Giter Site home page Giter Site logo

jsh9 / pydoclint Goto Github PK

View Code? Open in Web Editor NEW
111.0 4.0 10.0 462 KB

A Python docstring linter that checks arguments, returns, yields, and raises sections

Home Page: https://pypi.org/project/pydoclint/

License: MIT License

Python 100.00%
docstring docstring-checker flake8 flake8-extension flake8-extensions flake8-plugin flake8-plugins linter documantation documentation-tool python-documentation

pydoclint's Introduction

pydoclint

Pydoclint is a Python docstring linter to check whether a docstring's sections (arguments, returns, raises, ...) match the function signature or function implementation.

It runs really fast. In fact, it can be thousands of times faster than darglint (or its maintained fork darglint2).

Here is a comparison of linting time on some famous Python projects:

pydoclint darglint
numpy 2.0 sec 49 min 9 sec (1,475x slower)
scikit-learn 2.4 sec 3 hr 5 min 33 sec (4,639x slower)

Additionally, pydoclint can detect some quite a few style violations that darglint cannot.

Currently, pydoclint supports three docstring styles: numpy, Google, and Sphinx.

Another note: this linter and pydocstyle serves complementary purposes. It is recommended that you use both together.

The full documentation of pydoclint (including this README) can be found here: https://jsh9.github.io/pydoclint

The corresponding Github repository of pydoclint is: https://github.com/jsh9/pydoclint


Table of Contents

1. Installation

To install only the native pydoclint tooling, run this command:

pip install pydoclint

To use pydoclint as a flake8 plugin, please run this command, which will also install flake8 to the current Python environment:

pip install pydoclint[flake8]

Note that pydoclint currently only supports Python 3.8 and above. (Python 3.7 support may be added if there are interests and requests.)

2. Usage

2.1. As a native command line tool

pydoclint <FILE_OR_FOLDER>

Replace <FILE_OR_FOLDER> with the file/folder names you want, such as ..

2.2. As a flake8 plugin

Once you install pydoclint you will have also installed flake8. Then you can run:

flake8 --select=DOC <FILE_OR_FOLDER>

If you don't include --select=DOC in your command, flake8 will also run other built-in flake8 linters on your code.

2.3. As a pre-commit hook

pydoclint is configured for pre-commit and can be set up as a hook with the following .pre-commit-config.yaml configuration:

- repo: https://github.com/jsh9/pydoclint
  rev: <latest_tag>
  hooks:
    - id: pydoclint
      args: [--style=google, --check-return-types=False]

You will need to install pre-commit and run pre-commit install.

2.4. Native vs flake8

Should I use pydoclint as a native command line tool or a flake8 plugin? Here's comparison:

Pros Cons
Native tool Slightly faster; supports "baseline" [*] No inline or project-wide omission support right now [**]
flake8 plugin Supports inline or project-wide omission Slightly slower because other flake8 plugins are run together

*) "Baseline" allows you to log the current violation state of your existing project, making adoption of pydoclint much easier.

**) This feature may be added in the near future

2.5. How to configure pydoclint

Please read this page: How to configure pydoclint

2.6. How to ignore certain violations in flake8 mode

Please read this page: How to ignore certain violations

3. Style violation codes

pydoclint currently has 6 categories of style violation codes:

  • DOC0xx: Docstring parsing issues
  • DOC1xx: Violations about input arguments
  • DOC2xx: Violations about return argument(s)
  • DOC3xx: Violations about class docstring and class constructor
  • DOC4xx: Violations about "yield" statements
  • DOC5xx: Violations about "raise" statements

For detailed explanations of each violation code, please read this page: pydoclint style violation codes.

4. Notes for users

If you'd like to use pydoclint for your project, it is recommended that you read these additional notes here.

Specifically, there is a section in the additional notes on how to easily adopt pydoclint for existing legacy projects.

5. Notes for developers

If you'd like to contribute to the code base of pydoclint, thank you!

This guide can hopefully help you get familiar with the code base faster.

pydoclint's People

Contributors

egorrko avatar jamesbraza avatar jenste avatar jsh9 avatar llucax avatar m472 avatar real-yfprojects avatar s-weigand avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

pydoclint's Issues

Unused function arguments

Hi ๐Ÿ‘‹ I wonder if it's possible to somehow cleverly handle the case where there are unused function arguments that don't require a docstring. These situations may occur when you are forced to maintain a specific function signature, e.g. when writing attrs-validators.

For example:

def foo(_: int, b: float):
    """Bar.

    Args:
        b: a number
    """

In this case, pydoclint will complain with

debugging.py:1:1: DOC101 Function `foo`: Docstring contains fewer arguments than in function signature. 
debugging.py:1:1: DOC103 Function `foo`: Docstring arguments are different from function arguments. (Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). Arguments in the function signature but not in the docstring: [_: int, b: float]. Arguments in the docstring but not in the function signature: [b: ].

Recursion Error

On a couple of repositories I am getting a recursion error from pydoclint in flake8.

I am running pydoclint with the following settings in my pyproject.toml tool.flake8 section:

skip-checking-short-docstrings = false
style = 'sphinx'

If I remove both of those settings (but not one...) then I do not get a recursion error.
If I remove skip-checking-short-docstrings and change style to google then I do not get a recursion error.

This is only happening on a couple of projects. I have not yet worked out what makes this happen or not happen on other projects with the same settings.
I will try and work out what exactly is causing this tomorrow.

The trace of the error I get is:

pre-commit run -a flake8
flake8...................................................................Failed
- hook id: flake8
- exit code: 1

multiprocessing.pool.RemoteTraceback: 
"""
Traceback (most recent call last):
  File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/multiprocessing/pool.py", line 125, in worker
    result = (True, func(*args, **kwds))
                    ^^^^^^^^^^^^^^^^^^^
  File "/home/jforrest/.cache/pre-commit/repojjx2ocym/py_env-python3.11/lib/python3.11/site-packages/flake8/checker.py", line 82, in _mp_run
    ).run_checks()
      ^^^^^^^^^^^^
  File "/home/jforrest/.cache/pre-commit/repojjx2ocym/py_env-python3.11/lib/python3.11/site-packages/flake8_noqa/noqa_filter.py", line 189, in run_checks
    result = super().run_checks(*args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jforrest/.cache/pre-commit/repojjx2ocym/py_env-python3.11/lib/python3.11/site-packages/flake8/checker.py", line 525, in run_checks
    self.run_ast_checks()
  File "/home/jforrest/.cache/pre-commit/repojjx2ocym/py_env-python3.11/lib/python3.11/site-packages/flake8/checker.py", line 427, in run_ast_checks
    for line_number, offset, text, _ in runner:
  File "/home/jforrest/.cache/pre-commit/repojjx2ocym/py_env-python3.11/lib/python3.11/site-packages/pydoclint/flake8_entry.py", line 218, in run
    v.visit(self._tree)
  File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/ast.py", line 418, in visit
    return visitor(node)
           ^^^^^^^^^^^^^
  File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/ast.py", line 426, in generic_visit
    self.visit(item)
  File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/ast.py", line 418, in visit
    return visitor(node)
           ^^^^^^^^^^^^^
  File "/home/jforrest/.cache/pre-commit/repojjx2ocym/py_env-python3.11/lib/python3.11/site-packages/pydoclint/visitor.py", line 68, in visit_ClassDef
    self.generic_visit(node)
  File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/ast.py", line 426, in generic_visit
    self.visit(item)
  File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/ast.py", line 418, in visit
    return visitor(node)
           ^^^^^^^^^^^^^
  File "/home/jforrest/.cache/pre-commit/repojjx2ocym/py_env-python3.11/lib/python3.11/site-packages/pydoclint/visitor.py", line 127, in visit_FunctionDef
    argViolations = self.checkArguments(node, parent_, doc)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jforrest/.cache/pre-commit/repojjx2ocym/py_env-python3.11/lib/python3.11/site-packages/pydoclint/visitor.py", line 315, in checkArguments
    astArgList: List[ast.arg] = collectFuncArgs(node)
                                ^^^^^^^^^^^^^^^^^^^^^
  File "/home/jforrest/.cache/pre-commit/repojjx2ocym/py_env-python3.11/lib/python3.11/site-packages/pydoclint/utils/generic.py", line 32, in collectFuncArgs
    kwarg = copy.deepcopy(node.args.kwarg)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/copy.py", line 172, in deepcopy
    y = _reconstruct(x, memo, *rv)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/copy.py", line 271, in _reconstruct
    state = deepcopy(state, memo)
            ^^^^^^^^^^^^^^^^^^^^^
  File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/copy.py", line 146, in deepcopy
    y = copier(x, memo)
        ^^^^^^^^^^^^^^^
  File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/copy.py", line 231, in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
                             ^^^^^^^^^^^^^^^^^^^^^
  File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/copy.py", line 172, in deepcopy
    y = _reconstruct(x, memo, *rv)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/copy.py", line 271, in _reconstruct
    state = deepcopy(state, memo)
            ^^^^^^^^^^^^^^^^^^^^^


-------------------------------------- This continues for a very long time ---------------------------------------

 File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/copy.py", line 172, in deepcopy
    y = _reconstruct(x, memo, *rv)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/copy.py", line 271, in _reconstruct
    state = deepcopy(state, memo)
            ^^^^^^^^^^^^^^^^^^^^^
  File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/copy.py", line 146, in deepcopy
    y = copier(x, memo)
        ^^^^^^^^^^^^^^^
  File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/copy.py", line 231, in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
                             ^^^^^^^^^^^^^^^^^^^^^
  File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/copy.py", line 172, in deepcopy
    y = _reconstruct(x, memo, *rv)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/copy.py", line 271, in _reconstruct
    state = deepcopy(state, memo)
            ^^^^^^^^^^^^^^^^^^^^^
  File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/copy.py", line 146, in deepcopy
    y = copier(x, memo)
        ^^^^^^^^^^^^^^^
  File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/copy.py", line 231, in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
                             ^^^^^^^^^^^^^^^^^^^^^
  File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/copy.py", line 172, in deepcopy
    y = _reconstruct(x, memo, *rv)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/copy.py", line 271, in _reconstruct
    state = deepcopy(state, memo)
            ^^^^^^^^^^^^^^^^^^^^^
  File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/copy.py", line 146, in deepcopy
    y = copier(x, memo)
        ^^^^^^^^^^^^^^^
  File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/copy.py", line 231, in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
                             ^^^^^^^^^^^^^^^^^^^^^
  File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/copy.py", line 172, in deepcopy
    y = _reconstruct(x, memo, *rv)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/copy.py", line 271, in _reconstruct
    state = deepcopy(state, memo)
            ^^^^^^^^^^^^^^^^^^^^^
  File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/copy.py", line 137, in deepcopy
    d = id(x)
        ^^^^^
RecursionError: maximum recursion depth exceeded while calling a Python object
"""

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/jforrest/.cache/pre-commit/repojjx2ocym/py_env-python3.11/bin/flake8", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/home/jforrest/.cache/pre-commit/repojjx2ocym/py_env-python3.11/lib/python3.11/site-packages/flake8/main/cli.py", line 23, in main
    app.run(argv)
  File "/home/jforrest/.cache/pre-commit/repojjx2ocym/py_env-python3.11/lib/python3.11/site-packages/flake8/main/application.py", line 198, in run
    self._run(argv)
  File "/home/jforrest/.cache/pre-commit/repojjx2ocym/py_env-python3.11/lib/python3.11/site-packages/flake8/main/application.py", line 187, in _run
    self.run_checks()
  File "/home/jforrest/.cache/pre-commit/repojjx2ocym/py_env-python3.11/lib/python3.11/site-packages/flake8/main/application.py", line 103, in run_checks
    self.file_checker_manager.run()
  File "/home/jforrest/.cache/pre-commit/repojjx2ocym/py_env-python3.11/lib/python3.11/site-packages/flake8/checker.py", line 235, in run
    self.run_parallel()
  File "/home/jforrest/.cache/pre-commit/repojjx2ocym/py_env-python3.11/lib/python3.11/site-packages/flake8/checker.py", line 204, in run_parallel
    self.results = list(pool.imap_unordered(_mp_run, self.filenames))
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jforrest/.pyenv/versions/3.11.4/lib/python3.11/multiprocessing/pool.py", line 873, in next
    raise value
RecursionError: maximum recursion depth exceeded while calling a Python object

Optional arguments false positive

Both of these examples trigger an error DOC105: Function test: Argument names match, but type hints do not match:

def test(var1: Optional[str] = None) -> str:
    """Test function.

    Args:
        var1 (str, optional): var1


    Returns:
        str
    """
def test(var1: str | None = None) -> str:
    """Test function.

    Args:
        var1 (str, optional): var1


    Returns:
        str
    """

Changing the var1 type to str | None solves it, but this is a bit redundant and, as far as I know, inconsistent with how most other tools work. You can see for example the function module_level_function from Sphinx napoleon documentation.

Arguments wrongly listed as missing based on type

I believe I've stumbled into a bug, or at least a usability issue.

Say we have the following test.py:

"""Module docstring."""


def f(x: int, y: int, z: int) -> None:
    """
    Run f().

    Parameters
    ----------
    y
        y
    z
        z
    """
    pass

Running pydoclint with --arg-type-hints-in-docstring False results in the following:

  • DOC101: Function f: Docstring contains fewer arguments than in function signature.
  • DOC103: Function f: Docstring arguments are different from function arguments. (Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). Arguments in the function signature but not in the docstring: [x: int, y: int, z: int]. Arguments in the docstring but not in the function signature: [y: , z: ].

It seems that the latter part should just be Arguments in the function signature but not in the docstring: [x: int].

Adding x to the docstring removes all errors. Likewise, adding --arg-type-hints-in-signature False and removing the type annotations results in the expected error Arguments in the function signature but not in the docstring: [x: ].

Allow ignoring errors using code comments

Like most linters, it would be very useful being able to ignore a certain type of error for a particular function/class/module.

With documentation linting is harder to achieve, as annotating docstrings

Darglint allowed this but by annotating the docstrings, but that is not a good option, as the annotations will show up then in the help, which is unhelpful and confusing to users.

The flexibility of the scheme # noqa: <error> <argument> is very useful though. For example, to only ignore the checking of one argument, one can do:

def a_bound_function(self, arg1):
  """Do something interesting.

  Args:
    arg1: The first argument.

  # noqa: DAR101 arg1
  """

Or to ignore only one type of exception in Raises:

def always_raises_exception(x):
    """Raise a zero division error or type error.o

    Args:
      x: The argument which could be a number or could not be.

    Raises:
      ZeroDivisionError: If x is a number.  # noqa: DAR402
      TypeError: If x is not a number.  # noqa: DAR402

    """
    x / 0

I suggest, if possible, adding the # noqa: comments directly in the code, for the above examples:

def a_bound_function(self,
    arg1 # noqa: DAR101
):
  """Do something interesting.

  Args:
    arg1: The first argument.
  """

def always_raises_exception(x):  # noqa: DAR402 ZeroDivisionError TypeError
    """Raise a zero division error or type error.o

    Args:
      x: The argument which could be a number or could not be.

    Raises:
      ZeroDivisionError: If x is a number.
      TypeError: If x is not a number.

    """
    x / 0

Using the equivalent codes from pydoclint, of course.

Docs request: more info on `allow-init-docstring`

I am sort of curious with this one, why is allow-init-docstring defaulted to False in 0.3.8?

PEP 257 talks about __init__ needing a docstring, so I am wondering why this tool chose to deviate from PEP 257 in its default behavior?

This default piqued my curiosity. If you wouldn't mind, can you link docs on why __init__ docstrings are discouraged?

Bug: return values that is a union is dependent on order

Edit:

  • pydoclint version: 0.3.3
  • Current settings in pyproject.toml
[tool.pydoclint]
style = "google"

Thanks for starting this project. As a current user of darglint, i am trying to migrate to using pydoclint and oh boy is it much better!

I noticed a unexpected error. The following code will raise a pydoclint violation:

from typing import Any

def test_function() -> dict[str, Any] | None:
    """Some function

    Returns:
        dict[str, Any] | None: Something
    """
    ...
DOC203: Function `test_function` return type(s) in docstring not consistent with the return annotation. Return annotation types: ['dict[str, Any] | None']; docstring return section types: ['']

However, flipping the union around does not raise any violations

def test_function_2() -> None | dict[str, Any]:
    """Some other function

    Returns:
        None | dict[str, Any]: Something else
    """
    ...

# No errors here

As far as i know, the Union operator is commutative, i.e X | Y == Y | X, so neither of these should raise errors

Escaping characters in docstring raises DOC103

This is more a question than an issue (I'm sorry for opening my third issue in a week's time...).

I am on the brink of adding pydoclint to my company's codebase (started at 1000 issues.... now down to 1), but the only way I'm able to is by excluding the file via pre-commit.

I have the following class:

class Foo:
    r"""Description

    Args:
        with\_ (dict): Description
        select (str): Description
    """
    def __init__(self, with_: dict, select: str):
        ...

pydoclint imo correctly raises:

    8: DOC103: Method `Foo.__init__`: Docstring arguments are different from function arguments.
(Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). 
Arguments in the function signature but not in the docstring: [with_: dict]. Arguments in the docstring but 
not in the function signature: [with\_: dict].

If I try to remove the \ before the underscore, sphinx complains:

$ .Foo:7: ERROR: Unknown target name: "with".

...and the documentation shows with instead of with_.

Do you have a suggested approach here (outside excluding the entire file?) ๐Ÿ™

[dev] Lint with ruff

I would like to configure ruff as a linter for the our python code. It is very fast and supports all rules known from flake8, pylint,...

Functions `yield`ing wrongly report errors about `Return:` being missing

When functions yield instead of return, a Yield: should be used in the docstring (at least in google-style) instead of Return:, but pydoclint complains about a missing Return:.

For example this code: https://github.com/frequenz-floss/frequenz-sdk-python/blob/776352c0e9383506a778751560a3e21e91e2d6fd/src/frequenz/sdk/timeseries/_base_types.py#L52-L60

    def __iter__(self) -> Iterator[float | None]:
        """Return an iterator that yields values from each of the phases.

        Yields:
            Per-phase measurements one-by-one.
        """
        yield self.value_p1
        yield self.value_p2
        yield self.value_p3

Fails with:

src/frequenz/sdk/timeseries/_base_types.py
    52: DOC201: Method `Sample3Phase.__iter__` does not have a return section in docstring

When running: pydoclint --style google -aid True -th False src/ (for pydoclint 0.0.6)

Yields section requires wrong type annotation

For a generator (function) like the following, according to the Google style docs, the annotation in the Yields: section should be annotated as follows:

from typing import Generator


def foo(n: int) -> Generator[int, None, None]:
    """Description

    Args:
        n (int): Description

    Yields:
        int: Description
    """
    yield from range(n)

This throws an error when I try to run it through pydoclint:

4: DOC404: Function `foo` yield type(s) in docstring not consistent with the return annotation.
  Return annotation types: ['Generator[int, None, None]']; docstring return section types: ['int']

I.e., it wants:

    Yields:
        Generator[int, None, None]: Description

Is this a bug?

False Positive on Yields/Disable check on a line

Hello. Thanks for this library. It's very useful.

We're getting false positive on the yield checks. This happens when there's a sub function inside a function that uses yields. Our code looks something like this.

def func(arg: List[int]) -> str:
    """Converts int to string

    Args: 
        arg (List[int]): Number to convert

    Returns:
        str: String equivalent
    """

    def _convert(num: List[int]) -> Iterable[str]:
        for i in num:
            yield str(num)

    return ",".join(_convert(arg))

We get an error like: 106: DOC402: Function func has "yield" statements, but the docstring does not have a "Yields" section

As a work around, is there a way to disable the check per line?

Unclear error message when colons are not preceded by a space

Here is the code to reproduce the issue:
image

Here is the error raised by pydoclint:

image
Here the 2 error messages do not indicate the root cause: lack of a space before the colons in the docstrings.

A good first fix would be to add that these errors could be triggered by the lack of a space before the colons.
Ideally, this should be detected.

google: Allow omitting `Yields:` if the yield (generator/iterator) type is `None`

The same as Returns: is optional if the return type is None, according to the google style guide it is also optional for Yields:.

Even when this sounds odd to have, it is an useful idiom to write context managers very easily.

import collections.abc
import contextlib


@contextlib.contextmanager
def f() -> collections.abc.Iterator[None]:
    """This function doesn't yield anything."""
    # Some some initialization
    yield
    # Some cleanup


with f():
    print("I'm initialized")

print("I'm cleaned up")
$ pydoclint --style=google --check-return-types=False --check-yield-types=False --skip-checking-short-docstrings=False yieldnone.py
Skipping files that match this pattern: \.git|\.tox
yieldnone.py
yieldnone.py
    6: DOC201: Function `f` does not have a return section in docstring 
    6: DOC402: Function `f` has "yield" statements, but the docstring does not have a "Yields" section 

Use pydoclint as pre-commit hook

I like to use pydoclint as pre-commit hook.

It works perfectly well when running it from the CL pydoclint --config=pyproject.toml src tests rest_api.
But running the hook defined in pre-commit-config.yaml:

  - repo: local
    hooks:
      - id: pydoclint
        name: pydoclint
        entry: pydoclint --config=pyproject.toml
        language: system
        files: ^(src|tests|rest_api)/.*\.py$
        always_run: true
        stages: [pre-commit, pre-push, manual]

with pre-commit run --all-files --hook-stage manual pydoclint results in

pydoclint................................................................Failed
- hook id: pydoclint
- exit code: 1

Loading config from user-specified .toml file: pyproject.toml
Found options defined in pyproject.toml:
{'style': 'google', 'allow_init_docstring': True, 'check_type_hint': False}
Skipping files that match this pattern: \.git|\.tox
rest_api\tool_1\views.py
rest_api\tool_1\apps.py
rest_api\config\urls.py
rest_api\tool_1\urls.py
Traceback (most recent call last):
  File "C:\Users\liebschs\Programs\WinPython64-3.10.9\WPy64-31090\python-3.10.9.amd64\lib\runpy.py", line 196, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "C:\Users\liebschs\Programs\WinPython64-3.10.9\WPy64-31090\python-3.10.9.amd64\lib\runpy.py", line 86, in _run_code
    exec(code, run_globals)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\Scripts\pydoclint.EXE\__main__.py", line 7, in <module>
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\decorators.py", line 26, in new_func
    return f(get_current_context(), *args, **kwargs)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\pydoclint\main.py", line 219, in main
    click.echo(click.style('\U0001f389 No violations \U0001f389', fg='green', bold=True))
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\utils.py", line 299, in echo
    file.write(out)  # type: ignore
  File "C:\Users\liebschs\Programs\WinPython64-3.10.9\WPy64-31090\python-3.10.9.amd64\lib\encodings\cp1252.py", line 19, in encode
    return codecs.charmap_encode(input,self.errors,encoding_table)[0]
UnicodeEncodeError: 'charmap' codec can't encode character '\U0001f389' in position 0: character maps to <undefined>
Loading config from user-specified .toml file: pyproject.toml
Found options defined in pyproject.toml:
{'style': 'google', 'allow_init_docstring': True, 'check_type_hint': False}
Skipping files that match this pattern: \.git|\.tox
rest_api\tool_1\tests.py
rest_api\tool_1\models.py
rest_api\config\__init__.py
rest_api\config\asgi.py
Traceback (most recent call last):
  File "C:\Users\liebschs\Programs\WinPython64-3.10.9\WPy64-31090\python-3.10.9.amd64\lib\runpy.py", line 196, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "C:\Users\liebschs\Programs\WinPython64-3.10.9\WPy64-31090\python-3.10.9.amd64\lib\runpy.py", line 86, in _run_code
    exec(code, run_globals)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\Scripts\pydoclint.EXE\__main__.py", line 7, in <module>
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\decorators.py", line 26, in new_func
    return f(get_current_context(), *args, **kwargs)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\pydoclint\main.py", line 219, in main
    click.echo(click.style('\U0001f389 No violations \U0001f389', fg='green', bold=True))
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\utils.py", line 299, in echo
    file.write(out)  # type: ignore
  File "C:\Users\liebschs\Programs\WinPython64-3.10.9\WPy64-31090\python-3.10.9.amd64\lib\encodings\cp1252.py", line 19, in encode
    return codecs.charmap_encode(input,self.errors,encoding_table)[0]
UnicodeEncodeError: 'charmap' codec can't encode character '\U0001f389' in position 0: character maps to <undefined>
Loading config from user-specified .toml file: pyproject.toml
Found options defined in pyproject.toml:
{'style': 'google', 'allow_init_docstring': True, 'check_type_hint': False}
Skipping files that match this pattern: \.git|\.tox
src\new_sunfire_python_package\tool_1.py
rest_api\config\helpers.py
rest_api\tool_1\admin.py
rest_api\config\settings.py
Traceback (most recent call last):
  File "C:\Users\liebschs\Programs\WinPython64-3.10.9\WPy64-31090\python-3.10.9.amd64\lib\runpy.py", line 196, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "C:\Users\liebschs\Programs\WinPython64-3.10.9\WPy64-31090\python-3.10.9.amd64\lib\runpy.py", line 86, in _run_code
    exec(code, run_globals)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\Scripts\pydoclint.EXE\__main__.py", line 7, in <module>
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\decorators.py", line 26, in new_func
    return f(get_current_context(), *args, **kwargs)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\pydoclint\main.py", line 219, in main
    click.echo(click.style('\U0001f389 No violations \U0001f389', fg='green', bold=True))
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\utils.py", line 299, in echo
    file.write(out)  # type: ignore
  File "C:\Users\liebschs\Programs\WinPython64-3.10.9\WPy64-31090\python-3.10.9.amd64\lib\encodings\cp1252.py", line 19, in encode
    return codecs.charmap_encode(input,self.errors,encoding_table)[0]
UnicodeEncodeError: 'charmap' codec can't encode character '\U0001f389' in position 0: character maps to <undefined>
Loading config from user-specified .toml file: pyproject.toml
Found options defined in pyproject.toml:
{'style': 'google', 'allow_init_docstring': True, 'check_type_hint': False}
Skipping files that match this pattern: \.git|\.tox
rest_api\tool_1\__init__.py
tests\tool_1\test_tool_1.py
rest_api\config\wsgi.py
rest_api\tool_1\migrations\__init__.py
Traceback (most recent call last):
  File "C:\Users\liebschs\Programs\WinPython64-3.10.9\WPy64-31090\python-3.10.9.amd64\lib\runpy.py", line 196, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "C:\Users\liebschs\Programs\WinPython64-3.10.9\WPy64-31090\python-3.10.9.amd64\lib\runpy.py", line 86, in _run_code
    exec(code, run_globals)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\Scripts\pydoclint.EXE\__main__.py", line 7, in <module>
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\decorators.py", line 26, in new_func
    return f(get_current_context(), *args, **kwargs)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\pydoclint\main.py", line 219, in main
    click.echo(click.style('\U0001f389 No violations \U0001f389', fg='green', bold=True))
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\utils.py", line 299, in echo
    file.write(out)  # type: ignore
  File "C:\Users\liebschs\Programs\WinPython64-3.10.9\WPy64-31090\python-3.10.9.amd64\lib\encodings\cp1252.py", line 19, in encode
    return codecs.charmap_encode(input,self.errors,encoding_table)[0]
UnicodeEncodeError: 'charmap' codec can't encode character '\U0001f389' in position 0: character maps to <undefined>
Loading config from user-specified .toml file: pyproject.toml
Found options defined in pyproject.toml:
{'style': 'google', 'allow_init_docstring': True, 'check_type_hint': False}
Skipping files that match this pattern: \.git|\.tox
rest_api\manage.py
src\new_sunfire_python_package\__init__.py
Traceback (most recent call last):
  File "C:\Users\liebschs\Programs\WinPython64-3.10.9\WPy64-31090\python-3.10.9.amd64\lib\runpy.py", line 196, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "C:\Users\liebschs\Programs\WinPython64-3.10.9\WPy64-31090\python-3.10.9.amd64\lib\runpy.py", line 86, in _run_code
    exec(code, run_globals)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\Scripts\pydoclint.EXE\__main__.py", line 7, in <module>
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\decorators.py", line 26, in new_func
    return f(get_current_context(), *args, **kwargs)
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\pydoclint\main.py", line 219, in main
    click.echo(click.style('\U0001f389 No violations \U0001f389', fg='green', bold=True))
  File "C:\Users\liebschs\MyFiles\playground\cookiecutter-playground\new-sunfire-python-package\venv\lib\site-packages\click\utils.py", line 299, in echo
    file.write(out)  # type: ignore
  File "C:\Users\liebschs\Programs\WinPython64-3.10.9\WPy64-31090\python-3.10.9.amd64\lib\encodings\cp1252.py", line 19, in encode
    return codecs.charmap_encode(input,self.errors,encoding_table)[0]
UnicodeEncodeError: 'charmap' codec can't encode character '\U0001f389' in position 0: character maps to <undefined>

It seems that it lints all files it should, but for some a UnicodeEncodeError is raised. Any ideas?

SyntaxError

Hi,

since updating to version 0.1.5 I get a SyntaxError with the following function:

def test(a="a"):
    """
    Title

    Parameters
    ----------
    a : str, default a
    """
    pass
Traceback (most recent call last):
  File "/workspace/.conda/flake/bin/pydoclint", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/click/core.py", line 1157, in __call__
    return self.main(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/click/core.py", line 1078, in main
    rv = self.invoke(ctx)
         ^^^^^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/click/core.py", line 1434, in invoke
    return ctx.invoke(self.callback, **ctx.params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/click/core.py", line 783, in invoke
    return __callback(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/click/decorators.py", line 33, in new_func
    return f(get_current_context(), *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/pydoclint/main.py", line 254, in main
    violationsInAllFiles: Dict[str, List[Violation]] = _checkPaths(
                                                       ^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/pydoclint/main.py", line 354, in _checkPaths
    violationsInThisFile: List[Violation] = _checkFile(
                                            ^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/pydoclint/main.py", line 398, in _checkFile
    visitor.visit(tree)
  File "/workspace/.conda/flake/lib/python3.11/ast.py", line 418, in visit
    return visitor(node)
           ^^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/ast.py", line 426, in generic_visit
    self.visit(item)
  File "/workspace/.conda/flake/lib/python3.11/ast.py", line 418, in visit
    return visitor(node)
           ^^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/pydoclint/visitor.py", line 126, in visit_FunctionDef
    argViolations = self.checkArguments(node, parent_, doc)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/pydoclint/visitor.py", line 370, in checkArguments
    if not docArgs.equals(
           ^^^^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/pydoclint/utils/arg.py", line 213, in equals
    verdict = self == other
              ^^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/pydoclint/utils/arg.py", line 135, in __eq__
    return self.infoList == other.infoList
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/pydoclint/utils/arg.py", line 36, in __eq__
    return self.name == o.name and self._eq(self.typeHint, o.typeHint)
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/pydoclint/utils/arg.py", line 105, in _eq
    str1_: str = unparseAnnotation(ast.parse(stripQuotes(str1)))
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/ast.py", line 50, in parse
    return compile(source, filename, mode, flags,
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<unknown>", line 1
    str, default a
                 ^
SyntaxError: invalid syntax

When I tried to switch to the other numpy equivalents a : str, default=a worked and a : str, default: a resulted in a different error:

Traceback (most recent call last):
  File "/workspace/.conda/flake/bin/pydoclint", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/click/core.py", line 1157, in __call__
    return self.main(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/click/core.py", line 1078, in main
    rv = self.invoke(ctx)
         ^^^^^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/click/core.py", line 1434, in invoke
    return ctx.invoke(self.callback, **ctx.params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/click/core.py", line 783, in invoke
    return __callback(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/click/decorators.py", line 33, in new_func
    return f(get_current_context(), *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/pydoclint/main.py", line 254, in main
    violationsInAllFiles: Dict[str, List[Violation]] = _checkPaths(
                                                       ^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/pydoclint/main.py", line 354, in _checkPaths
    violationsInThisFile: List[Violation] = _checkFile(
                                            ^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/pydoclint/main.py", line 398, in _checkFile
    visitor.visit(tree)
  File "/workspace/.conda/flake/lib/python3.11/ast.py", line 418, in visit
    return visitor(node)
           ^^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/ast.py", line 426, in generic_visit
    self.visit(item)
  File "/workspace/.conda/flake/lib/python3.11/ast.py", line 418, in visit
    return visitor(node)
           ^^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/pydoclint/visitor.py", line 126, in visit_FunctionDef
    argViolations = self.checkArguments(node, parent_, doc)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/pydoclint/visitor.py", line 370, in checkArguments
    if not docArgs.equals(
           ^^^^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/pydoclint/utils/arg.py", line 213, in equals
    verdict = self == other
              ^^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/pydoclint/utils/arg.py", line 135, in __eq__
    return self.infoList == other.infoList
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/pydoclint/utils/arg.py", line 36, in __eq__
    return self.name == o.name and self._eq(self.typeHint, o.typeHint)
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/site-packages/pydoclint/utils/arg.py", line 105, in _eq
    str1_: str = unparseAnnotation(ast.parse(stripQuotes(str1)))
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/workspace/.conda/flake/lib/python3.11/ast.py", line 50, in parse
    return compile(source, filename, mode, flags,
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<unknown>", line 1
    str, default: a
    ^^^
SyntaxError: only single target (not tuple) can be annotated

I am not sure what either of the errors mean so thank you for your help in advance.

Bug: Requires Return Section in Docstring When None Is the Return Type

I noticed that docstrings require a return section even when the return type is None, but this only occurs when there are parameters in the function. The error does not occur if there are zero parameters in the function. Also, the docstrings use Google style.

Example (Error):

def test(text: str) -> None:
    """Run test.

    Args:
        text (str): Text for the function
    """

Example (Pass):

def test() -> None:
    """Run test.

    """

Add shortcuts for long typing.Annotated type hints

Some frameworks, such as FastAPI make heavy use of typing.Annotated to add metadata to function parameters. This becomes unwieldy very fast, if the entire annotation has to be added in the docstring. For example, consider the following pseudo-code for a FastAPI function that gets invoked for an HTTP GET request to "/{id}":

@app.get(path='/{id}')
async def get_entity_by_id(
    entity_id: Annotated[
        int,
        Path(
            alias='id',
            title='Entity ID',
            description='The ID of the entity that is to be retrieved.',
            gt=0,
            examples=[1, 123]
        )
    ]
) -> Entity:
    ...

The metadata can become arbitrarily large and I would bet there are more frameworks out there that also use typing.Annotated in this way.

I want to propose to add an exception for typing.Annotated. I have read Cases that pydoclint is not designed to handle and Notes on writing type hints and I understand that using the AST module does not allow us to derive any semantic meaning about type annotations, but I think it would still be beneficial to make an exception on matching names alone, i.e., when the name of the type starts with Annotated[ or typing.Annotated[, then the user can substitute the actual type hint with a short hand.

Since the actual type that results from the annotation is the type specified as the first argument to typing.Annotated, I would propose that the actual type should be allowed as a short hand. So instead of adding the entire type to the docstring, it would suffice to specify the first argument of Annotated. For example, for the function definition above, PyDocLint should not raise a validation error, when the following docstring would be added:

@app.get(path='/{id}')
async def get_entity_by_id(
    entity_id: Annotated[
        int,
        Path(
            alias='id',
            title='Entity ID',
            description='The ID of the entity that is to be retrieved.',
            gt=0,
            examples=[1, 123]
        )
    ]
) -> Entity:
    """Retrieve the entity with the specified ID.

    Args:
        entity_id (int): The ID of the entity that is to be retrieved.

    Returns:
        Entity: The entity with the specified ID.
    """
    ...

I have had a look at PyDocLint's code and I believe (besides updating tests and docs), the only thing that needs to be changed is in the Arg._typeHintsEq method in the pydoclint/utils/arg.py file. I believe the following heuristic would be enough to implement this:

@classmethod
def _typeHintsEq(cls, hint1: str, hint2: str) -> bool:
    try:
        hint1_: str = unparseAnnotation(ast.parse(stripQuotes(hint1)))
    except SyntaxError:
        hint1_ = hint1

    try:
        hint2_: str = unparseAnnotation(ast.parse(stripQuotes(hint2)))
    except SyntaxError:
        hint2_ = hint2

    return (
        hint1_ == hint2_ or
        hint1_.startswith(f'typing.Annotated[{hint2_},') or hint1_.startswith(f'Annotated[{hint2_},') or
        hint2_.startswith(f'typing.Annotated[{hint1_},') or hint2_.startswith(f'Annotated[{hint1_},')
    )

Before submitting a pull request, I wanted to get feedback from you. Would you be willing to include an exception for typing.Annotated?

Re-raising an exception is not properly handled (google-style only)

luca@frq-lap-019:/tmp/venv [venv] 1 ! $ pip show pydoclint 
Name: pydoclint
Version: 0.0.6
Summary: A linter to check arguments in Python docstrings
Home-page: https://github.com/jsh9/pydoclint
Author: 
Author-email: 
License: MIT
Location: /tmp/venv/lib/python3.11/site-packages
Requires: click, docstring-parser, flake8, numpydoc
Required-by: 
luca@frq-lap-019:/tmp/venv [venv] $ cat t.py 
def f() -> None:
    """This is a function.

    Raises:
        FileNotFoundError: If the file does not exist.
    """
    try:
        open("nonexistent")
    except FileNotFoundError:
        raise
luca@frq-lap-019:/tmp/venv [venv] $ pydoclint t.py 
Skipping files that match this pattern: \.git|\.tox
t.py
๐ŸŽ‰ No violations ๐ŸŽ‰
luca@frq-lap-019:/tmp/venv [venv] $ pydoclint --style google t.py 
Skipping files that match this pattern: \.git|\.tox
t.py
t.py
    1: DOC502: Function `f` has a "Raises" section in the docstring, but there are not "raise" statements in the body 

Docs request: `pre-commit` config for `flake8`

    - repo: https://github.com/jsh9/pydoclint
      rev: 0.3.8
      hooks:
          - id: pydoclint
            additional_dependencies: [flake8]

This is a possible config for using pydoclint as a flake8-plugin, via pre-commit. However, it doesn't work, noqa comments aren't being respected.

This is the correct way:

    - repo: https://github.com/pycqa/flake8
      rev: 6.1.0
      hooks:
          - id: flake8
            args: [--toml-config=pyproject.toml]
            additional_dependencies:
                - Flake8-pyproject>=1.2.0 # For --toml-config
                - pydoclint

With the `pyproject.toml:

[tool.flake8]
allow-init-docstring = true
arg-type-hints-in-docstring = false
check-return-types = false
style = "google"

Can we add this to the docs? Notably, Flake8-pyproject is key to keep using TOML config.

Bug with return annotation when using `@contextmanager`

I use contextmanager in several places in my testing utilities, and one of these functions are part of the public API, meaning it's documented. I struggle to write it in a manner that does not raise any errors from pydoclint:

from contextlib import contextmanager
from typing import Iterator


@contextmanager
def stuff_maker() -> Iterator[int]:
    """Text

    Yields:
        Iterator[int]: The stuff
    """
    yield 5

Running this file gives me a DOC203 error:

$ pydoclint --style=google aaaa.py
    6: DOC203: Function `stuff_maker` return type(s) in docstring not consistent with the return annotation. Return annotation has 1 type(s); docstring return section has 0 type(s).

I have tried several variations, but all lead to at least one error ๐Ÿค” Any suggestions?

๐Ÿ› False positive 'Return annotation not ending with `]`' on union retun type

For union return types that start with tuple[ (e.g. my use case tuple[Figure, Axis] | None) InternalError is raised false positively.
This is because _isTuple returns True for union types

def _isTuple(self) -> bool:
return (
self.annotation is not None
and self.annotation.lower().startswith('tuple[')
)

And consecutive checks are meant to work with pure tuples.

DOC203 reported even when type hints in docstring are disabled

When --type-hints-in-docstring False is passed, I'd expect that the check for DOC203 is skipped. With no type from the docstring to compare against, the check can never succeed.

$ pydoclint --version
pydoclint, version 0.1.1
$ cat foo.py 
def foo(i: int) -> int:
    """Calculate something.

    :param i: Some argument.
    :return: Some result.
    """
    return 2 * i
$ pydoclint --style sphinx --type-hints-in-docstring False foo.py 
Skipping files that match this pattern: \.git|\.tox
foo.py
foo.py
    1: DOC203: Function `foo` return type(s) in docstring not consistent with the return annotation. Return annotation types: ['int']; docstring return section types: ['']

[DOC403/DOC502] False positive for abstract methods

Abstract method / abstract classes should be able to define a signature/docstring containing Yields or Raises sections even though they do not contain a yield or raise statement.

For example:

from abc import ABC, abstractmethod
from collections.abc import Iterator


class AbstractClass(ABC):
    """Example abstract class."""

    @abstractmethod
    def abstract_method(self, var1: str) -> Iterator[str]:
        """Abstract method.

        Args:
            var1 (str): Variable.

        Raises:
            ValueError: Example exception

        Yields:
            str: Paths to the files and directories listed.
        """

Results in:

    11: DOC201: Method `AbstractClass.abstract_method` does not have a return section in docstring 
    11: DOC403: Method `AbstractClass.abstract_method` has a "Yields" section in the docstring, but there are no "yield" statements or a Generator return annotation  
    11: DOC502: Method `AbstractClass.abstract_method` has a "Raises" section in the docstring, but there are not "raise" statements in the body 

I also do not understand with DOC201 is triggered in this case.

NB: DOC202 is not impacted, the following code does not trigger any warning:

from abc import ABC, abstractmethod


class AbstractClass(ABC):
    """Example abstract class."""

    @abstractmethod
    def abstract_method(self, var1: str) -> str:
        """Abstract method.

        Args:
            var1 (str): Variable.

        Returns:
            str: Paths to the files and directories listed.
        """

Option to disable printing config

pre-commit checks every file separately, so the config object is printed multiple times polluting stdout.
It feels like it should only be printed for debugging purposes (e.g. with --debug option enabled).

if len(finalConfig) > 0:
print(f'Found options defined in {tomlFilename}:')
print(finalConfig)
else:
print(f'No config found in {tomlFilename}.')

Parameters not processed correctly for numpy style with incorrect underscoring

In numpy style, when the underscoring count doesn't match the "Parameter", then pydoclint (version 0.3.8) reports error about arguments.

Example code:

def example(parameter: int) -> int:
    """This is a example function.

    Parameters
    ------
    parameter : int
        This is a example parameter.

    Returns
    -------
    int
        This is a example return value.
    """
    print(parameter)
    return parameter

Errors reported:

    1: DOC101: Function `example`: Docstring contains fewer arguments than in function signature. 
    1: DOC109: Function `example`: The option `--arg-type-hints-in-docstring` is `True` but there are no type hints in the docstring arg list
    1: DOC103: Function `example`: Docstring arguments are different from function arguments. (Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). Arguments in 
the function signature but not in the docstring: [parameter: int].

No errors are reported when the underscores are fixed to match the "Parameters" line.

Multiline types

Hi,

I have a type that I am unable to not get a DOC105 for.

If I have something like:

def my_function(
    my_variable_1: int,
    my_variable_2: Literal[
        "option_1",
        "option_2",
        "option_3",
        "option_4",
        "option_5",
        "option_6",
    ],
) -> None:
    """
    Some random function.

    :param my_variable_1: Some integer that does something.
    :type my_variable_1: int
    :param my_variable_2: Some literal that does something.
    :type my_variable_2: Literal[
        "option_1",
        "option_2",
        "option_3",
        "option_4",
        "option_5",
        "option_6",
    ]
    """

And I can't quite work out how to get the type for `my_variable_2` to work.

Relevant settings I am using are:

skip-checking-short-docstrings = false
style = 'sphinx'


I am assuming there is some way I can structure that type for my_variable_2 that works, I just haven't quite figured it out yet.

Long literal types that need a line return are not understood correctly by pydoclint

Code to reproduce :
image

Here the line return in argument c's type in the docstring is truncated because of the line break.
pydoclint therefore thinks types do not match:

image

(Note that using double quotes does not fix this issue)
Of course a better practice would have been to declare this literal type elsewhere and use it as a constant, but i stumbled into that while linting old code from my company.

Unclear error messages when sections are not separated with white lines

Here is the sample code to reproduce this issue
image

Note that despite of the rule D410 from ruff, the lack of a white line before the Returns section is not detected. (Issue on ruff as well)
image

When running pydoclint, the error message is unclear:
image

4 error codes are returned, none describing the actual root cause (lack of a white line between sections)

Note that this would not be an issue if D410 from ruff had worked so maybe not a priority.

Request: not throwing DOC501 for `abstractmethod`

from abc import ABC, abstractmethod


class Foo(ABC):
    @abstractmethod
    def bar(self, arg: int = 1) -> None:
        """
        Do something.

        Args:
            arg: Some argument.
        """
        raise NotImplementedError


class DFoo(Foo):
    def bar(self, arg: int = 1) -> None:
        print(arg)  # NOTE: subclass doesn't have potential to raise a NotImplementedError

For abstractmethod, it's a common practice to make the base class's body be raise NotImplementedError. This basically denies subclasses from calling super(), double enforcing the override.

Currently, as of pydoclint==0.3.8, it throws DOC501 on the base class's docstring for a missing "raise" statement (per the NotImplementedError). However, subclasses won't have the NotImplementedError in their implementation, so this is sort of a false positive DOC501.

I think pydoclint should not be throwing DOC501 when it's a docstring for an abstractmethod in an ABC.

Missed raises in match case blocks

The following function has the error: Function test has a "Raises" section in the docstring, but there are not "raise" statements in the body Flake8(DOC502)

It doesn't matter if the ValueError is within the catchall case or one of the actual cases.

def test(number: int) -> bool:
    """
    Test function.

    :param number: A number
    :type number: int
    :raises ValueError: When number is not 1 or 2
    :return: True if number is 1, False if 2
    :rtype: bool
    """
    match number:
        case 1:
            return True
        case 2:
            return False
        case _:
            raise ValueError('Number is not 1 or 2.')

False positive for return annotations using `|`

I have a few files using the pipe operator instead of typing.Union, and I get what I think looks like a false positive DOC203.

To reproduce:

from typing import Sequence

class Foo:
    pass

class Bar:
    def fn(self, a: Foo | Sequence[Foo]) -> Foo | int:
        """Some description.

        Args:
            a (Foo | Sequence[Foo]): Some description.

        Returns:
            Foo | int: Some description.

        Examples:

            Some examples
        """
        return ...

When running the following file through pydoclint I get the following output:

Loading config from user-specified .toml file: pyproject.toml
Found options defined in pyproject.toml:
{'style': 'google',
 'exclude': 'tests/|scripts/',
 'quiet': True,
 'require_return_section_when_returning_nothing': True,
 'arg_type_hints_in_signature': True,
 'arg_type_hints_in_docstring': True,
 'allow_init_docstring': False,
 'check_return_types': True,
 'check_arg_order': True}
aaaa.py
    9: DOC203: Method `Bar.fn` return type(s) in docstring not consistent with the return annotation. 
    Return annotation types: ['Foo | int']; docstring return section types: ['']

Do I have to use -> Union[Foo, int]:? Or what's wrong here ๐Ÿค”

[google] Adding types to the documentation should not be enforced

luca@frq-lap-019:/tmp/venv [venv] 1 ! $ cat t.py 
def f(a: int) -> None:
    """This is a function.

    Args:
        a: This is an argument.
    """
    pass
luca@frq-lap-019:/tmp/venv [venv] $ pydoclint --style google t.py 
Skipping files that match this pattern: \.git|\.tox
t.py
t.py
    1: DOC105: Function `f`: Argument names match, but type hints do not match 

This should not fail when using google-style, as it is allowed:

The description should include required type(s) if the code does not contain a corresponding type annotation.

https://google.github.io/styleguide/pyguide.html#doc-function-args
Originally posted by @leandro-lucarella-frequenz in #14 (comment)

[dev]: Seperate lint from tests in ci.

This is very useful for understanding why ci fails and it also speeds up the PR checks.
Currently linting errors will fail code tests and one has to click a lot to find out what exactly failed.

In my projects its as simple as running pre-commit run --all-files. However here you have configured linters in multiple places. There is pre-commit and there is tox. It might make sense to run some linters for multiple python versions if they can only detect errors for the currently installed version. But I can't make sense of running formatters for each version.\

What do you think?

Request: ability to disable certain codes explicitly via config

For me, I would like to be able to globally disable specific codes via a pyproject.toml config file.

I see there are CLI flags for this that one can find using pydoclint --help:

  • allow-init-docstring I believe disables DOC301
  • skip-checking-raises I believe disables D5XX

I am more hoping for a rules ignore list as offered by tools like ruff and pylint. The list would explicitly list codes like D301, as opposed to one-off CLI flags that one has to look up/figure out.

The request is thus to add support for a generalized ignore CLI argument, where one can input a list of codes to globally disable.

Sphinx prefers escaping the asterisk for *args and **kwargs with a backslash but this confuses pydoclint

Given this

def f(*args, **kwargs):
    """My function.

    :param *args: Positional args.
    :param **kwargs: Keyword args.
    """

sphinx throws a warning:

WARNING: Inline emphasis start-string without end-string.

and this also seems to mess with the formatting a bit.

But when it's escaped with a backslash, it's fine:

def f(*args, **kwargs):
    """My function.

    :param \\*args: Positional args.
    :param \\**kwargs: Keyword args.
    """

However, pydoclint then complains:

 DOC103: Function `f`: Docstring arguments are different from function arguments. (Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). Arguments in the function signature but not in the docstring: [**kwargs: Any]. Arguments in the docstring but not in the function signature: [\**kwargs: ].

So, it seems the backslash confuses pydoclint.

Is dependency on flake8 needed?

Looking into this package as a replacement for darglint and really like it so far. ๐Ÿ˜ƒ

Right now I am using ruff for linting instead instead of separate flake8 packages and plugins.
I see that this project depends on flake8 package according to the setup.cfg file.
But is it really needed? I was able to uninstall flake8 from my environment via pip uninstall ... and could still run pydoclint without issues.

Is it possible to have flake8 as an optional dependency instead? Or am I missing something?

BUG: Return literal with double quotes incorrectly raises v203

To reproduce, use the following code in ./tests/data/playground.py and run pytest.

from typing import Literal

def testLiteral() -> Literal["foo"]:
    """
    Test literal.

    Returns:
        Literal["foo"]: Return literal string foo.
    """
    return "foo"

It raises the following error:
DOC203: Function `testLiteral` return type(s) in docstring not consistent with the return annotation. Return annotation types: ["Literal[\'foo\']"]; docstring return section types: [\'Literal["foo"]\']

The issue seems to stem from the fact that when the Python AST unparses code it uses single quotes by default:

>>> code = 'def testLiteral() -> Literal["a"]:\n\treturn "a"'
>>> ast.unparse(ast.parse(code))
"def testLiteral() -> Literal['a']:\n    return 'a'"

Some code formatters like black replace single quotes with double, which will result in violation 203 being thrown. A temporary work around for users is continuing to use the double quotes in the return but using single quotes in the docstring, i.e.

from typing import Literal

def testLiteral() -> Literal["foo"]:
    """
    Test literal.

    Returns:
        Literal['foo']: Return literal string foo.
    """
    return "foo"

as black doesn't touch the single quotes in the docstring. But I'm not a fan of the (slight) inconsistency between docstring and return annotation ๐Ÿ˜›

Add support for Google-style comments

I know it is already planned, but I just wanted to create an issue so this has some visibility and people can also vote for it with a ๐Ÿ‘ reaction for example.

We would love to replace the dead corpse of darglint with this tool :)

Review `DOC404` / `DOC405` (Generator vs Iterator)

The rule imposed by DOC404/DOC405, including the justification in Notes on Generator vs Iterator doesn't go in line with Python's official documentation, which explicitly says Iterator and AsyncIterator can be used as return types for functions that yield (even more, the documentation say that also Iterable/AsyncIterable can be used).

Having this (artificial) restriction in pydoclint only makes users have to write more pedantic code, which is also harder to read for the casual reader (what are all the None in Generator?), when the shorter form is supported by Python and type-checkers like mypy and pyright.

image

image

Adjust default option based on the `--style`

Now that google-style is also supported, it would be good if other options are adjusted to what is valid for google-style. For example:

  • --allow-init-docstring should default to True
  • --check-type-hint should default to False (ideally it should be checked if the function has type hints in the signature, then type hints can be omitted in the docstring, otherwise it should be present)

Support return type `NoReturn`

Currently if a function uses the return type typing.NoReturn a Returns: section is still required in the docstring, which doesn't make a lot of sense.

from typing import NoReturn


def f() -> NoReturn:
    """This function never returns.

    Raises:
        Exception: Always raises an exception.
    """
    raise Exception("Error")
$ pydoclint --style=google --check-return-types=False --arg-type-hints-in-docstring=False --arg-type-hints-in-signature=True noreturn.py 
Skipping files that match this pattern: \.git|\.tox
noreturn.py
noreturn.py
    4: DOC201: Function `f` does not have a return section in docstring 

Program description in help is "Yes"

When calling pydoclint with the --help option the program description is "Yes". There is a comment in the source code about clicker setting it to the docstring.

Output: (truncated because only the first few lines are important)

Usage: pydoclint [OPTIONS] [PATHS]...

  Yes

Options:
  -s, --src TEXT                  The source code to check
  -q, --quiet                     If True, do not print the file names being

This happens for me both when I install pydoclint from source and when using pip install . cloned from github.

Request: Add configuration option that allows for type hint in signature _or_ docstring, but not both

We are in the midst of updating our codebase to use type hints in signatures. While --arg-type-hints-in-docstring and --arg-type-hints-in-signature are great for repositories/projects that are already following one style and want to enforce it, they don't support one in the midst of transitioning between one and the other. With our repositories enforcing flake8 passing via pre-commit and GitHub Actions, we can't turn on pydoclint with either of these options without fully converting the codebase to one or the other.

We'd really like a configuration option that allows for type hints in either one, but not both - something like --arg-type-hints-in-either, which checks that type hints are defined in exactly one place, but doesn't care which one they are in. Gradually, we would convert our codebase to using type hints in signatures, and eventually use --arg-type-hints-in-signature=True and --arg-type-hints-in-docstring=False, taking care of any stragglers.

If this functionality is already available and I'm missing something, please let me know! If not, I'm happy to give it a whirl if you agree it would be nice to have.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.