Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

- Changed
- Dropped support for Python 3.8
- Added
- Added static type checking using `mypy`

## [0.5.11] - 2024-12-14

Expand Down
6 changes: 4 additions & 2 deletions pydoclint/flake8_entry.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# mypy: disable-error-code=attr-defined

import ast
import importlib.metadata as importlib_metadata
from typing import Any, Generator, Tuple
Expand All @@ -15,7 +17,7 @@ def __init__(self, tree: ast.AST) -> None:
self._tree = tree

@classmethod
def add_options(cls, parser): # noqa: D102
def add_options(cls, parser: Any) -> None: # noqa: D102
parser.add_option(
'--style',
action='store',
Expand Down Expand Up @@ -196,7 +198,7 @@ def add_options(cls, parser): # noqa: D102
)

@classmethod
def parse_options(cls, options): # noqa: D102
def parse_options(cls, options: Any) -> None: # noqa: D102
cls.type_hints_in_signature = options.type_hints_in_signature
cls.type_hints_in_docstring = options.type_hints_in_docstring
cls.arg_type_hints_in_signature = options.arg_type_hints_in_signature
Expand Down
2 changes: 1 addition & 1 deletion pydoclint/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ def main( # noqa: C901
ctx.exit(1)

# it means users supply this option
if require_return_section_when_returning_none != 'None':
if require_return_section_when_returning_none != 'None': # type:ignore[comparison-overlap]
click.echo(
click.style(
''.join([
Expand Down
6 changes: 3 additions & 3 deletions pydoclint/parse_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,10 @@ def findCommonParentFolder(
makeAbsolute: bool = True, # allow makeAbsolute=False just for testing
) -> Path:
"""Find the common parent folder of the given ``paths``"""
paths = [Path(path) for path in paths]
paths_: Sequence[Path] = [Path(path) for path in paths]

common_parent = paths[0]
for path in paths[1:]:
common_parent = paths_[0]
for path in paths_[1:]:
if len(common_parent.parts) > len(path.parts):
common_parent, path = path, common_parent

Expand Down
35 changes: 23 additions & 12 deletions pydoclint/utils/arg.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def __repr__(self) -> str:
def __str__(self) -> str:
return f'{self.name}: {self.typeHint}'

def __eq__(self, other: 'Arg') -> bool:
def __eq__(self, other: object) -> bool:
if not isinstance(other, Arg):
return False

Expand Down Expand Up @@ -84,17 +84,22 @@ def fromDocstringAttr(cls, attr: DocstringAttr) -> 'Arg':
@classmethod
def fromAstArg(cls, astArg: ast.arg) -> 'Arg':
"""Construct an Arg object from a Python AST argument object"""
anno = astArg.annotation
typeHint: str = '' if anno is None else unparseName(anno)
anno: Optional[ast.expr] = astArg.annotation
typeHint: Optional[str] = '' if anno is None else unparseName(anno)
assert typeHint is not None # to help mypy better understand type
return Arg(name=astArg.arg, typeHint=typeHint)

@classmethod
def fromAstAnnAssign(cls, astAnnAssign: ast.AnnAssign) -> 'Arg':
"""Construct an Arg object from a Python ast.AnnAssign object"""
return Arg(
name=unparseName(astAnnAssign.target),
typeHint=unparseName(astAnnAssign.annotation),
)
unparsedArgName = unparseName(astAnnAssign.target)
unparsedTypeHint = unparseName(astAnnAssign.annotation)

# These assertions are to help mypy better interpret types
assert unparsedArgName is not None
assert unparsedTypeHint is not None

return Arg(name=unparsedArgName, typeHint=unparsedTypeHint)

@classmethod
def _str(cls, typeName: Optional[str]) -> str:
Expand All @@ -113,12 +118,12 @@ def _typeHintsEq(cls, hint1: str, hint2: str) -> bool:
# >>> "ghi",
# >>> ]
try:
hint1_: str = unparseName(ast.parse(stripQuotes(hint1)))
hint1_: str = unparseName(ast.parse(stripQuotes(hint1))) # type:ignore[arg-type,assignment]
except SyntaxError:
hint1_ = hint1

try:
hint2_: str = unparseName(ast.parse(stripQuotes(hint2)))
hint2_: str = unparseName(ast.parse(stripQuotes(hint2))) # type:ignore[arg-type,assignment]
except SyntaxError:
hint2_ = hint2

Expand Down Expand Up @@ -156,7 +161,7 @@ def __repr__(self) -> str:
def __str__(self) -> str:
return '[' + ', '.join(str(_) for _ in self.infoList) + ']'

def __eq__(self, other: 'ArgList') -> bool:
def __eq__(self, other: object) -> bool:
if not isinstance(other, ArgList):
return False

Expand Down Expand Up @@ -221,7 +226,9 @@ def fromAstAssign(cls, astAssign: ast.Assign) -> 'ArgList':
elif isinstance(target, ast.Name): # such as `a = 1` or `a = b = 2`
infoList.append(Arg(name=target.id, typeHint=''))
elif isinstance(target, ast.Attribute): # e.g., uvw.xyz = 1
infoList.append(Arg(name=unparseName(target), typeHint=''))
unparsedTarget: Optional[str] = unparseName(target)
assert unparsedTarget is not None # to help mypy understand type
infoList.append(Arg(name=unparsedTarget, typeHint=''))
else:
raise EdgeCaseError(
f'astAssign.targets[{i}] is of type {type(target)}'
Expand Down Expand Up @@ -303,7 +310,11 @@ def findArgsWithDifferentTypeHints(self, other: 'ArgList') -> List[Arg]:

return result

def subtract(self, other: 'ArgList', checkTypeHint=True) -> Set[Arg]:
def subtract(
self,
other: 'ArgList',
checkTypeHint: bool = True,
) -> Set[Arg]:
"""Find the args that are in this object but not in `other`."""
if checkTypeHint:
return set(self.infoList) - set(other.infoList)
Expand Down
9 changes: 0 additions & 9 deletions pydoclint/utils/astTypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,6 @@

FuncOrAsyncFuncDef = Union[ast.AsyncFunctionDef, ast.FunctionDef]
ClassOrFunctionDef = Union[ast.ClassDef, ast.AsyncFunctionDef, ast.FunctionDef]
AnnotationType = Union[
ast.Name,
ast.Subscript,
ast.Index,
ast.Tuple,
ast.Constant,
ast.BinOp,
ast.Attribute,
]

LegacyBlockTypes = [
ast.If,
Expand Down
17 changes: 9 additions & 8 deletions pydoclint/utils/doc.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import pprint
from typing import Any, List
from typing import Any, List, Optional, Union

from docstring_parser.common import (
Docstring,
Expand All @@ -23,6 +23,7 @@ def __init__(self, docstring: str, style: str = 'numpy') -> None:
self.docstring = docstring
self.style = style

parser: Union[NumpydocParser, GoogleParser]
if style == 'numpy':
parser = NumpydocParser()
self.parsed = parser.parse(docstring)
Expand All @@ -38,7 +39,7 @@ def __repr__(self) -> str:
return pprint.pformat(self.__dict__, indent=2)

@property
def isShortDocstring(self) -> bool:
def isShortDocstring(self) -> bool: # type:ignore[return]
"""Is the docstring a short one (containing only a summary)"""
if self.style in {'google', 'numpy', 'sphinx'}:
# API documentation:
Expand All @@ -60,32 +61,32 @@ def isShortDocstring(self) -> bool:
self._raiseException() # noqa: R503

@property
def argList(self) -> ArgList:
def argList(self) -> ArgList: # type:ignore[return]
"""The argument info in the docstring, presented as an ArgList"""
if self.style in {'google', 'numpy', 'sphinx'}:
return ArgList.fromDocstringParam(self.parsed.params)

self._raiseException() # noqa: R503

@property
def attrList(self) -> ArgList:
def attrList(self) -> ArgList: # type:ignore[return]
"""The attributes info in the docstring, presented as an ArgList"""
if self.style in {'google', 'numpy', 'sphinx'}:
return ArgList.fromDocstringAttr(self.parsed.attrs)

self._raiseException() # noqa: R503

@property
def hasReturnsSection(self) -> bool:
def hasReturnsSection(self) -> bool: # type:ignore[return]
"""Whether the docstring has a 'Returns' section"""
if self.style in {'google', 'numpy', 'sphinx'}:
retSection: DocstringReturns = self.parsed.returns
retSection: Optional[DocstringReturns] = self.parsed.returns
return retSection is not None and not retSection.is_generator

self._raiseException() # noqa: R503

@property
def hasYieldsSection(self) -> bool:
def hasYieldsSection(self) -> bool: # type:ignore[return]
"""Whether the docstring has a 'Yields' section"""
if self.style in {'google', 'numpy', 'sphinx'}:
yieldSection: DocstringYields = self.parsed.yields
Expand All @@ -94,7 +95,7 @@ def hasYieldsSection(self) -> bool:
self._raiseException() # noqa: R503

@property
def hasRaisesSection(self) -> bool:
def hasRaisesSection(self) -> bool: # type:ignore[return]
"""Whether the docstring has a 'Raises' section"""
if self.style in {'google', 'numpy', 'sphinx'}:
return len(self.parsed.raises) > 0
Expand Down
27 changes: 19 additions & 8 deletions pydoclint/utils/generic.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
from __future__ import annotations

import ast
import copy
import re
from typing import List, Match, Optional, Tuple, Union
from typing import TYPE_CHECKING, List, Match, Optional, Tuple, Union

from pydoclint.utils.astTypes import ClassOrFunctionDef, FuncOrAsyncFuncDef
from pydoclint.utils.method_type import MethodType
from pydoclint.utils.violation import Violation

if TYPE_CHECKING:
from pydoclint.utils.arg import Arg, ArgList


def collectFuncArgs(node: FuncOrAsyncFuncDef) -> List[ast.arg]:
"""
Expand Down Expand Up @@ -70,7 +75,7 @@ def getFunctionId(node: FuncOrAsyncFuncDef) -> Tuple[int, int, str]:
return node.lineno, node.col_offset, node.name


def detectMethodType(node: ast.FunctionDef) -> MethodType:
def detectMethodType(node: FuncOrAsyncFuncDef) -> MethodType:
"""
Detect whether the function def is an instance method,
a classmethod, or a staticmethod.
Expand Down Expand Up @@ -159,11 +164,17 @@ def getNodeName(node: ast.AST) -> str:
if node is None:
return ''

return node.name if 'name' in node.__dict__ else ''
return getattr(node, 'name', '')


def stringStartsWith(string: str, substrings: Tuple[str, ...]) -> bool:
def stringStartsWith(
string: Optional[str],
substrings: Tuple[str, ...],
) -> bool:
"""Check whether the string starts with any of the substrings"""
if string is None:
return False

for substring in substrings:
if string.startswith(substring):
return True
Expand Down Expand Up @@ -202,11 +213,11 @@ def _replacer(match: Match[str]) -> str:
def appendArgsToCheckToV105(
*,
original_v105: Violation,
funcArgs: 'ArgList', # noqa: F821
docArgs: 'ArgList', # noqa: F821
funcArgs: ArgList,
docArgs: ArgList,
) -> Violation:
"""Append the arg names to check to the error message of v105 or v605"""
argsToCheck: List['Arg'] = funcArgs.findArgsWithDifferentTypeHints(docArgs) # noqa: F821
argsToCheck: List[Arg] = funcArgs.findArgsWithDifferentTypeHints(docArgs)
argNames: str = ', '.join(_.name for _ in argsToCheck)
return original_v105.appendMoreMsg(moreMsg=argNames)

Expand Down Expand Up @@ -244,4 +255,4 @@ def getFullAttributeName(node: Union[ast.Attribute, ast.Name]) -> str:
if isinstance(node, ast.Name):
return node.id

return getFullAttributeName(node.value) + '.' + node.attr
return getFullAttributeName(node.value) + '.' + node.attr # type:ignore[arg-type]
14 changes: 9 additions & 5 deletions pydoclint/utils/return_anno.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ def decompose(self) -> List[str]:
When the annotation string has strange values
"""
if self._isTuple(): # noqa: R506
assert self.annotation is not None # to help mypy understand type

if not self.annotation.endswith(']'):
raise EdgeCaseError('Return annotation not ending with `]`')

Expand All @@ -49,23 +51,25 @@ def decompose(self) -> List[str]:

insideTuple: str = self.annotation[6:-1]
if insideTuple.endswith('...'): # like this: Tuple[int, ...]
return [self.annotation] # b/c we don't know the tuple's length
# because we don't know the tuple's length
return [self.annotation]

parsedBody0: ast.Expr = ast.parse(insideTuple).body[0]
parsedBody0: ast.Expr = ast.parse(insideTuple).body[0] # type:ignore[assignment]
if isinstance(parsedBody0.value, ast.Name): # like this: Tuple[int]
return [insideTuple]

if isinstance(parsedBody0.value, ast.Tuple): # like Tuple[int, str]
elts: List = parsedBody0.value.elts
return [unparseName(_) for _ in elts]
elts: List[ast.expr] = parsedBody0.value.elts
return [unparseName(_) for _ in elts] # type:ignore[misc]

raise EdgeCaseError('decompose(): This should not have happened')
else:
return self.putAnnotationInList()

def _isTuple(self) -> bool:
try:
annoHead = ast.parse(self.annotation).body[0].value.value.id
assert self.annotation is not None # to help mypy understand type
annoHead = ast.parse(self.annotation).body[0].value.value.id # type:ignore[attr-defined]
return annoHead in {'tuple', 'Tuple'}
except Exception:
return False
Expand Down
Loading
Loading