From 46c5cb78a36446feed5bee30ab763d0f157a21b3 Mon Sep 17 00:00:00 2001 From: jsh9 <25124332+jsh9@users.noreply.github.com> Date: Thu, 26 Dec 2024 00:05:33 -0500 Subject: [PATCH] Fix a bug with ast.assign (#195) --- CHANGELOG.md | 6 +- pydoclint/utils/arg.py | 71 ++++++++++++------- setup.cfg | 2 +- .../edge_cases/16_assign_to_attr/cases.py | 15 ++++ 4 files changed, 68 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76f794c..5fd39bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log -## [unpublished] +## [0.5.14] - 2024-12-26 - Changed @@ -10,6 +10,10 @@ Python's AST can correctly parse the files - Added end-to-end test (essentially an integration test) +- Fixed + + - A bug in ast.assign + - Full diff - https://github.com/jsh9/pydoclint/compare/0.5.13...0.5.14 diff --git a/pydoclint/utils/arg.py b/pydoclint/utils/arg.py index 13578bf..5dadd3e 100644 --- a/pydoclint/utils/arg.py +++ b/pydoclint/utils/arg.py @@ -1,6 +1,7 @@ from __future__ import annotations import ast +from typing import Any from docstring_parser.common import DocstringAttr, DocstringParam @@ -217,34 +218,56 @@ def fromAstAssign(cls, astAssign: ast.Assign) -> 'ArgList': 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): - if not isinstance(item, ast.Name): - raise EdgeCaseError( - f'astAssign.targets[{i}].elts[{j}] is of' - f' type {type(item)} instead of ast.Name' - ) - - infoList.append(Arg(name=item.id, typeHint='')) - elif isinstance(target, ast.Name): # such as `a = 1` or `a = b = 2` - infoList.append(Arg(name=target.id, typeHint='')) - else: - try: # we may not know all potential cases, so we use try/catch - unparsedTarget: str | None = unparseName(target) - assert unparsedTarget is not None # to help mypy understand type - infoList.append(Arg(name=unparsedTarget, typeHint='')) - except Exception as ex: - lineRange: str = ( - f'in Line {astAssign.lineno}' - if astAssign.lineno == astAssign.end_lineno - else f'in Lines {astAssign.lineno}-{astAssign.end_lineno}' - ) - msg: str = ( - f'Edge case encountered {lineRange}.' - f' astAssign.targets[{i}] is of type {type(target)}.' + cls._unparseTargetAndAppendToInfoList( + target=item, + infoList=infoList, + lineNum=astAssign.lineno, + endLineNum=astAssign.end_lineno, + i=i, + j=j, ) - raise EdgeCaseError(msg) from ex + else: # a single element + cls._unparseTargetAndAppendToInfoList( + target=target, + infoList=infoList, + lineNum=astAssign.lineno, + endLineNum=astAssign.end_lineno, + i=i, + j=None, + ) return ArgList(infoList=infoList) + @classmethod + def _unparseTargetAndAppendToInfoList( + cls, + *, + target: Any, + infoList: list[Arg], + lineNum: int | None, + endLineNum: int | None, + i: int, + j: int | None = None, + ) -> None: + try: # we may not know all potential cases, so we use try/catch + unparsedTarget: str | None = unparseName(target) + assert unparsedTarget is not None # to help mypy understand type + infoList.append(Arg(name=unparsedTarget, typeHint='')) + except Exception as ex: + lineRange: str = ( + f'in Line {lineNum}' + if lineNum == endLineNum + else f'in Lines {lineNum}-{endLineNum}' + ) + msg1: str = f'Edge case encountered {lineRange}.' + msg2: str = ( + f' astAssign.targets[{i}] is of type {type(target)}.' + if j is None + else f' astAssign.targets[{i}].elts[{j}] is of type {type(target)}.' + ) + msg: str = msg1 + msg2 + raise EdgeCaseError(msg) from ex + def contains(self, arg: Arg) -> bool: """Whether a given `Arg` object exists in the list""" return arg.name in self.lookup diff --git a/setup.cfg b/setup.cfg index 5a9dac9..729471c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pydoclint -version = 0.5.13 +version = 0.5.14 description = A Python docstring linter that checks arguments, returns, yields, and raises sections long_description = file: README.md long_description_content_type = text/markdown diff --git a/tests/data/edge_cases/16_assign_to_attr/cases.py b/tests/data/edge_cases/16_assign_to_attr/cases.py index 5737de3..fe21f20 100644 --- a/tests/data/edge_cases/16_assign_to_attr/cases.py +++ b/tests/data/edge_cases/16_assign_to_attr/cases.py @@ -10,3 +10,18 @@ def large_drawing(self, obj): # Non-self attribute should not be type hinted, because this could lead to # potential ambiguities. See more: https://stackoverflow.com/a/77831273 large_drawing.descr_2: str = 'Drawing' + + # The following is from: + # https://github.com/matplotlib/matplotlib/blob/c2d502d219c8c0abe8722279b21f817aeae2058a/lib/matplotlib/backends/backend_agg.py#L510-L521 + print_jpg.__doc__, print_tif.__doc__, print_webp.__doc__ = map( + """ + Write the figure to a {} file. + + Parameters + ---------- + filename_or_obj : str or path-like or file-like + The file to write to. + pil_kwargs : dict, optional + Additional keyword arguments that are passed to + `PIL.Image.Image.save` when saving the figure. + """.format, ["JPEG", "TIFF", "WebP"])