From 5e24b6e291a2c8fbd6e0b6af46c898a4efc69349 Mon Sep 17 00:00:00 2001 From: jsh9 <25124332+jsh9@users.noreply.github.com> Date: Tue, 22 Aug 2023 01:35:18 -0700 Subject: [PATCH] Better handling of backslash in docstring (#74) --- CHANGELOG.md | 10 +++++ pydoclint/utils/arg.py | 36 ++++++++++++---- setup.cfg | 2 +- .../data/edge_cases/05_escape_char/google.py | 29 +++++++++++++ tests/data/edge_cases/05_escape_char/numpy.py | 41 +++++++++++++++++++ .../data/edge_cases/05_escape_char/sphinx.py | 36 ++++++++++++++++ tests/test_main.py | 3 ++ tests/utils/test_arg.py | 2 +- 8 files changed, 148 insertions(+), 11 deletions(-) create mode 100644 tests/data/edge_cases/05_escape_char/google.py create mode 100644 tests/data/edge_cases/05_escape_char/numpy.py create mode 100644 tests/data/edge_cases/05_escape_char/sphinx.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 34c8b40..8efde91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Change Log +## [0.2.2] - 2023-08-22 + +- Improved + + - Improved handling of escape symbol (`\`) in docstrings + (https://github.com/jsh9/pydoclint/issues/73) + +- Full diff + - https://github.com/jsh9/pydoclint/compare/0.2.1...0.2.2 + ## [0.2.1] - 2023-08-21 - Improved diff --git a/pydoclint/utils/arg.py b/pydoclint/utils/arg.py index 8872b7e..ff3e26b 100644 --- a/pydoclint/utils/arg.py +++ b/pydoclint/utils/arg.py @@ -29,11 +29,13 @@ def __repr__(self) -> str: def __str__(self) -> str: return f'{self.name}: {self.typeHint}' - def __eq__(self, o: 'Arg') -> bool: - if not isinstance(o, Arg): + def __eq__(self, other: 'Arg') -> bool: + if not isinstance(other, Arg): return False - return self.name == o.name and self._eq(self.typeHint, o.typeHint) + argNamesEqual: bool = self._argNamesEq(self.name, other.name) + typeHintsEqual: bool = self._typeHintsEq(self.typeHint, other.typeHint) + return argNamesEqual and typeHintsEqual def __lt__(self, other: 'Arg') -> bool: if not isinstance(other, Arg): @@ -91,7 +93,7 @@ def _str(cls, typeName: Optional[str]) -> str: return '' if typeName is None else typeName @classmethod - def _eq(cls, str1: str, str2: str) -> bool: + def _typeHintsEq(cls, hint1: str, hint2: str) -> bool: # We parse and then unparse so that cases like this can be # treated as equal: # @@ -103,16 +105,32 @@ def _eq(cls, str1: str, str2: str) -> bool: # >>> "ghi", # >>> ] try: - str1_: str = unparseAnnotation(ast.parse(stripQuotes(str1))) + hint1_: str = unparseAnnotation(ast.parse(stripQuotes(hint1))) except SyntaxError: - str1_ = str1 + hint1_ = hint1 try: - str2_: str = unparseAnnotation(ast.parse(stripQuotes(str2))) + hint2_: str = unparseAnnotation(ast.parse(stripQuotes(hint2))) except SyntaxError: - str2_ = str2 + hint2_ = hint2 + + return hint1_ == hint2_ + + @classmethod + def _argNamesEq(cls, name1: str, name2: str) -> bool: + return cls._removeEscapeChar(name1) == cls._removeEscapeChar(name2) - return str1_ == str2_ + @classmethod + def _removeEscapeChar(cls, string: str) -> str: + # We need to remove `\` from the arg names before comparing them, + # because when there are 1 or 2 trailing underscores in an argument, + # people need to use `\_` or `\_\_`, otherwise Sphinx will somehow + # not render the underscores (and for some reason, 3 or more trailing + # underscores are fine). + # + # For example: + # arg1\_\_ (int): The first argument + return string.replace('\\', '') class ArgList: diff --git a/setup.cfg b/setup.cfg index ea78505..a9269f6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pydoclint -version = 0.2.1 +version = 0.2.2 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/05_escape_char/google.py b/tests/data/edge_cases/05_escape_char/google.py new file mode 100644 index 0000000..7b545ef --- /dev/null +++ b/tests/data/edge_cases/05_escape_char/google.py @@ -0,0 +1,29 @@ +def myFunc( + arg1_: int, + arg2__: int, + arg3___: int, + arg4____: int, + some_thing_1_: int, + some_thing_2__: int, + some_thing_3___: int, + some_thing_4____: int, + some_thing_5_____: str, +) -> None: + r""" + Do something. + + Args: + arg1\_ (int): Arg + arg2\_\_ (int): Arg + arg3\_\_\_ (int): Arg + arg4\_\_\_\_ (int): Arg + some_thing_1\_ (int): Arg + some_thing_2\_\_ (int): Arg + some_thing_3\_\_\_ (int): Arg + some_thing_4\_\_\_\_ (int): Arg + some_thing_5_____ (str): Arg + + Returns: + None: Return value + """ + pass diff --git a/tests/data/edge_cases/05_escape_char/numpy.py b/tests/data/edge_cases/05_escape_char/numpy.py new file mode 100644 index 0000000..22ecbe1 --- /dev/null +++ b/tests/data/edge_cases/05_escape_char/numpy.py @@ -0,0 +1,41 @@ +def myFunc( + arg1_: int, + arg2__: int, + arg3___: int, + arg4____: int, + some_thing_1_: int, + some_thing_2__: int, + some_thing_3___: int, + some_thing_4____: int, + some_thing_5_____: str, +) -> None: + r""" + Do something. + + Parameters + ---------- + arg1\_ : int + Arg + arg2\_\_ : int + Arg + arg3\_\_\_ : int + Arg + arg4\_\_\_\_ : int + Arg + some_thing_1\_ : int + Arg + some_thing_2\_\_ : int + Arg + some_thing_3\_\_\_ : int + Arg + some_thing_4\_\_\_\_ : int + Arg + some_thing_5_____ : str + Arg + + Returns + ------- + None + Return value + """ + pass diff --git a/tests/data/edge_cases/05_escape_char/sphinx.py b/tests/data/edge_cases/05_escape_char/sphinx.py new file mode 100644 index 0000000..ab8fc97 --- /dev/null +++ b/tests/data/edge_cases/05_escape_char/sphinx.py @@ -0,0 +1,36 @@ +def myFunc( + arg1_: int, + arg2__: int, + arg3___: int, + arg4____: int, + some_thing_1_: int, + some_thing_2__: int, + some_thing_3___: int, + some_thing_4____: int, + some_thing_5_____: str, +) -> None: + r""" + Do something. + + :param arg1\_: Arg + :type arg1\_: int + :param arg2\_\_: Arg + :type arg2\_\_: int + :param arg3\_\_\_: Arg + :type arg3\_\_\_: int + :param arg4\_\_\_\_: Arg + :type arg4\_\_\_\_: int + :param some_thing_1\_: Arg + :type some_thing_1\_: int + :param some_thing_2\_\_: Arg + :type some_thing_2\_\_: int + :param some_thing_3\_\_\_: Arg + :type some_thing_3\_\_\_: int + :param some_thing_4\_\_\_\_: Arg + :type some_thing_4\_\_\_\_: int + :param some_thing_5_____: Arg + :type some_thing_5_____: str + :return: Return value + :rtype: None + """ + pass diff --git a/tests/test_main.py b/tests/test_main.py index 2ce420d..35bb0e6 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1058,6 +1058,9 @@ def testNonAscii() -> None: {'style': 'numpy'}, [], ), + ('05_escape_char/google.py', {'style': 'google'}, []), + ('05_escape_char/numpy.py', {'style': 'numpy'}, []), + ('05_escape_char/sphinx.py', {'style': 'sphinx'}, []), ], ) def testEdgeCases( diff --git a/tests/utils/test_arg.py b/tests/utils/test_arg.py index e5a0bf0..8cc07d5 100644 --- a/tests/utils/test_arg.py +++ b/tests/utils/test_arg.py @@ -138,7 +138,7 @@ def testArg_sorting(original: Set[Arg], after: List[Arg]) -> None: ], ) def testArg_eq(str1: str, str2: str, expected: bool) -> None: - assert Arg._eq(str1, str2) == expected + assert Arg._typeHintsEq(str1, str2) == expected @pytest.mark.parametrize(