Skip to content

Commit

Permalink
Fix double quotes in Literal type hint (#123)
Browse files Browse the repository at this point in the history
  • Loading branch information
jsh9 committed Feb 17, 2024
1 parent d2b4b99 commit 70eb3c8
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 6 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Change Log

## [unpublished] - 2024-02-17

- Fixed
- A bug where using double quotes in Literal type (such as `Literal["foo"]`
could produce a false positive `DOC203` violation.

## [0.4.0] - 2024-02-08

- Changed
Expand Down
24 changes: 24 additions & 0 deletions pydoclint/utils/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,27 @@ def appendArgsToCheckToV105(
argsToCheck: List['Arg'] = funcArgs.findArgsWithDifferentTypeHints(docArgs) # noqa: F821
argNames: str = ', '.join(_.name for _ in argsToCheck)
return original_v105.appendMoreMsg(moreMsg=argNames)


def specialEqual(str1: str, str2: str) -> bool:
"""
Check string equality but treat any single quotes as the same as
double quotes.
"""
if str1 == str2:
return True # using shortcuts to speed up evaluation

if len(str1) != len(str2):
return False # using shortcuts to speed up evaluation

quotes = {'"', "'"}
for char1, char2 in zip(str1, str2):
if char1 == char2:
continue

if char1 in quotes and char2 in quotes:
continue

return False

return True
11 changes: 7 additions & 4 deletions pydoclint/utils/visitor_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import List, Optional

from pydoclint.utils.annotation import unparseAnnotation
from pydoclint.utils.generic import stripQuotes
from pydoclint.utils.generic import specialEqual, stripQuotes
from pydoclint.utils.return_anno import ReturnAnnotation
from pydoclint.utils.return_arg import ReturnArg
from pydoclint.utils.violation import Violation
Expand Down Expand Up @@ -75,7 +75,10 @@ def checkReturnTypesForNumpyStyle(
msg += f' {len(returnSection)} type(s).'
violationList.append(violation.appendMoreMsg(moreMsg=msg))
else:
if returnSecTypes != returnAnnoItems:
if not all(
specialEqual(x, y)
for x, y in zip(returnSecTypes, returnAnnoItems)
):
msg1 = f'Return annotation types: {returnAnnoItems}; '
msg2 = f'docstring return section types: {returnSecTypes}'
violationList.append(violation.appendMoreMsg(msg1 + msg2))
Expand All @@ -97,12 +100,12 @@ def checkReturnTypesForGoogleOrSphinxStyle(
# use one compound style for tuples.

if len(returnSection) > 0:
retArgType = stripQuotes(returnSection[0].argType)
retArgType: str = stripQuotes(returnSection[0].argType)
if returnAnnotation.annotation is None:
msg = 'Return annotation has 0 type(s); docstring'
msg += ' return section has 1 type(s).'
violationList.append(violation.appendMoreMsg(moreMsg=msg))
elif retArgType != returnAnnotation.annotation:
elif not specialEqual(retArgType, returnAnnotation.annotation):
msg = 'Return annotation types: '
msg += str([returnAnnotation.annotation]) + '; '
msg += 'docstring return section types: '
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ skip = ['unparser.py']

[tool.cercis]
wrap-line-with-long-string = true
extend-exclude = 'tests/data'

[tool.pydoclint]
style = 'numpy'
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ classifiers =
[options]
packages = find:
install_requires =
click>=8.0.0
click>=8.1.0
docstring_parser_fork>=0.0.5
tomli>=2.0.1; python_version<'3.11'
python_requires = >=3.8
Expand Down
46 changes: 46 additions & 0 deletions tests/data/edge_cases/09_double_quotes_in_Literal/google.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# fmt: off

# This edge case comes from https://github.com/jsh9/pydoclint/issues/105

from __future__ import annotations

from typing import Literal


def func_1(arg1: Literal["foo"]) -> Literal["foo"]:
"""
Test literal.
Args:
arg1 (Literal["foo"]): Arg 1
Returns:
Literal["foo"]: The literal string "foo".
"""
return "foo"


def func_2(arg1: Literal['foo']) -> Literal['foo']:
"""
Test literal.
Args:
arg1 (Literal['foo']): Arg 1
Returns:
Literal['foo']: The literal string "foo".
"""
return "foo"


def func_3(arg1: Literal['foo']) -> tuple[Literal['foo'], Literal["bar"]]:
"""
Test literal.
Args:
arg1 (Literal['foo']): Arg 1
Returns:
tuple[Literal['foo'], Literal["bar"]]: The literal strings 'foo' & "bar"
"""
return 'foo', "bar"
57 changes: 57 additions & 0 deletions tests/data/edge_cases/09_double_quotes_in_Literal/numpy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# fmt: off

# This edge case comes from https://github.com/jsh9/pydoclint/issues/105

from __future__ import annotations

from typing import Literal


def func_1(arg1: Literal["foo"]) -> Literal["foo"]:
"""
Test literal.
Parameters
----------
arg1 : Literal["foo"]
Arg 1
Returns
-------
Literal["foo"]
The literal string "foo".
"""
return "foo"


def func_2(arg1: Literal['foo']) -> Literal['foo']:
"""
Test literal.
Parameters
----------
arg1 : Literal['foo']
Arg 1
Returns
-------
Literal['foo']
The literal string 'foo'.
"""
return "foo"


def func_3() -> tuple[Literal['foo'], Literal["bar"]]:
"""
Test literal.
Returns
-------
Literal['foo']
The literal string 'foo'. And the quote style (single) must match
the function signature.
Literal["bar"]
The literal string "bar". And the quote style (double) must match
the function signature.
"""
return "foo", 'bar'
2 changes: 2 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1127,6 +1127,8 @@ def testNonAscii() -> None:
],
),
('08_return_section_parsing/google.py', {'style': 'google'}, []),
('09_double_quotes_in_Literal/google.py', {'style': 'google'}, []),
('09_double_quotes_in_Literal/numpy.py', {'style': 'numpy'}, []),
],
)
def testEdgeCases(
Expand Down
20 changes: 19 additions & 1 deletion tests/utils/test_generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import pytest

from pydoclint.utils.generic import collectFuncArgs, stripQuotes
from pydoclint.utils.generic import collectFuncArgs, specialEqual, stripQuotes

src1 = """
def func1(
Expand Down Expand Up @@ -91,3 +91,21 @@ def testCollectFuncArgs(src: str, expected: List[str]) -> None:
)
def testStripQuotes(string: str, expected: str) -> None:
assert stripQuotes(string) == expected


@pytest.mark.parametrize(
'str1, str2, expected',
[
('', '', True), # truly equal
('"', '"', True), # truly equal
("'", "'", True), # truly equal
('"', "'", True),
('Hello" world\' 123', 'Hello" world\' 123', True), # truly equal
('Hello" world\' 123', "Hello' world' 123", True),
('Hello" world\' 123', 'Hello\' world" 123', True),
('Hello" world\' 123', "Hello' world` 123", False),
('Hello" world\' 123', 'Hello\' world" 1234', False),
],
)
def testSpecialEqual(str1: str, str2: str, expected: bool) -> None:
assert specialEqual(str1, str2) == expected

0 comments on commit 70eb3c8

Please sign in to comment.