Skip to content

Commit 8c10818

Browse files
authored
Fix how multiple __init__() are recognized (#273)
1 parent 51435e3 commit 8c10818

12 files changed

Lines changed: 349 additions & 32 deletions

File tree

CHANGELOG.md

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

3-
## [Unpublished]
3+
## [0.8.2] - 2025-11-21
44

55
- Added
66
- Ability to partially match violation codes in inline `noqa` in the native
77
mode (which _flake8_ already supports)
8+
- Fixed
9+
- A bug: when there are more than one `__init__()` in a class (overloaded),
10+
the first `__init__()` is incorrectly recognized as the "right" one. (The
11+
last `__init__()` should be considered the right one.)
12+
- Full diff
13+
- https://github.com/jsh9/pydoclint/compare/0.8.1...0.8.2
814

915
## [0.8.1] - 2025-11-03
1016

pydoclint/utils/generic.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,36 @@ def getDocstring(node: ClassOrFunctionDef) -> str:
109109
return '' if docstring_ is None else docstring_
110110

111111

112+
def isLastConstructor(
113+
node: FuncOrAsyncFuncDef,
114+
parentClass: ast.ClassDef,
115+
) -> bool:
116+
"""
117+
Return True if the given __init__() is the last constructor in the class.
118+
119+
Overload stubs typically appear before the real implementation; by
120+
detecting whether another constructor follows, we can ignore those stubs so
121+
only the final body gets linted.
122+
"""
123+
hasSeenNode = False
124+
for child in parentClass.body:
125+
if child is node:
126+
hasSeenNode = True
127+
continue
128+
129+
if not hasSeenNode:
130+
continue
131+
132+
isConstructor = (
133+
isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef))
134+
and child.name == '__init__'
135+
)
136+
if isConstructor:
137+
return False
138+
139+
return True
140+
141+
112142
def generateClassMsgPrefix(node: ast.ClassDef, *, appendColon: bool) -> str:
113143
"""
114144
Generate violation message prefix for classes.

pydoclint/visitor.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
generateClassMsgPrefix,
2222
generateFuncMsgPrefix,
2323
getDocstring,
24+
isLastConstructor,
2425
)
2526
from pydoclint.utils.method_type import MethodType
2627
from pydoclint.utils.parse_docstring import (
@@ -172,6 +173,18 @@ def visit_FunctionDef(self, node: FuncOrAsyncFuncDef) -> None: # noqa: D102, PL
172173
parent_, ast.ClassDef
173174
)
174175

176+
if (
177+
isClassConstructor
178+
and isinstance(parent_, ast.ClassDef)
179+
and not isLastConstructor(node=node, parentClass=parent_)
180+
):
181+
# Multiple __init__ definitions can exist when using overload stubs
182+
# but only the last implementation represents the real constructor
183+
# body, so earlier ones are skipped to avoid reporting bogus
184+
# violations.
185+
self.parent = parent_ # restore
186+
return
187+
175188
docstring: str = getDocstring(node)
176189

177190
self.isAbstractMethod = checkIsAbstractMethod(node)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2", "wheel"]
44

55
[project]
66
name = "pydoclint"
7-
version = "0.8.1"
7+
version = "0.8.2"
88
dependencies = [
99
"click>=8.1.0",
1010
"docstring_parser_fork>=0.0.12",
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from __future__ import annotations
2+
3+
from typing import overload
4+
5+
6+
class Example:
7+
"""Example class docstring."""
8+
9+
@overload
10+
def __init__(self, value: str) -> None:
11+
"""Incorrect docstring that should be ignored.
12+
13+
Args:
14+
wrong (int): This argument does not exist in the signature.
15+
"""
16+
17+
def __init__(self, value: str, size: int) -> None:
18+
"""Actual implementation docstring.
19+
20+
Args:
21+
value (str): Primary value.
22+
size (int): Size of something important.
23+
"""
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from __future__ import annotations
2+
3+
from typing import overload
4+
5+
6+
class Example:
7+
"""Example class docstring."""
8+
9+
@overload
10+
def __init__(self, value: str) -> None:
11+
"""Incorrect docstring that should be ignored.
12+
13+
Parameters
14+
----------
15+
wrong : int
16+
This argument does not exist in the signature.
17+
"""
18+
19+
def __init__(self, value: str, size: int) -> None:
20+
"""Actual implementation docstring.
21+
22+
Parameters
23+
----------
24+
value : str
25+
Primary value.
26+
size : int
27+
Size of something important.
28+
"""
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from __future__ import annotations
2+
3+
from typing import overload
4+
5+
6+
class Example:
7+
"""Example class docstring."""
8+
9+
@overload
10+
def __init__(self, value: str) -> None:
11+
"""Incorrect docstring that should be ignored.
12+
13+
:param wrong: This argument does not exist in the signature.
14+
:type wrong: int
15+
"""
16+
17+
def __init__(self, value: str, size: int) -> None:
18+
"""Actual implementation docstring.
19+
20+
:param value: Primary value.
21+
:param size: Size of something important.
22+
:type value: str
23+
:type size: int
24+
"""
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from __future__ import annotations
2+
3+
from typing import overload
4+
5+
# This case comes from: https://github.com/jsh9/pydoclint/issues/255
6+
class FooWrite(SomeObject):
7+
"""
8+
The Foo write class.
9+
10+
Args:
11+
bar (str | None): Description of bar.
12+
baz (int | None): Description of baz.
13+
14+
Raises:
15+
ValueError: If both bar and baz are None.
16+
"""
17+
18+
@overload
19+
def __init__(self, bar: None, baz: None) -> NoReturn: ...
20+
21+
@overload
22+
def __init__(self, bar: str | None, baz: int | None) -> None: ...
23+
24+
def __init__(self, bar: str | None, baz: int | None) -> None:
25+
if bar is None and baz is None:
26+
raise ValueError
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from __future__ import annotations
2+
3+
from typing import overload
4+
5+
# This case comes from: https://github.com/jsh9/pydoclint/issues/255
6+
class FooWrite(SomeObject):
7+
"""
8+
The Foo write class.
9+
10+
Parameters
11+
----------
12+
bar : str | None
13+
Description of bar.
14+
baz : int | None
15+
Description of baz.
16+
17+
Raises
18+
------
19+
ValueError
20+
If both bar and baz are None.
21+
"""
22+
23+
@overload
24+
def __init__(self, bar: None, baz: None) -> NoReturn: ...
25+
26+
@overload
27+
def __init__(self, bar: str | None, baz: int | None) -> None: ...
28+
29+
def __init__(self, bar: str | None, baz: int | None) -> None:
30+
if bar is None and baz is None:
31+
raise ValueError
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from __future__ import annotations
2+
3+
from typing import overload
4+
5+
# This case comes from: https://github.com/jsh9/pydoclint/issues/255
6+
class FooWrite(SomeObject):
7+
"""
8+
The Foo write class.
9+
10+
:param bar: Description of bar.
11+
:param baz: Description of baz.
12+
:type bar: str | None
13+
:type baz: int | None
14+
:raises ValueError: If both bar and baz are None.
15+
"""
16+
17+
@overload
18+
def __init__(self, bar: None, baz: None) -> NoReturn: ...
19+
20+
@overload
21+
def __init__(self, bar: str | None, baz: int | None) -> None: ...
22+
23+
def __init__(self, bar: str | None, baz: int | None) -> None:
24+
if bar is None and baz is None:
25+
raise ValueError

0 commit comments

Comments
 (0)