Skip to content

Commit 94039b2

Browse files
authored
Add new config option for documenting star arguments (#206)
1 parent 22b9b37 commit 94039b2

File tree

10 files changed

+240
-16
lines changed

10 files changed

+240
-16
lines changed

.pre-commit-config.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,11 @@ repos:
3838
entry: python .pre_commit_helper_scripts/copy_readme.py
3939
language: system
4040
types: [python]
41+
42+
- repo: local
43+
hooks:
44+
- id: check_full_diff_in_changelog
45+
name: Check "full diff" exists in CHANGELOG.md
46+
entry: python .pre_commit_helper_scripts/check_full_diff_in_changelog.py
47+
language: python
48+
additional_dependencies: ["markdown-it-py"]
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import sys
2+
3+
from markdown_it import MarkdownIt
4+
5+
6+
def parseChangelog(filePath: str) -> tuple[bool, list[str]]:
7+
"""
8+
Parses the changelog file and ensures each version section has "Full diff".
9+
10+
Parameters
11+
----------
12+
filePath : str
13+
Path to the CHANGELOG.md file.
14+
15+
Returns
16+
-------
17+
bool
18+
True if all sections include a "Full diff", False otherwise.
19+
list[str]
20+
A list of version headers missing the "Full diff" section.
21+
"""
22+
with open(filePath, 'r', encoding='utf-8') as file:
23+
content = file.read()
24+
25+
# Parse the Markdown content
26+
md = MarkdownIt()
27+
tokens = md.parse(content)
28+
29+
versionHeaders = []
30+
missingFullDiff = []
31+
32+
# Iterate through parsed tokens to find version sections
33+
for i, token in enumerate(tokens):
34+
if token.type == 'heading_open' and token.tag == 'h2':
35+
# Extract version header text
36+
header = tokens[i + 1].content
37+
if header.startswith('[') and ' - ' in header:
38+
versionHeaders.append((header, i))
39+
40+
# Check each version section for "Full diff"
41+
for idx, (header, startIdx) in enumerate(versionHeaders):
42+
if header.startswith('[0.0.1]'):
43+
# The initial version shouldn't have a "Full diff" section.
44+
continue
45+
46+
endIdx = (
47+
versionHeaders[idx + 1][1]
48+
if idx + 1 < len(versionHeaders)
49+
else len(tokens)
50+
)
51+
sectionTokens = tokens[startIdx:endIdx]
52+
53+
# Check for "Full diff" in section content
54+
if not any(
55+
token.type == 'inline' and 'Full diff' in token.content
56+
for token in sectionTokens
57+
):
58+
missingFullDiff.append(header)
59+
60+
return len(missingFullDiff) == 0, missingFullDiff
61+
62+
63+
if __name__ == '__main__':
64+
filePath = 'CHANGELOG.md'
65+
isValid, missingSections = parseChangelog(filePath)
66+
67+
if isValid:
68+
print("All sections include a 'Full diff' section.")
69+
sys.exit(0)
70+
71+
print("The following sections are missing a 'Full diff':")
72+
for section in missingSections:
73+
print(section)
74+
75+
sys.exit(1)

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
# Change Log
22

3-
## [Unpublished]
3+
## [0.5.19] - 2025-01-12
44

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

916
## [0.5.18] - 2025-01-12
1017

docs/config_options.md

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,12 @@ page:
2828
- [15. `--should-document-private-class-attributes` (shortform: `-sdpca`, default: `False`)](#15---should-document-private-class-attributes-shortform--sdpca-default-false)
2929
- [16. `--treat-property-methods-as-class-attributes` (shortform: `-tpmaca`, default: `False`)](#16---treat-property-methods-as-class-attributes-shortform--tpmaca-default-false)
3030
- [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)
31-
- [18. `--baseline`](#18---baseline)
32-
- [19. `--generate-baseline` (default: `False`)](#19---generate-baseline-default-false)
33-
- [20. `--auto-regenerate-baseline` (shortform: `-arb`, default: `True`)](#20---auto-regenerate-baseline-shortform--arb-default-true)
34-
- [21. `--show-filenames-in-every-violation-message` (shortform: `-sfn`, default: `False`)](#21---show-filenames-in-every-violation-message-shortform--sfn-default-false)
35-
- [22. `--config` (default: `pyproject.toml`)](#22---config-default-pyprojecttoml)
31+
- [18. `--should-document-star-arguments` (shortform: `-sdsa`, default: `True`)](#18---should-document-star-arguments-shortform--sdsa-default-true)
32+
- [19. `--baseline`](#19---baseline)
33+
- [20. `--generate-baseline` (default: `False`)](#20---generate-baseline-default-false)
34+
- [21. `--auto-regenerate-baseline` (shortform: `-arb`, default: `True`)](#21---auto-regenerate-baseline-shortform--arb-default-true)
35+
- [22. `--show-filenames-in-every-violation-message` (shortform: `-sfn`, default: `False`)](#22---show-filenames-in-every-violation-message-shortform--sfn-default-false)
36+
- [23. `--config` (default: `pyproject.toml`)](#23---config-default-pyprojecttoml)
3637

3738
<!--TOC-->
3839

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

213-
## 18. `--baseline`
214+
## 18. `--should-document-star-arguments` (shortform: `-sdsa`, default: `True`)
215+
216+
If True, "star arguments" (such as `*args`, `**kwargs`, `**props`, etc.)
217+
in the function signature should be documented in the docstring. If False,
218+
they should not appear in the docstring.
219+
220+
## 19. `--baseline`
214221

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

235-
## 19. `--generate-baseline` (default: `False`)
242+
## 20. `--generate-baseline` (default: `False`)
236243

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

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

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

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

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

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

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

282-
## 22. `--config` (default: `pyproject.toml`)
289+
## 23. `--config` (default: `pyproject.toml`)
283290

284291
The full path of the .toml config file that contains the config options. Note
285292
that the command line options take precedence over the .toml file. Look at this

pydoclint/flake8_entry.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,19 @@ def add_options(cls, parser: Any) -> None: # noqa: D102
210210
' treated as instance attributes.'
211211
),
212212
)
213+
parser.add_option(
214+
'-sdsa',
215+
'--should-document-star-arguments',
216+
action='store',
217+
default='True',
218+
parse_from_config=True,
219+
help=(
220+
'If True, "star arguments" (such as *args, **kwargs,'
221+
' **props, etc.) in the function signature should be'
222+
' documented in the docstring. If False, they should not'
223+
' appear in the docstring.'
224+
),
225+
)
213226

214227
@classmethod
215228
def parse_options(cls, options: Any) -> None: # noqa: D102
@@ -245,6 +258,9 @@ def parse_options(cls, options: Any) -> None: # noqa: D102
245258
cls.only_attrs_with_ClassVar_are_treated_as_class_attrs = (
246259
options.only_attrs_with_ClassVar_are_treated_as_class_attrs
247260
)
261+
cls.should_document_star_arguments = (
262+
options.should_document_star_arguments
263+
)
248264
cls.style = options.style
249265

250266
def run(self) -> Generator[tuple[int, int, str, Any], None, None]:
@@ -322,6 +338,10 @@ def run(self) -> Generator[tuple[int, int, str, Any], None, None]:
322338
'--treat-property-methods-as-class-attributes',
323339
self.treat_property_methods_as_class_attributes,
324340
)
341+
shouldDocumentStarArguments = self._bool(
342+
'--should-document-star-arguments',
343+
self.should_document_star_arguments,
344+
)
325345

326346
if self.style not in {'numpy', 'google', 'sphinx'}:
327347
raise ValueError(
@@ -351,6 +371,7 @@ def run(self) -> Generator[tuple[int, int, str, Any], None, None]:
351371
treatPropertyMethodsAsClassAttributes=(
352372
treatPropertyMethodsAsClassAttributes
353373
),
374+
shouldDocumentStarArguments=shouldDocumentStarArguments,
354375
style=self.style,
355376
)
356377
v.visit(self._tree)

pydoclint/main.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,19 @@ def validateStyleValue(
249249
' treated as instance attributes.'
250250
),
251251
)
252+
@click.option(
253+
'-sdsa',
254+
'--should-document-star-arguments',
255+
type=bool,
256+
show_default=True,
257+
default=True,
258+
help=(
259+
'If True, "star arguments" (such as *args, **kwargs,'
260+
' **props, etc.) in the function signature should be'
261+
' documented in the docstring. If False, they should not'
262+
' appear in the docstring.'
263+
),
264+
)
252265
@click.option(
253266
'--baseline',
254267
type=click.Path(
@@ -351,6 +364,7 @@ def main( # noqa: C901
351364
require_return_section_when_returning_nothing: bool,
352365
require_yield_section_when_yielding_nothing: bool,
353366
only_attrs_with_classvar_are_treated_as_class_attrs: bool,
367+
should_document_star_arguments: bool,
354368
generate_baseline: bool,
355369
auto_regenerate_baseline: bool,
356370
baseline: str,
@@ -450,6 +464,7 @@ def main( # noqa: C901
450464
requireYieldSectionWhenYieldingNothing=(
451465
require_yield_section_when_yielding_nothing
452466
),
467+
shouldDocumentStarArguments=should_document_star_arguments,
453468
)
454469

455470
if generate_baseline:
@@ -585,6 +600,7 @@ def _checkPaths(
585600
onlyAttrsWithClassVarAreTreatedAsClassAttrs: bool = False,
586601
requireReturnSectionWhenReturningNothing: bool = False,
587602
requireYieldSectionWhenYieldingNothing: bool = False,
603+
shouldDocumentStarArguments: bool = True,
588604
quiet: bool = False,
589605
exclude: str = '',
590606
) -> dict[str, list[Violation]]:
@@ -644,6 +660,7 @@ def _checkPaths(
644660
requireYieldSectionWhenYieldingNothing=(
645661
requireYieldSectionWhenYieldingNothing
646662
),
663+
shouldDocumentStarArguments=shouldDocumentStarArguments,
647664
)
648665
allViolations[filename.as_posix()] = violationsInThisFile
649666

@@ -668,6 +685,7 @@ def _checkFile(
668685
onlyAttrsWithClassVarAreTreatedAsClassAttrs: bool = False,
669686
requireReturnSectionWhenReturningNothing: bool = False,
670687
requireYieldSectionWhenYieldingNothing: bool = False,
688+
shouldDocumentStarArguments: bool = True,
671689
) -> list[Violation]:
672690
if not filename.is_file(): # sometimes folder names can end with `.py`
673691
return []
@@ -722,6 +740,7 @@ def _checkFile(
722740
requireYieldSectionWhenYieldingNothing=(
723741
requireYieldSectionWhenYieldingNothing
724742
),
743+
shouldDocumentStarArguments=shouldDocumentStarArguments,
725744
)
726745
visitor.visit(tree)
727746
return visitor.violations

pydoclint/visitor.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def __init__(
6969
onlyAttrsWithClassVarAreTreatedAsClassAttrs: bool = False,
7070
requireReturnSectionWhenReturningNothing: bool = False,
7171
requireYieldSectionWhenYieldingNothing: bool = False,
72+
shouldDocumentStarArguments: bool = True,
7273
) -> None:
7374
self.style: str = style
7475
self.argTypeHintsInSignature: bool = argTypeHintsInSignature
@@ -96,6 +97,7 @@ def __init__(
9697
self.requireYieldSectionWhenYieldingNothing: bool = (
9798
requireYieldSectionWhenYieldingNothing
9899
)
100+
self.shouldDocumentStarArguments: bool = shouldDocumentStarArguments
99101

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

432+
if not self.shouldDocumentStarArguments:
433+
# This is "should not" rather than "need not", which means that
434+
# if this config option is set to False, there CANNOT be
435+
# documentation of star arguments in the docstring
436+
funcArgs = ArgList(
437+
[_ for _ in funcArgs.infoList if not _.name.startswith('*')]
438+
)
439+
430440
if docArgs.length == 0 and funcArgs.length == 0:
431441
return []
432442

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = pydoclint
3-
version = 0.5.18
3+
version = 0.5.19
44
description = A Python docstring linter that checks arguments, returns, yields, and raises sections
55
long_description = file: README.md
66
long_description_content_type = text/markdown
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# From: https://github.com/jsh9/pydoclint/issues/121
2+
3+
def function_1(arg1: int, *args: Any, **kwargs: Any) -> None:
4+
"""
5+
Do something
6+
7+
Parameters
8+
----------
9+
arg1 : int
10+
Arg 1
11+
"""
12+
pass
13+
14+
15+
def function_2(arg1: int, *args: Any, **kwargs: Any) -> None:
16+
"""
17+
Do something
18+
19+
Parameters
20+
----------
21+
arg1 : int
22+
Arg 1
23+
*args : Any
24+
Args
25+
**kwargs : Any
26+
Kwargs
27+
"""
28+
pass
29+
30+
31+
def function_3(arg1: int, *args: Any, **kwargs: Any) -> None:
32+
"""
33+
Do something
34+
35+
Parameters
36+
----------
37+
arg1 : int
38+
Arg 1
39+
**kwargs : Any
40+
Kwargs
41+
"""
42+
pass

0 commit comments

Comments
 (0)