diff --git a/CHANGELOG.md b/CHANGELOG.md index 92b8333..437ce3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Changed - Dropped support for Python 3.8 + - Use "modern" type annotation, such as `list` and `str | None` - Added - Added static type checking using `mypy` diff --git a/pydoclint/baseline.py b/pydoclint/baseline.py index 427d0d5..a0b375a 100644 --- a/pydoclint/baseline.py +++ b/pydoclint/baseline.py @@ -1,6 +1,7 @@ +from __future__ import annotations + from itertools import groupby from pathlib import Path -from typing import Dict, List, Set, Tuple from pydoclint.utils.violation import Violation @@ -10,7 +11,7 @@ def generateBaseline( - violationsInAllFiles: Dict[str, List[Violation]], path: Path + violationsInAllFiles: dict[str, list[Violation]], path: Path ) -> None: """Generate baseline file based of passed violations.""" with path.open('w', encoding='utf-8') as baseline: @@ -23,7 +24,7 @@ def generateBaseline( baseline.write(f'{SEPARATOR}') -def parseBaseline(path: Path) -> Dict[str, Set[str]]: +def parseBaseline(path: Path) -> dict[str, set[str]]: """Parse baseline file.""" with path.open('r', encoding='utf-8') as baseline: parsed: dict[str, set[str]] = {} @@ -41,15 +42,15 @@ def parseBaseline(path: Path) -> Dict[str, Set[str]]: def removeBaselineViolations( - baseline: Dict[str, Set[str]], - violationsInAllFiles: Dict[str, List[Violation]], -) -> Tuple[bool, Dict[str, List[Violation]]]: + baseline: dict[str, set[str]], + violationsInAllFiles: dict[str, list[Violation]], +) -> tuple[bool, dict[str, list[Violation]]]: """ Remove from the violation dictionary the already existing violations specified in the baseline file. """ baselineRegenerationNeeded = False - clearedViolationsAllFiles: Dict[str, List[Violation]] = {} + clearedViolationsAllFiles: dict[str, list[Violation]] = {} for file, violations in violationsInAllFiles.items(): if oldViolations := baseline.get(file): newViolations = [] diff --git a/pydoclint/flake8_entry.py b/pydoclint/flake8_entry.py index 74558f2..cab2f1a 100644 --- a/pydoclint/flake8_entry.py +++ b/pydoclint/flake8_entry.py @@ -1,8 +1,9 @@ # mypy: disable-error-code=attr-defined +from __future__ import annotations import ast import importlib.metadata as importlib_metadata -from typing import Any, Generator, Tuple +from typing import Any, Generator from pydoclint.visitor import Visitor @@ -230,7 +231,7 @@ def parse_options(cls, options: Any) -> None: # noqa: D102 ) cls.style = options.style - def run(self) -> Generator[Tuple[int, int, str, Any], None, None]: + def run(self) -> Generator[tuple[int, int, str, Any], None, None]: """Run the linter and yield the violation information""" if self.type_hints_in_docstring != 'None': # user supplies this option raise ValueError( diff --git a/pydoclint/main.py b/pydoclint/main.py index 7a0b8d5..04dbc55 100644 --- a/pydoclint/main.py +++ b/pydoclint/main.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import ast import logging import re from pathlib import Path -from typing import Dict, List, Optional, Tuple import click @@ -28,8 +29,8 @@ def validateStyleValue( context: click.Context, param: click.Parameter, - value: Optional[str], -) -> Optional[str]: + value: str | None, +) -> str | None: """Validate the value of the 'style' option""" if value not in {'numpy', 'google', 'sphinx'}: raise click.BadParameter( @@ -306,7 +307,7 @@ def main( # noqa: C901 quiet: bool, exclude: str, style: str, - paths: Tuple[str, ...], + paths: tuple[str, ...], type_hints_in_signature: str, type_hints_in_docstring: str, arg_type_hints_in_signature: bool, @@ -327,7 +328,7 @@ def main( # noqa: C901 generate_baseline: bool, baseline: str, show_filenames_in_every_violation_message: bool, - config: Optional[str], # don't remove it b/c it's required by `click` + config: str | None, # don't remove it b/c it's required by `click` ) -> None: """Command-line entry point of pydoclint""" logging.basicConfig(level=logging.WARN if quiet else logging.INFO) @@ -392,7 +393,7 @@ def main( # noqa: C901 ) ctx.exit(1) - violationsInAllFiles: Dict[str, List[Violation]] = _checkPaths( + violationsInAllFiles: dict[str, list[Violation]] = _checkPaths( quiet=quiet, exclude=exclude, style=style, @@ -516,7 +517,7 @@ def main( # noqa: C901 def _checkPaths( - paths: Tuple[str, ...], + paths: tuple[str, ...], style: str = 'numpy', argTypeHintsInSignature: bool = True, argTypeHintsInDocstring: bool = True, @@ -534,8 +535,8 @@ def _checkPaths( requireYieldSectionWhenYieldingNothing: bool = False, quiet: bool = False, exclude: str = '', -) -> Dict[str, List[Violation]]: - filenames: List[Path] = [] +) -> dict[str, list[Violation]]: + filenames: list[Path] = [] if not quiet: skipMsg = f'Skipping files that match this pattern: {exclude}' @@ -552,7 +553,7 @@ def _checkPaths( elif path.is_dir(): filenames.extend(sorted(path.rglob('*.py'))) - allViolations: Dict[str, List[Violation]] = {} + allViolations: dict[str, list[Violation]] = {} for filename in filenames: if excludePattern.search(filename.as_posix()): @@ -563,7 +564,7 @@ def _checkPaths( click.style(filename, fg='cyan', bold=True), err=echoAsError ) - violationsInThisFile: List[Violation] = _checkFile( + violationsInThisFile: list[Violation] = _checkFile( filename, style=style, argTypeHintsInSignature=argTypeHintsInSignature, @@ -611,7 +612,7 @@ def _checkFile( treatPropertyMethodsAsClassAttributes: bool = False, requireReturnSectionWhenReturningNothing: bool = False, requireYieldSectionWhenYieldingNothing: bool = False, -) -> List[Violation]: +) -> list[Violation]: if not filename.is_file(): # sometimes folder names can end with `.py` return [] diff --git a/pydoclint/parse_config.py b/pydoclint/parse_config.py index 25c4847..1a099b0 100644 --- a/pydoclint/parse_config.py +++ b/pydoclint/parse_config.py @@ -1,7 +1,9 @@ +from __future__ import annotations + import logging import sys from pathlib import Path -from typing import Any, Dict, Optional, Sequence +from typing import Any, Sequence import click @@ -14,8 +16,8 @@ def injectDefaultOptionsFromUserSpecifiedTomlFilePath( ctx: click.Context, param: click.Parameter, - value: Optional[str], -) -> Optional[str]: + value: str | None, +) -> str | None: """ Inject default objects from user-specified .toml file path. @@ -25,13 +27,13 @@ def injectDefaultOptionsFromUserSpecifiedTomlFilePath( The "click" context param : click.Parameter The "click" parameter; not used in this function; just a placeholder - value : Optional[str] + value : str | None The full path of the .toml file. (It needs to be named ``value`` so that ``click`` can correctly use it as a callback function.) Returns ------- - Optional[str] + str | None The full path of the .toml file """ if not value: @@ -43,7 +45,7 @@ def injectDefaultOptionsFromUserSpecifiedTomlFilePath( return value -def parseToml(paths: Optional[Sequence[str]]) -> Dict[str, Any]: +def parseToml(paths: Sequence[str] | None) -> dict[str, Any]: """Parse the pyproject.toml located in the common parent of ``paths``""" if paths is None: return {} @@ -56,7 +58,7 @@ def parseToml(paths: Optional[Sequence[str]]) -> Dict[str, Any]: return parseOneTomlFile(tomlFilename) -def parseOneTomlFile(tomlFilename: Path) -> Dict[str, Any]: +def parseOneTomlFile(tomlFilename: Path) -> dict[str, Any]: """Parse a .toml file""" if not tomlFilename.exists(): logging.info(f'File "{tomlFilename}" does not exist; nothing to load.') @@ -105,9 +107,9 @@ def findCommonParentFolder( return common_parent -def updateCtxDefaultMap(ctx: click.Context, config: Dict[str, Any]) -> None: +def updateCtxDefaultMap(ctx: click.Context, config: dict[str, Any]) -> None: """Update the ``click`` context default map with the provided ``config``""" - default_map: Dict[str, Any] = {} + default_map: dict[str, Any] = {} if ctx.default_map: default_map.update(ctx.default_map) diff --git a/pydoclint/utils/arg.py b/pydoclint/utils/arg.py index 4b0e08e..165eb44 100644 --- a/pydoclint/utils/arg.py +++ b/pydoclint/utils/arg.py @@ -1,5 +1,6 @@ +from __future__ import annotations + import ast -from typing import List, Optional, Set from docstring_parser.common import DocstringAttr, DocstringParam @@ -84,8 +85,8 @@ 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: Optional[ast.expr] = astArg.annotation - typeHint: Optional[str] = '' if anno is None else unparseName(anno) + anno: ast.expr | None = astArg.annotation + typeHint: str | None = '' 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) @@ -102,7 +103,7 @@ def fromAstAnnAssign(cls, astAnnAssign: ast.AnnAssign) -> 'Arg': return Arg(name=unparsedArgName, typeHint=unparsedTypeHint) @classmethod - def _str(cls, typeName: Optional[str]) -> str: + def _str(cls, typeName: str | None) -> str: return '' if typeName is None else typeName @classmethod @@ -148,7 +149,7 @@ class ArgList: equality, length calculation, etc. """ - def __init__(self, infoList: List[Arg]): + def __init__(self, infoList: list[Arg]) -> None: if not all(isinstance(_, Arg) for _ in infoList): raise TypeError('All elements of `infoList` must be Arg.') @@ -186,7 +187,7 @@ def length(self) -> int: return len(self.infoList) @classmethod - def fromDocstringParam(cls, params: List[DocstringParam]) -> 'ArgList': + def fromDocstringParam(cls, params: list[DocstringParam]) -> 'ArgList': """Construct an ArgList from a list of DocstringParam objects""" infoList = [ Arg.fromDocstringParam(_) @@ -198,7 +199,7 @@ def fromDocstringParam(cls, params: List[DocstringParam]) -> 'ArgList': @classmethod def fromDocstringAttr( cls, - params: List[DocstringAttr], + params: list[DocstringAttr], ) -> 'ArgList': """Construct an ArgList from a list of DocstringAttr objects""" infoList = [ @@ -212,7 +213,7 @@ def fromDocstringAttr( @classmethod def fromAstAssign(cls, astAssign: ast.Assign) -> 'ArgList': """Construct an ArgList from variable declaration/assignment""" - infoList: List[Arg] = [] + infoList: list[Arg] = [] for i, target in enumerate(astAssign.targets): if isinstance(target, ast.Tuple): # such as `a, b = c, d = 1, 2` for j, item in enumerate(target.elts): @@ -226,7 +227,7 @@ 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 - unparsedTarget: Optional[str] = unparseName(target) + unparsedTarget: str | None = unparseName(target) assert unparsedTarget is not None # to help mypy understand type infoList.append(Arg(name=unparsedTarget, typeHint='')) else: @@ -295,13 +296,13 @@ def equals( return verdict # noqa: R504 - def findArgsWithDifferentTypeHints(self, other: 'ArgList') -> List[Arg]: + def findArgsWithDifferentTypeHints(self, other: 'ArgList') -> list[Arg]: """Find args with unmatched type hints.""" if not self.equals(other, checkTypeHint=False, orderMatters=False): msg = 'These 2 arg lists do not have the same arg names' raise EdgeCaseError(msg) - result: List[Arg] = [] + result: list[Arg] = [] for selfArg in self.infoList: selfArgTypeHint: str = selfArg.typeHint otherArgTypeHint: str = other.lookup[selfArg.name] @@ -314,7 +315,7 @@ def subtract( self, other: 'ArgList', checkTypeHint: bool = True, - ) -> Set[Arg]: + ) -> set[Arg]: """Find the args that are in this object but not in `other`.""" if checkTypeHint: return set(self.infoList) - set(other.infoList) diff --git a/pydoclint/utils/astTypes.py b/pydoclint/utils/astTypes.py index 8e02f46..77a0a84 100644 --- a/pydoclint/utils/astTypes.py +++ b/pydoclint/utils/astTypes.py @@ -1,7 +1,11 @@ +from __future__ import annotations + import ast import sys from typing import Union +# typing.Union is still needed when defining custom types in Python 3.9. +# It can be changed to `xxx | yyy` after Python 3.9 is dropped. FuncOrAsyncFuncDef = Union[ast.AsyncFunctionDef, ast.FunctionDef] ClassOrFunctionDef = Union[ast.ClassDef, ast.AsyncFunctionDef, ast.FunctionDef] diff --git a/pydoclint/utils/doc.py b/pydoclint/utils/doc.py index 720c1c0..50450fd 100644 --- a/pydoclint/utils/doc.py +++ b/pydoclint/utils/doc.py @@ -1,5 +1,7 @@ +from __future__ import annotations + import pprint -from typing import Any, List, Optional, Union +from typing import Any from docstring_parser.common import ( Docstring, @@ -23,7 +25,7 @@ def __init__(self, docstring: str, style: str = 'numpy') -> None: self.docstring = docstring self.style = style - parser: Union[NumpydocParser, GoogleParser] + parser: NumpydocParser | GoogleParser if style == 'numpy': parser = NumpydocParser() self.parsed = parser.parse(docstring) @@ -80,7 +82,7 @@ def attrList(self) -> ArgList: # type:ignore[return] def hasReturnsSection(self) -> bool: # type:ignore[return] """Whether the docstring has a 'Returns' section""" if self.style in {'google', 'numpy', 'sphinx'}: - retSection: Optional[DocstringReturns] = self.parsed.returns + retSection: DocstringReturns | None = self.parsed.returns return retSection is not None and not retSection.is_generator self._raiseException() # noqa: R503 @@ -103,11 +105,11 @@ def hasRaisesSection(self) -> bool: # type:ignore[return] self._raiseException() # noqa: R503 @property - def returnSection(self) -> List[ReturnArg]: + def returnSection(self) -> list[ReturnArg]: """Get the return section of the docstring""" if isinstance(self.parsed, Docstring): # Google, numpy, Sphinx styles - returnSection: List[DocstringReturns] = self.parsed.many_returns - result: List[ReturnArg] = [] + returnSection: list[DocstringReturns] = self.parsed.many_returns + result: list[ReturnArg] = [] for element in returnSection: result.append( ReturnArg( @@ -122,11 +124,11 @@ def returnSection(self) -> List[ReturnArg]: return [] @property - def yieldSection(self) -> List[YieldArg]: + def yieldSection(self) -> list[YieldArg]: """Get the yield section of the docstring""" if isinstance(self.parsed, Docstring): # Google, numpy, Sphinx styles - yieldSection: List[DocstringYields] = self.parsed.many_yields - result: List[YieldArg] = [] + yieldSection: list[DocstringYields] = self.parsed.many_yields + result: list[YieldArg] = [] for element in yieldSection: result.append( YieldArg( diff --git a/pydoclint/utils/generic.py b/pydoclint/utils/generic.py index 7560617..65e78d7 100644 --- a/pydoclint/utils/generic.py +++ b/pydoclint/utils/generic.py @@ -3,7 +3,7 @@ import ast import copy import re -from typing import TYPE_CHECKING, List, Match, Optional, Tuple, Union +from typing import TYPE_CHECKING, Match from pydoclint.utils.astTypes import ClassOrFunctionDef, FuncOrAsyncFuncDef from pydoclint.utils.method_type import MethodType @@ -13,12 +13,12 @@ from pydoclint.utils.arg import Arg, ArgList -def collectFuncArgs(node: FuncOrAsyncFuncDef) -> List[ast.arg]: +def collectFuncArgs(node: FuncOrAsyncFuncDef) -> list[ast.arg]: """ Collect all arguments from a function node, and return them in their original order in the function signature. """ - allArgs: List[ast.arg] = [] + allArgs: list[ast.arg] = [] allArgs.extend(node.args.args) allArgs.extend(node.args.posonlyargs) allArgs.extend(node.args.kwonlyargs) @@ -64,7 +64,7 @@ def collectFuncArgs(node: FuncOrAsyncFuncDef) -> List[ast.arg]: ) -def getFunctionId(node: FuncOrAsyncFuncDef) -> Tuple[int, int, str]: +def getFunctionId(node: FuncOrAsyncFuncDef) -> tuple[int, int, str]: """ Get unique identifier of a function def. We also need line and column number because different function can have identical names. @@ -100,7 +100,7 @@ def detectMethodType(node: FuncOrAsyncFuncDef) -> MethodType: def getDocstring(node: ClassOrFunctionDef) -> str: """Get docstring from a class definition or a function definition""" - docstring_: Optional[str] = ast.get_docstring(node) + docstring_: str | None = ast.get_docstring(node) return '' if docstring_ is None else docstring_ @@ -167,10 +167,7 @@ def getNodeName(node: ast.AST) -> str: return getattr(node, 'name', '') -def stringStartsWith( - string: Optional[str], - substrings: Tuple[str, ...], -) -> bool: +def stringStartsWith(string: str | None, substrings: tuple[str, ...]) -> bool: """Check whether the string starts with any of the substrings""" if string is None: return False @@ -182,7 +179,7 @@ def stringStartsWith( return False -def stripQuotes(string: Optional[str]) -> Optional[str]: +def stripQuotes(string: str | None) -> str | None: """ Strip quotes (both double and single quotes) from the given string. Also, strip backticks (`) or double backticks (``) from the beginning @@ -217,7 +214,7 @@ def appendArgsToCheckToV105( docArgs: ArgList, ) -> Violation: """Append the arg names to check to the error message of v105 or v605""" - argsToCheck: List[Arg] = funcArgs.findArgsWithDifferentTypeHints(docArgs) + argsToCheck: list[Arg] = funcArgs.findArgsWithDifferentTypeHints(docArgs) argNames: str = ', '.join(_.name for _ in argsToCheck) return original_v105.appendMoreMsg(moreMsg=argNames) @@ -250,7 +247,7 @@ def specialEqual(str1: str, str2: str) -> bool: return True -def getFullAttributeName(node: Union[ast.Attribute, ast.Name]) -> str: +def getFullAttributeName(node: ast.Attribute | ast.Name) -> str: """Get the full name of a symbol like a.b.c.foo""" if isinstance(node, ast.Name): return node.id diff --git a/pydoclint/utils/return_anno.py b/pydoclint/utils/return_anno.py index 603c8e9..702f059 100644 --- a/pydoclint/utils/return_anno.py +++ b/pydoclint/utils/return_anno.py @@ -1,6 +1,7 @@ +from __future__ import annotations + import ast import json -from typing import List, Optional from pydoclint.utils.edge_case_error import EdgeCaseError from pydoclint.utils.generic import stripQuotes @@ -10,8 +11,8 @@ class ReturnAnnotation: """A class to hold the return annotation in a function's signature""" - def __init__(self, annotation: Optional[str]) -> None: - self.annotation: Optional[str] = stripQuotes(annotation) + def __init__(self, annotation: str | None) -> None: + self.annotation: str | None = stripQuotes(annotation) def __str__(self) -> str: return f'ReturnAnnotation(annotation={json.dumps(self.annotation)})' @@ -19,7 +20,7 @@ def __str__(self) -> str: def __repr__(self) -> str: return self.__str__() - def decompose(self) -> List[str]: + def decompose(self) -> list[str]: """ Numpy style allows decomposing the returning tuple into individual element. For example, if the return annotation is `Tuple[int, bool]`, @@ -29,7 +30,7 @@ def decompose(self) -> List[str]: Returns ------- - List[str] + list[str] The decomposed element Raises @@ -59,7 +60,7 @@ def decompose(self) -> List[str]: return [insideTuple] if isinstance(parsedBody0.value, ast.Tuple): # like Tuple[int, str] - elts: List[ast.expr] = parsedBody0.value.elts + elts: list[ast.expr] = parsedBody0.value.elts return [unparseName(_) for _ in elts] # type:ignore[misc] raise EdgeCaseError('decompose(): This should not have happened') @@ -74,6 +75,6 @@ def _isTuple(self) -> bool: except Exception: return False - def putAnnotationInList(self) -> List[str]: + def putAnnotationInList(self) -> list[str]: """Put annotation string in a list""" return [] if self.annotation is None else [self.annotation] diff --git a/pydoclint/utils/return_yield_raise.py b/pydoclint/utils/return_yield_raise.py index a94a6d7..a62d062 100644 --- a/pydoclint/utils/return_yield_raise.py +++ b/pydoclint/utils/return_yield_raise.py @@ -1,5 +1,7 @@ +from __future__ import annotations + import ast -from typing import Callable, Dict, Generator, List, Optional, Tuple, Type +from typing import Callable, Generator, Type from pydoclint.utils import walk from pydoclint.utils.astTypes import BlockType, FuncOrAsyncFuncDef @@ -8,8 +10,8 @@ ReturnType = Type[ast.Return] ExprType = Type[ast.expr] -YieldAndYieldFromTypes = Tuple[Type[ast.Yield], Type[ast.YieldFrom]] -FuncOrAsyncFuncTypes = Tuple[Type[ast.FunctionDef], Type[ast.AsyncFunctionDef]] +YieldAndYieldFromTypes = tuple[Type[ast.Yield], Type[ast.YieldFrom]] +FuncOrAsyncFuncTypes = tuple[Type[ast.FunctionDef], Type[ast.AsyncFunctionDef]] FuncOrAsyncFunc = (ast.FunctionDef, ast.AsyncFunctionDef) @@ -23,7 +25,7 @@ def isReturnAnnotationNone(node: FuncOrAsyncFuncDef) -> bool: return _isNone(node.returns) -def _isNone(node: Optional[ast.expr]) -> bool: +def _isNone(node: ast.expr | None) -> bool: return isinstance(node, ast.Constant) and node.value is None @@ -32,7 +34,7 @@ def isReturnAnnotationNoReturn(node: FuncOrAsyncFuncDef) -> bool: if node.returns is None: return False - returnAnnotation: Optional[str] = unparseName(node.returns) + returnAnnotation: str | None = unparseName(node.returns) return returnAnnotation == 'NoReturn' @@ -41,7 +43,7 @@ def hasGeneratorAsReturnAnnotation(node: FuncOrAsyncFuncDef) -> bool: if node.returns is None: return False - returnAnno: Optional[str] = unparseName(node.returns) + returnAnno: str | None = unparseName(node.returns) return returnAnno in {'Generator', 'AsyncGenerator'} or stringStartsWith( returnAnno, ('Generator[', 'AsyncGenerator[') ) @@ -52,7 +54,7 @@ def hasIteratorOrIterableAsReturnAnnotation(node: FuncOrAsyncFuncDef) -> bool: if node.returns is None: return False - returnAnnotation: Optional[str] = unparseName(node.returns) + returnAnnotation: str | None = unparseName(node.returns) return returnAnnotation in { 'Iterator', 'Iterable', @@ -93,7 +95,7 @@ def isThisNodeARaiseStmt(node_: ast.AST) -> bool: return _hasExpectedStatements(node, isThisNodeARaiseStmt) -def getRaisedExceptions(node: FuncOrAsyncFuncDef) -> List[str]: +def getRaisedExceptions(node: FuncOrAsyncFuncDef) -> list[str]: """Get the raised exceptions in a function node as a sorted list""" return sorted(set(_getRaisedExceptions(node))) @@ -105,9 +107,9 @@ def _getRaisedExceptions( childLineNum: int = -999 # key: child lineno, value: (parent lineno, is parent a function?) - familyTree: Dict[int, Tuple[int, bool]] = {} + familyTree: dict[int, tuple[int, bool]] = {} - currentParentExceptHandler: Optional[ast.ExceptHandler] = None + currentParentExceptHandler: ast.ExceptHandler | None = None # Depth-first guarantees the last-seen exception handler # is a parent of child. @@ -198,7 +200,7 @@ def _hasExpectedStatements( foundExpectedStmt: bool = False # key: child lineno, value: (parent lineno, is parent a function?) - familyTree: Dict[int, Tuple[int, bool]] = {} + familyTree: dict[int, tuple[int, bool]] = {} for child, parent in walk.walk(node): childLineNum = _updateFamilyTree(child, parent, familyTree) @@ -223,7 +225,7 @@ def _hasExpectedStatements( def _updateFamilyTree( child: ast.AST, parent: ast.AST, - familyTree: Dict[int, Tuple[int, bool]], + familyTree: dict[int, tuple[int, bool]], ) -> int: """ Structure of `familyTree`: @@ -256,7 +258,7 @@ def _getLineNum(node: ast.AST) -> int: def _confirmThisStmtIsNotWithinNestedFunc( foundStatementTemp: bool, - familyTree: Dict[int, Tuple[int, bool]], + familyTree: dict[int, tuple[int, bool]], lineNumOfStatement: int, lineNumOfThisNode: int, ) -> bool: @@ -278,7 +280,7 @@ def _confirmThisStmtIsNotWithinNestedFunc( def _lookupParentFunc( - familyLine: Dict[int, Tuple[int, bool]], + familyLine: dict[int, tuple[int, bool]], lineNumOfChildNode: int, ) -> int: """ diff --git a/pydoclint/utils/unparser_custom.py b/pydoclint/utils/unparser_custom.py index df6cf33..d3c3a9e 100644 --- a/pydoclint/utils/unparser_custom.py +++ b/pydoclint/utils/unparser_custom.py @@ -1,7 +1,8 @@ +from __future__ import annotations + import ast import re import sys -from typing import Optional, Union from pydoclint.utils.edge_case_error import EdgeCaseError @@ -31,8 +32,8 @@ def py311unparse(astObj: ast.AST) -> str: def unparseName( - node: Union[ast.expr, ast.Module, None], -) -> Optional[str]: + node: ast.expr | ast.Module | None, +) -> str | None: """Parse type annotations from argument list or return annotation.""" if node is None: return None diff --git a/pydoclint/utils/violation.py b/pydoclint/utils/violation.py index 92207c9..965d84e 100644 --- a/pydoclint/utils/violation.py +++ b/pydoclint/utils/violation.py @@ -1,6 +1,7 @@ +from __future__ import annotations + import types from copy import deepcopy -from typing import Tuple from pydoclint.utils.edge_case_error import EdgeCaseError @@ -108,7 +109,7 @@ def _str(self, showLineNum: bool = False) -> str: return f'{self.line}: {self.__str__()}' - def getInfoForFlake8(self) -> Tuple[int, int, str]: + def getInfoForFlake8(self) -> tuple[int, int, str]: """Get the violation info for flake8""" colOffset: int = 0 # we don't need column offset to locate the issue msg = f'{self.fullErrorCode} {self.msg}' # no colon b/c that would cause 'yesqa' issues diff --git a/pydoclint/utils/visitor_helper.py b/pydoclint/utils/visitor_helper.py index 743bc7b..c08b8a0 100644 --- a/pydoclint/utils/visitor_helper.py +++ b/pydoclint/utils/visitor_helper.py @@ -1,7 +1,8 @@ """Helper functions to classes/methods in visitor.py""" +from __future__ import annotations + import ast import sys -from typing import List, Optional, Set from pydoclint.utils.arg import Arg, ArgList from pydoclint.utils.doc import Doc @@ -30,7 +31,7 @@ def checkClassAttributesAgainstClassDocstring( *, node: ast.ClassDef, style: str, - violations: List[Violation], + violations: list[Violation], lineNum: int, msgPrefix: str, shouldCheckArgOrder: bool, @@ -153,7 +154,7 @@ def extractClassAttributesFromNode( if 'body' not in node.__dict__ or len(node.body) == 0: return ArgList([]) - atl: List[Arg] = [] + atl: list[Arg] = [] for itm in node.body: if isinstance(itm, ast.AnnAssign): # with type hints ("a: int = 1") atl.append(Arg.fromAstAnnAssign(itm)) @@ -184,7 +185,7 @@ def checkDocArgsLengthAgainstActualArgs( *, docArgs: ArgList, actualArgs: ArgList, - violations: List[Violation], + violations: list[Violation], violationForDocArgsLengthShorter: Violation, # such as V101, V601 violationForDocArgsLengthLonger: Violation, # such as V102, V602 ) -> None: @@ -200,7 +201,7 @@ def checkNameOrderAndTypeHintsOfDocArgsAgainstActualArgs( *, docArgs: ArgList, actualArgs: ArgList, - violations: List[Violation], + violations: list[Violation], actualArgsAreClassAttributes: bool, violationForOrderMismatch: Violation, # such as V104, V604 violationForTypeHintMismatch: Violation, # such as V105, V605 @@ -250,16 +251,16 @@ def checkNameOrderAndTypeHintsOfDocArgsAgainstActualArgs( violations.append(violationForOrderMismatch) violations.append(v105new) else: - argsInFuncNotInDoc: Set[Arg] = actualArgs.subtract( + argsInFuncNotInDoc: set[Arg] = actualArgs.subtract( docArgs, checkTypeHint=False, ) - argsInDocNotInFunc: Set[Arg] = docArgs.subtract( + argsInDocNotInFunc: set[Arg] = docArgs.subtract( actualArgs, checkTypeHint=False, ) - msgPostfixParts: List[str] = [] + msgPostfixParts: list[str] = [] string0 = ( 'Attributes in the class definition but not in the' @@ -304,8 +305,8 @@ def checkReturnTypesForViolations( *, style: str, returnAnnotation: ReturnAnnotation, - violationList: List[Violation], - returnSection: List[ReturnArg], + violationList: list[Violation], + returnSection: list[ReturnArg], violation: Violation, ) -> None: """Check return types between function signature and docstring""" @@ -328,8 +329,8 @@ def checkReturnTypesForViolations( def checkReturnTypesForNumpyStyle( *, returnAnnotation: ReturnAnnotation, - violationList: List[Violation], - returnSection: List[ReturnArg], + violationList: list[Violation], + returnSection: list[ReturnArg], violation: Violation, ) -> None: """Check return types for numpy docstring style""" @@ -352,10 +353,10 @@ def checkReturnTypesForNumpyStyle( # # This is why we are comparing both the decomposed annotation # types and the original annotation type - returnAnnoItems: List[str] = returnAnnotation.decompose() - returnAnnoInList: List[str] = returnAnnotation.putAnnotationInList() + returnAnnoItems: list[str] = returnAnnotation.decompose() + returnAnnoInList: list[str] = returnAnnotation.putAnnotationInList() - returnSecTypes: List[str] = [stripQuotes(_.argType) for _ in returnSection] # type:ignore[misc] + returnSecTypes: list[str] = [stripQuotes(_.argType) for _ in returnSection] # type:ignore[misc] if returnAnnoInList != returnSecTypes: if len(returnAnnoItems) != len(returnSection): @@ -376,8 +377,8 @@ def checkReturnTypesForNumpyStyle( def checkReturnTypesForGoogleOrSphinxStyle( *, returnAnnotation: ReturnAnnotation, - violationList: List[Violation], - returnSection: List[ReturnArg], + violationList: list[Violation], + returnSection: list[ReturnArg], violation: Violation, ) -> None: """Check return types for Google or Sphinx docstring style""" @@ -410,8 +411,8 @@ def checkReturnTypesForGoogleOrSphinxStyle( def checkYieldTypesForViolations( *, returnAnnotation: ReturnAnnotation, - violationList: List[Violation], - yieldSection: List[YieldArg], + violationList: list[Violation], + yieldSection: list[YieldArg], violation: Violation, hasGeneratorAsReturnAnnotation: bool, hasIteratorOrIterableAsReturnAnnotation: bool, @@ -425,10 +426,10 @@ def checkYieldTypesForViolations( # values into one `Generator[..., ..., ...]`, because it is easier # to check and less ambiguous. - returnAnnoText: Optional[str] = returnAnnotation.annotation + returnAnnoText: str | None = returnAnnotation.annotation extract = extractYieldTypeFromGeneratorOrIteratorAnnotation - yieldType: Optional[str] = extract( + yieldType: str | None = extract( returnAnnoText, hasGeneratorAsReturnAnnotation, hasIteratorOrIterableAsReturnAnnotation, @@ -469,10 +470,10 @@ def checkYieldTypesForViolations( def extractYieldTypeFromGeneratorOrIteratorAnnotation( - returnAnnoText: Optional[str], + returnAnnoText: str | None, hasGeneratorAsReturnAnnotation: bool, hasIteratorOrIterableAsReturnAnnotation: bool, -) -> Optional[str]: +) -> str | None: """Extract yield type from Generator or Iterator annotations""" # # "Yield type" is the 0th element in a Generator @@ -480,7 +481,7 @@ def extractYieldTypeFromGeneratorOrIteratorAnnotation( # ReturnType]) # https://docs.python.org/3/library/typing.html#typing.Generator # Or it's the 0th (only) element in Iterator - yieldType: Optional[str] + yieldType: str | None try: if hasGeneratorAsReturnAnnotation: @@ -504,16 +505,14 @@ def extractYieldTypeFromGeneratorOrIteratorAnnotation( return stripQuotes(yieldType) -def extractReturnTypeFromGenerator( - returnAnnoText: Optional[str], -) -> Optional[str]: +def extractReturnTypeFromGenerator(returnAnnoText: str | None) -> str | None: """Extract return type from Generator annotations""" # # "Return type" is the last element in a Generator # type annotation (Generator[YieldType, SendType, # ReturnType]) # https://docs.python.org/3/library/typing.html#typing.Generator - returnType: Optional[str] + returnType: str | None try: if sys.version_info >= (3, 9): returnType = unparseName( @@ -531,9 +530,9 @@ def extractReturnTypeFromGenerator( def addMismatchedRaisesExceptionViolation( *, - docRaises: List[str], - actualRaises: List[str], - violations: List[Violation], + docRaises: list[str], + actualRaises: list[str], + violations: list[Violation], violationForRaisesMismatch: Violation, # such as V503 lineNum: int, msgPrefix: str, diff --git a/pydoclint/visitor.py b/pydoclint/visitor.py index eb94c7e..33c8692 100644 --- a/pydoclint/visitor.py +++ b/pydoclint/visitor.py @@ -1,5 +1,6 @@ +from __future__ import annotations + import ast -from typing import List, Optional, Union from pydoclint.utils.arg import Arg, ArgList from pydoclint.utils.astTypes import FuncOrAsyncFuncDef @@ -91,7 +92,7 @@ def __init__( ) self.parent: ast.AST = ast.Pass() # keep track of parent node - self.violations: List[Violation] = [] + self.violations: list[Violation] = [] def visit_ClassDef(self, node: ast.ClassDef) -> None: # noqa: D102 currentParent = self.parent # keep aside @@ -121,7 +122,7 @@ def visit_ClassDef(self, node: ast.ClassDef) -> None: # noqa: D102 self.parent = currentParent # restore def visit_FunctionDef(self, node: FuncOrAsyncFuncDef) -> None: # noqa: D102 - parent_: Union[ast.ClassDef, FuncOrAsyncFuncDef] = self.parent # type:ignore[assignment] + parent_: ast.ClassDef | FuncOrAsyncFuncDef = self.parent # type:ignore[assignment] self.parent = node isClassConstructor: bool = node.name == '__init__' and isinstance( @@ -140,10 +141,10 @@ def visit_FunctionDef(self, node: FuncOrAsyncFuncDef) -> None: # noqa: D102 initDocstring=docstring, ) - argViolations: List[Violation] - returnViolations: List[Violation] - yieldViolations: List[Violation] - raiseViolations: List[Violation] + argViolations: list[Violation] + returnViolations: list[Violation] + yieldViolations: list[Violation] + raiseViolations: list[Violation] if docstring == '': # We don't check functions without docstrings. @@ -357,7 +358,7 @@ def checkArguments( # noqa: C901 node: FuncOrAsyncFuncDef, parent_: ast.AST, doc: Doc, - ) -> List[Violation]: + ) -> list[Violation]: """ Check input arguments of the function. @@ -374,10 +375,10 @@ def checkArguments( # noqa: C901 Returns ------- - List[Violation] + list[Violation] A list of argument violations """ - astArgList: List[ast.arg] = collectFuncArgs(node) + astArgList: list[ast.arg] = collectFuncArgs(node) isMethod: bool = isinstance(parent_, ast.ClassDef) msgPrefix: str = generateFuncMsgPrefix(node, parent_, appendColon=True) @@ -414,7 +415,7 @@ def checkArguments( # noqa: C901 if docArgs.length == 0 and funcArgs.length == 0: return [] - violations: List[Violation] = [] + violations: list[Violation] = [] checkDocArgsLengthAgainstActualArgs( docArgs=docArgs, @@ -473,7 +474,7 @@ def checkReturns( # noqa: C901 node: FuncOrAsyncFuncDef, parent: ast.AST, doc: Doc, - ) -> List[Violation]: + ) -> list[Violation]: """Check return statement & return type annotation of this function""" lineNum: int = node.lineno msgPrefix = generateFuncMsgPrefix(node, parent, appendColon=False) @@ -492,7 +493,7 @@ def checkReturns( # noqa: C901 docstringHasReturnSection: bool = doc.hasReturnsSection - violations: List[Violation] = [] + violations: list[Violation] = [] if not docstringHasReturnSection and not isPropertyMethod: if ( # fmt: off @@ -525,7 +526,7 @@ def checkReturns( # noqa: C901 returnAnno = ReturnAnnotation(annotation=None) if docstringHasReturnSection: - returnSec: List[ReturnArg] = doc.returnSection + returnSec: list[ReturnArg] = doc.returnSection else: returnSec = [] @@ -566,9 +567,9 @@ def checkReturnsAndYieldsInClassConstructor( cls, parent: ast.ClassDef, doc: Doc, - ) -> List[Violation]: + ) -> list[Violation]: """Check the presence of a Returns/Yields section in class docstring""" - violations: List[Violation] = [] + violations: list[Violation] = [] if doc.hasReturnsSection: violations.append( Violation( @@ -594,9 +595,9 @@ def checkYields( # noqa: C901 node: FuncOrAsyncFuncDef, parent: ast.AST, doc: Doc, - ) -> List[Violation]: + ) -> list[Violation]: """Check violations on 'yield' statements or 'Generator' annotation""" - violations: List[Violation] = [] + violations: list[Violation] = [] lineNum: int = node.lineno msgPrefix = generateFuncMsgPrefix(node, parent, appendColon=False) @@ -621,7 +622,7 @@ def checkYields( # noqa: C901 if not docstringHasYieldsSection: extract = extractYieldTypeFromGeneratorOrIteratorAnnotation - yieldType: Optional[str] = extract( + yieldType: str | None = extract( returnAnnoText=returnAnno.annotation, hasGeneratorAsReturnAnnotation=hasGenAsRetAnno, hasIteratorOrIterableAsReturnAnnotation=hasIterAsRetAnno, @@ -645,7 +646,7 @@ def checkYields( # noqa: C901 if hasYieldStmt and self.checkYieldTypes: if docstringHasYieldsSection: - yieldSec: List[YieldArg] = doc.yieldSection + yieldSec: list[YieldArg] = doc.yieldSection else: yieldSec = [] @@ -668,7 +669,7 @@ def checkReturnAndYield( # noqa: C901 node: FuncOrAsyncFuncDef, parent: ast.AST, doc: Doc, - ) -> List[Violation]: + ) -> list[Violation]: """ Check violations when a function has both `return` and `yield` statements in it. @@ -699,7 +700,7 @@ def my_function(num: int) -> Generator[int, None, str]: # Just a sanity check: assert (hasYieldStatements(node) and hasReturnStatements(node)) is True - violations: List[Violation] = [] + violations: list[Violation] = [] lineNum: int = node.lineno msgPrefix = generateFuncMsgPrefix(node, parent, appendColon=False) @@ -722,10 +723,10 @@ def my_function(num: int) -> Generator[int, None, str]: hasReturnAnno: bool = hasReturnAnnotation(node) returnAnno = ReturnAnnotation(unparseName(node.returns)) - returnSec: List[ReturnArg] = doc.returnSection + returnSec: list[ReturnArg] = doc.returnSection # Check the return section in the docstring - retTypeInGenerator: Optional[str] + retTypeInGenerator: str | None if not docstringHasReturnSection: if doc.isShortDocstring and self.skipCheckingShortDocstrings: pass @@ -775,11 +776,11 @@ def my_function(num: int) -> Generator[int, None, str]: else: if self.checkYieldTypes: returnAnno = ReturnAnnotation(unparseName(node.returns)) - yieldSec: List[YieldArg] = doc.yieldSection + yieldSec: list[YieldArg] = doc.yieldSection if hasGenAsRetAnno or hasIterAsRetAnno: extract = extractYieldTypeFromGeneratorOrIteratorAnnotation - yieldType: Optional[str] = extract( + yieldType: str | None = extract( returnAnnoText=returnAnno.annotation, hasGeneratorAsReturnAnnotation=hasGenAsRetAnno, hasIteratorOrIterableAsReturnAnnotation=hasIterAsRetAnno, @@ -808,9 +809,9 @@ def checkRaises( node: FuncOrAsyncFuncDef, parent: ast.AST, doc: Doc, - ) -> List[Violation]: + ) -> list[Violation]: """Check violations on 'raise' statements""" - violations: List[Violation] = [] + violations: list[Violation] = [] lineNum: int = node.lineno msgPrefix = generateFuncMsgPrefix(node, parent, appendColon=False) @@ -831,7 +832,7 @@ def checkRaises( # check that the raise statements match those in body. if hasRaiseStmt: - docRaises: List[str] = [] + docRaises: list[str] = [] for raises in doc.parsed.raises: if raises.type_name: