Skip to content

Commit

Permalink
Use "modern" type annotations (#187)
Browse files Browse the repository at this point in the history
  • Loading branch information
jsh9 authored Dec 15, 2024
1 parent 903aa91 commit dc5a53b
Show file tree
Hide file tree
Showing 15 changed files with 164 additions and 149 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
15 changes: 8 additions & 7 deletions pydoclint/baseline.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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:
Expand All @@ -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]] = {}
Expand All @@ -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 = []
Expand Down
5 changes: 3 additions & 2 deletions pydoclint/flake8_entry.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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(
Expand Down
25 changes: 13 additions & 12 deletions pydoclint/main.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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(
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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}'
Expand All @@ -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()):
Expand All @@ -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,
Expand Down Expand Up @@ -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 []

Expand Down
20 changes: 11 additions & 9 deletions pydoclint/parse_config.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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.
Expand All @@ -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:
Expand All @@ -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 {}
Expand All @@ -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.')
Expand Down Expand Up @@ -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)

Expand Down
25 changes: 13 additions & 12 deletions pydoclint/utils/arg.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import ast
from typing import List, Optional, Set

from docstring_parser.common import DocstringAttr, DocstringParam

Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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.')

Expand Down Expand Up @@ -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(_)
Expand All @@ -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 = [
Expand All @@ -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):
Expand All @@ -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:
Expand Down Expand Up @@ -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]
Expand All @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions pydoclint/utils/astTypes.py
Original file line number Diff line number Diff line change
@@ -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]

Expand Down
Loading

0 comments on commit dc5a53b

Please sign in to comment.