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

Fix a bug with ast.assign #195

Merged
merged 2 commits into from
Dec 26, 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
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Change Log

## [unpublished]
## [0.5.14] - 2024-12-26

- Changed

Expand All @@ -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

Expand Down
71 changes: 47 additions & 24 deletions pydoclint/utils/arg.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import ast
from typing import Any

from docstring_parser.common import DocstringAttr, DocstringParam

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -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
Expand Down
15 changes: 15 additions & 0 deletions tests/data/edge_cases/16_assign_to_attr/cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Loading