Skip to content

Commit

Permalink
Add new config option for documenting star arguments (#206)
Browse files Browse the repository at this point in the history
  • Loading branch information
jsh9 authored Jan 13, 2025
1 parent 22b9b37 commit 94039b2
Show file tree
Hide file tree
Showing 10 changed files with 240 additions and 16 deletions.
8 changes: 8 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,11 @@ repos:
entry: python .pre_commit_helper_scripts/copy_readme.py
language: system
types: [python]

- repo: local
hooks:
- id: check_full_diff_in_changelog
name: Check "full diff" exists in CHANGELOG.md
entry: python .pre_commit_helper_scripts/check_full_diff_in_changelog.py
language: python
additional_dependencies: ["markdown-it-py"]
75 changes: 75 additions & 0 deletions .pre_commit_helper_scripts/check_full_diff_in_changelog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import sys

from markdown_it import MarkdownIt


def parseChangelog(filePath: str) -> tuple[bool, list[str]]:
"""
Parses the changelog file and ensures each version section has "Full diff".
Parameters
----------
filePath : str
Path to the CHANGELOG.md file.
Returns
-------
bool
True if all sections include a "Full diff", False otherwise.
list[str]
A list of version headers missing the "Full diff" section.
"""
with open(filePath, 'r', encoding='utf-8') as file:
content = file.read()

# Parse the Markdown content
md = MarkdownIt()
tokens = md.parse(content)

versionHeaders = []
missingFullDiff = []

# Iterate through parsed tokens to find version sections
for i, token in enumerate(tokens):
if token.type == 'heading_open' and token.tag == 'h2':
# Extract version header text
header = tokens[i + 1].content
if header.startswith('[') and ' - ' in header:
versionHeaders.append((header, i))

# Check each version section for "Full diff"
for idx, (header, startIdx) in enumerate(versionHeaders):
if header.startswith('[0.0.1]'):
# The initial version shouldn't have a "Full diff" section.
continue

endIdx = (
versionHeaders[idx + 1][1]
if idx + 1 < len(versionHeaders)
else len(tokens)
)
sectionTokens = tokens[startIdx:endIdx]

# Check for "Full diff" in section content
if not any(
token.type == 'inline' and 'Full diff' in token.content
for token in sectionTokens
):
missingFullDiff.append(header)

return len(missingFullDiff) == 0, missingFullDiff


if __name__ == '__main__':
filePath = 'CHANGELOG.md'
isValid, missingSections = parseChangelog(filePath)

if isValid:
print("All sections include a 'Full diff' section.")
sys.exit(0)

print("The following sections are missing a 'Full diff':")
for section in missingSections:
print(section)

sys.exit(1)
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
# Change Log

## [Unpublished]
## [0.5.19] - 2025-01-12

- Fixed
- False positive DOC405 and DOC201 when we have bare return statements
together with `yield` statements
- Added
- A new config option `--should-document-star-arguments` (if `False`, star
arguments such as `*args` and `**kwargs` should not be documented in the
docstring)
- A pre-commit step to check that "Full diff" is always added in CHANGELOG.md
- Full diff
- https://github.com/jsh9/pydoclint/compare/0.5.18...0.5.19

## [0.5.18] - 2025-01-12

Expand Down
27 changes: 17 additions & 10 deletions docs/config_options.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,12 @@ page:
- [15. `--should-document-private-class-attributes` (shortform: `-sdpca`, default: `False`)](#15---should-document-private-class-attributes-shortform--sdpca-default-false)
- [16. `--treat-property-methods-as-class-attributes` (shortform: `-tpmaca`, default: `False`)](#16---treat-property-methods-as-class-attributes-shortform--tpmaca-default-false)
- [17. `--only-attrs-with-ClassVar-are-treated-as-class-attrs` (shortform: `-oawcv`, default: `False)](#17---only-attrs-with-classvar-are-treated-as-class-attrs-shortform--oawcv-default-false)
- [18. `--baseline`](#18---baseline)
- [19. `--generate-baseline` (default: `False`)](#19---generate-baseline-default-false)
- [20. `--auto-regenerate-baseline` (shortform: `-arb`, default: `True`)](#20---auto-regenerate-baseline-shortform--arb-default-true)
- [21. `--show-filenames-in-every-violation-message` (shortform: `-sfn`, default: `False`)](#21---show-filenames-in-every-violation-message-shortform--sfn-default-false)
- [22. `--config` (default: `pyproject.toml`)](#22---config-default-pyprojecttoml)
- [18. `--should-document-star-arguments` (shortform: `-sdsa`, default: `True`)](#18---should-document-star-arguments-shortform--sdsa-default-true)
- [19. `--baseline`](#19---baseline)
- [20. `--generate-baseline` (default: `False`)](#20---generate-baseline-default-false)
- [21. `--auto-regenerate-baseline` (shortform: `-arb`, default: `True`)](#21---auto-regenerate-baseline-shortform--arb-default-true)
- [22. `--show-filenames-in-every-violation-message` (shortform: `-sfn`, default: `False`)](#22---show-filenames-in-every-violation-message-shortform--sfn-default-false)
- [23. `--config` (default: `pyproject.toml`)](#23---config-default-pyprojecttoml)

<!--TOC-->

Expand Down Expand Up @@ -210,7 +211,13 @@ If True, only the attributes whose type annotations are wrapped within
`ClassVar` (where `ClassVar` is imported from `typing`) are treated as class
attributes, and all other attributes are treated as instance attributes.

## 18. `--baseline`
## 18. `--should-document-star-arguments` (shortform: `-sdsa`, default: `True`)

If True, "star arguments" (such as `*args`, `**kwargs`, `**props`, etc.)
in the function signature should be documented in the docstring. If False,
they should not appear in the docstring.

## 19. `--baseline`

Baseline allows you to remember the current project state and then show only
new violations, ignoring old ones. This can be very useful when you'd like to
Expand All @@ -232,20 +239,20 @@ If `--generate-baseline` is not passed to _pydoclint_ (the default
is `False`), _pydoclint_ will read your baseline file, and ignore all
violations specified in that file.

## 19. `--generate-baseline` (default: `False`)
## 20. `--generate-baseline` (default: `False`)

Required to use with `--baseline` option. If `True`, generate the baseline file
that contains all current violations.

## 20. `--auto-regenerate-baseline` (shortform: `-arb`, default: `True`)
## 21. `--auto-regenerate-baseline` (shortform: `-arb`, default: `True`)

If it's set to True, _pydoclint_ will automatically regenerate the baseline
file every time you fix violations in the baseline and rerun _pydoclint_.

This saves you from having to manually regenerate the baseline file by setting
`--generate-baseline=True` and run _pydoclint_.

## 21. `--show-filenames-in-every-violation-message` (shortform: `-sfn`, default: `False`)
## 22. `--show-filenames-in-every-violation-message` (shortform: `-sfn`, default: `False`)

If False, in the terminal the violation messages are grouped by file names:

Expand Down Expand Up @@ -279,7 +286,7 @@ This can be convenient if you would like to click on each violation message and
go to the corresponding line in your IDE. (Note: not all terminal app offers
this functionality.)

## 22. `--config` (default: `pyproject.toml`)
## 23. `--config` (default: `pyproject.toml`)

The full path of the .toml config file that contains the config options. Note
that the command line options take precedence over the .toml file. Look at this
Expand Down
21 changes: 21 additions & 0 deletions pydoclint/flake8_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,19 @@ def add_options(cls, parser: Any) -> None: # noqa: D102
' treated as instance attributes.'
),
)
parser.add_option(
'-sdsa',
'--should-document-star-arguments',
action='store',
default='True',
parse_from_config=True,
help=(
'If True, "star arguments" (such as *args, **kwargs,'
' **props, etc.) in the function signature should be'
' documented in the docstring. If False, they should not'
' appear in the docstring.'
),
)

@classmethod
def parse_options(cls, options: Any) -> None: # noqa: D102
Expand Down Expand Up @@ -245,6 +258,9 @@ def parse_options(cls, options: Any) -> None: # noqa: D102
cls.only_attrs_with_ClassVar_are_treated_as_class_attrs = (
options.only_attrs_with_ClassVar_are_treated_as_class_attrs
)
cls.should_document_star_arguments = (
options.should_document_star_arguments
)
cls.style = options.style

def run(self) -> Generator[tuple[int, int, str, Any], None, None]:
Expand Down Expand Up @@ -322,6 +338,10 @@ def run(self) -> Generator[tuple[int, int, str, Any], None, None]:
'--treat-property-methods-as-class-attributes',
self.treat_property_methods_as_class_attributes,
)
shouldDocumentStarArguments = self._bool(
'--should-document-star-arguments',
self.should_document_star_arguments,
)

if self.style not in {'numpy', 'google', 'sphinx'}:
raise ValueError(
Expand Down Expand Up @@ -351,6 +371,7 @@ def run(self) -> Generator[tuple[int, int, str, Any], None, None]:
treatPropertyMethodsAsClassAttributes=(
treatPropertyMethodsAsClassAttributes
),
shouldDocumentStarArguments=shouldDocumentStarArguments,
style=self.style,
)
v.visit(self._tree)
Expand Down
19 changes: 19 additions & 0 deletions pydoclint/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,19 @@ def validateStyleValue(
' treated as instance attributes.'
),
)
@click.option(
'-sdsa',
'--should-document-star-arguments',
type=bool,
show_default=True,
default=True,
help=(
'If True, "star arguments" (such as *args, **kwargs,'
' **props, etc.) in the function signature should be'
' documented in the docstring. If False, they should not'
' appear in the docstring.'
),
)
@click.option(
'--baseline',
type=click.Path(
Expand Down Expand Up @@ -351,6 +364,7 @@ def main( # noqa: C901
require_return_section_when_returning_nothing: bool,
require_yield_section_when_yielding_nothing: bool,
only_attrs_with_classvar_are_treated_as_class_attrs: bool,
should_document_star_arguments: bool,
generate_baseline: bool,
auto_regenerate_baseline: bool,
baseline: str,
Expand Down Expand Up @@ -450,6 +464,7 @@ def main( # noqa: C901
requireYieldSectionWhenYieldingNothing=(
require_yield_section_when_yielding_nothing
),
shouldDocumentStarArguments=should_document_star_arguments,
)

if generate_baseline:
Expand Down Expand Up @@ -585,6 +600,7 @@ def _checkPaths(
onlyAttrsWithClassVarAreTreatedAsClassAttrs: bool = False,
requireReturnSectionWhenReturningNothing: bool = False,
requireYieldSectionWhenYieldingNothing: bool = False,
shouldDocumentStarArguments: bool = True,
quiet: bool = False,
exclude: str = '',
) -> dict[str, list[Violation]]:
Expand Down Expand Up @@ -644,6 +660,7 @@ def _checkPaths(
requireYieldSectionWhenYieldingNothing=(
requireYieldSectionWhenYieldingNothing
),
shouldDocumentStarArguments=shouldDocumentStarArguments,
)
allViolations[filename.as_posix()] = violationsInThisFile

Expand All @@ -668,6 +685,7 @@ def _checkFile(
onlyAttrsWithClassVarAreTreatedAsClassAttrs: bool = False,
requireReturnSectionWhenReturningNothing: bool = False,
requireYieldSectionWhenYieldingNothing: bool = False,
shouldDocumentStarArguments: bool = True,
) -> list[Violation]:
if not filename.is_file(): # sometimes folder names can end with `.py`
return []
Expand Down Expand Up @@ -722,6 +740,7 @@ def _checkFile(
requireYieldSectionWhenYieldingNothing=(
requireYieldSectionWhenYieldingNothing
),
shouldDocumentStarArguments=shouldDocumentStarArguments,
)
visitor.visit(tree)
return visitor.violations
Expand Down
10 changes: 10 additions & 0 deletions pydoclint/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def __init__(
onlyAttrsWithClassVarAreTreatedAsClassAttrs: bool = False,
requireReturnSectionWhenReturningNothing: bool = False,
requireYieldSectionWhenYieldingNothing: bool = False,
shouldDocumentStarArguments: bool = True,
) -> None:
self.style: str = style
self.argTypeHintsInSignature: bool = argTypeHintsInSignature
Expand Down Expand Up @@ -96,6 +97,7 @@ def __init__(
self.requireYieldSectionWhenYieldingNothing: bool = (
requireYieldSectionWhenYieldingNothing
)
self.shouldDocumentStarArguments: bool = shouldDocumentStarArguments

self.parent: ast.AST = ast.Pass() # keep track of parent node
self.violations: list[Violation] = []
Expand Down Expand Up @@ -427,6 +429,14 @@ def checkArguments( # noqa: C901
[_ for _ in funcArgs.infoList if set(_.name) != {'_'}]
)

if not self.shouldDocumentStarArguments:
# This is "should not" rather than "need not", which means that
# if this config option is set to False, there CANNOT be
# documentation of star arguments in the docstring
funcArgs = ArgList(
[_ for _ in funcArgs.infoList if not _.name.startswith('*')]
)

if docArgs.length == 0 and funcArgs.length == 0:
return []

Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = pydoclint
version = 0.5.18
version = 0.5.19
description = A Python docstring linter that checks arguments, returns, yields, and raises sections
long_description = file: README.md
long_description_content_type = text/markdown
Expand Down
42 changes: 42 additions & 0 deletions tests/data/edge_cases/24_star_arguments/numpy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# From: https://github.com/jsh9/pydoclint/issues/121

def function_1(arg1: int, *args: Any, **kwargs: Any) -> None:
"""
Do something
Parameters
----------
arg1 : int
Arg 1
"""
pass


def function_2(arg1: int, *args: Any, **kwargs: Any) -> None:
"""
Do something
Parameters
----------
arg1 : int
Arg 1
*args : Any
Args
**kwargs : Any
Kwargs
"""
pass


def function_3(arg1: int, *args: Any, **kwargs: Any) -> None:
"""
Do something
Parameters
----------
arg1 : int
Arg 1
**kwargs : Any
Kwargs
"""
pass
Loading

0 comments on commit 94039b2

Please sign in to comment.