Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use "modern" type annotations #187

Merged
merged 4 commits into from
Dec 15, 2024
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
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
Loading