diff options
-rw-r--r-- | ChangeLog | 4 | ||||
-rw-r--r-- | pylint/extensions/_check_docs_utils.py | 30 | ||||
-rw-r--r-- | pylint/extensions/docparams.py | 6 | ||||
-rw-r--r-- | pylint/test/extensions/test_check_raise_docs.py | 200 |
4 files changed, 226 insertions, 14 deletions
@@ -16,6 +16,10 @@ Release date: TBA Close #2635 +* Fix missing-raises-doc false positive (W9006) + + Close #1502 + * Exempt starred unpacking from ``*-not-iterating`` Python 3 checks Close #2651 diff --git a/pylint/extensions/_check_docs_utils.py b/pylint/extensions/_check_docs_utils.py index 2aaa70962..a01cd2b18 100644 --- a/pylint/extensions/_check_docs_utils.py +++ b/pylint/extensions/_check_docs_utils.py @@ -99,6 +99,14 @@ def returns_something(return_node): return not (isinstance(returns, astroid.Const) and returns.value is None) +def _get_raise_target(node): + if isinstance(node.exc, astroid.Call): + func = node.exc.func + if isinstance(func, (astroid.Name, astroid.Attribute)): + return utils.safe_infer(func) + return None + + def possible_exc_types(node): """ Gets all of the possible raised exception types for the given raise node. @@ -119,8 +127,16 @@ def possible_exc_types(node): inferred = utils.safe_infer(node.exc) if inferred: excs = [inferred.name] - elif isinstance(node.exc, astroid.Call) and isinstance(node.exc.func, astroid.Name): - target = utils.safe_infer(node.exc.func) + elif node.exc is None: + handler = node.parent + while handler and not isinstance(handler, astroid.ExceptHandler): + handler = handler.parent + + if handler and handler.type: + inferred_excs = astroid.unpack_infer(handler.type) + excs = (exc.name for exc in inferred_excs if exc is not astroid.Uninferable) + else: + target = _get_raise_target(node) if isinstance(target, astroid.ClassDef): excs = [target.name] elif isinstance(target, astroid.FunctionDef): @@ -136,14 +152,6 @@ def possible_exc_types(node): and utils.inherit_from_std_ex(val) ): excs.append(val.name) - elif node.exc is None: - handler = node.parent - while handler and not isinstance(handler, astroid.ExceptHandler): - handler = handler.parent - - if handler and handler.type: - inferred_excs = astroid.unpack_infer(handler.type) - excs = (exc.name for exc in inferred_excs if exc is not astroid.Uninferable) try: return {exc for exc in excs if not utils.node_ignores_exception(node, exc)} @@ -292,7 +300,7 @@ class SphinxDocstring(Docstring): \s+ )? - (\w+) # Parameter name + (\w(?:\w|\.[^\.])+) # Parameter name can include '.', e.g. re.error \s* # whitespace : # final colon """.format( diff --git a/pylint/extensions/docparams.py b/pylint/extensions/docparams.py index d55515a56..c464e0d0a 100644 --- a/pylint/extensions/docparams.py +++ b/pylint/extensions/docparams.py @@ -268,8 +268,10 @@ class DocstringParameterChecker(BaseChecker): self._handle_no_raise_doc(expected_excs, func_node) return - found_excs = doc.exceptions() - missing_excs = expected_excs - found_excs + found_excs_full_names = doc.exceptions() + # Extract just the class name, e.g. "error" from "re.error" + found_excs_class_names = {exc.split(".")[-1] for exc in found_excs_full_names} + missing_excs = expected_excs - found_excs_class_names self._add_raise_message(missing_excs, func_node) def visit_return(self, node): diff --git a/pylint/test/extensions/test_check_raise_docs.py b/pylint/test/extensions/test_check_raise_docs.py index 5c7920d20..5085b0b24 100644 --- a/pylint/test/extensions/test_check_raise_docs.py +++ b/pylint/test/extensions/test_check_raise_docs.py @@ -92,6 +92,70 @@ class TestDocstringCheckerRaise(CheckerTestCase): args=('RuntimeError', ))): self.checker.visit_raise(raise_node) + def test_find_google_attr_raises_exact_exc(self): + raise_node = astroid.extract_node(''' + def my_func(self): + """This is a google docstring. + + Raises: + re.error: Sometimes + """ + import re + raise re.error('hi') #@ + ''') + with self.assertNoMessages(): + self.checker.visit_raise(raise_node) + pass + + def test_find_google_attr_raises_substr_exc(self): + raise_node = astroid.extract_node(''' + def my_func(self): + """This is a google docstring. + + Raises: + re.error: Sometimes + """ + from re import error + raise error('hi') #@ + ''') + with self.assertNoMessages(): + self.checker.visit_raise(raise_node) + + def test_find_valid_missing_google_attr_raises(self): + node = astroid.extract_node(''' + def my_func(self): + """This is a google docstring. + + Raises: + re.anothererror: Sometimes + """ + from re import error + raise error('hi') + ''') + raise_node = node.body[1] + with self.assertAddsMessages( + Message( + msg_id='missing-raises-doc', + node=node, + args=('error', ))): + self.checker.visit_raise(raise_node) + + def test_find_invalid_missing_google_attr_raises(self): + raise_node = astroid.extract_node(''' + def my_func(self): + """This is a google docstring. + + Raises: + bogusmodule.error: Sometimes + """ + from re import error + raise error('hi') #@ + ''') + # pylint allows this to pass since the comparison between Raises and + # raise are based on the class name, not the qualified name. + with self.assertNoMessages(): + self.checker.visit_raise(raise_node) + def test_find_missing_numpy_raises(self): node = astroid.extract_node(''' def my_func(self): @@ -357,7 +421,7 @@ class TestDocstringCheckerRaise(CheckerTestCase): def test_ignores_caught_numpy_raises(self): raise_node = astroid.extract_node(''' def my_func(self): - """This is a docstring. + """This is a numpy docstring. Raises ------ @@ -374,6 +438,79 @@ class TestDocstringCheckerRaise(CheckerTestCase): with self.assertNoMessages(): self.checker.visit_raise(raise_node) + def test_find_numpy_attr_raises_exact_exc(self): + raise_node = astroid.extract_node(''' + def my_func(self): + """This is a numpy docstring. + + Raises + ------ + re.error + Sometimes + """ + import re + raise re.error('hi') #@ + ''') + with self.assertNoMessages(): + self.checker.visit_raise(raise_node) + pass + + def test_find_numpy_attr_raises_substr_exc(self): + raise_node = astroid.extract_node(''' + def my_func(self): + """This is a numpy docstring. + + Raises + ------ + re.error + Sometimes + """ + from re import error + raise error('hi') #@ + ''') + with self.assertNoMessages(): + self.checker.visit_raise(raise_node) + + def test_find_valid_missing_numpy_attr_raises(self): + node = astroid.extract_node(''' + def my_func(self): + """This is a numpy docstring. + + Raises + ------ + re.anothererror + Sometimes + """ + from re import error + raise error('hi') + ''') + raise_node = node.body[1] + with self.assertAddsMessages( + Message( + msg_id='missing-raises-doc', + node=node, + args=('error', ))): + self.checker.visit_raise(raise_node) + + def test_find_invalid_missing_numpy_attr_raises(self): + raise_node = astroid.extract_node(''' + def my_func(self): + """This is a numpy docstring. + + Raises + ------ + bogusmodule.error + Sometimes + """ + from re import error + raise error('hi') #@ + ''') + # pylint allows this to pass since the comparison between Raises and + # raise are based on the class name, not the qualified name. + with self.assertNoMessages(): + self.checker.visit_raise(raise_node) + + def test_find_missing_sphinx_raises_infer_from_instance(self): raise_node = astroid.extract_node(''' def my_func(self): @@ -413,6 +550,67 @@ class TestDocstringCheckerRaise(CheckerTestCase): args=('RuntimeError', ))): self.checker.visit_raise(raise_node) + def test_find_sphinx_attr_raises_exact_exc(self): + raise_node = astroid.extract_node(''' + def my_func(self): + """This is a sphinx docstring. + + :raises re.error: Sometimes + """ + import re + raise re.error('hi') #@ + ''') + with self.assertNoMessages(): + self.checker.visit_raise(raise_node) + pass + + def test_find_sphinx_attr_raises_substr_exc(self): + raise_node = astroid.extract_node(''' + def my_func(self): + """This is a sphinx docstring. + + :raises re.error: Sometimes + """ + from re import error + raise error('hi') #@ + ''') + with self.assertNoMessages(): + self.checker.visit_raise(raise_node) + + def test_find_valid_missing_sphinx_attr_raises(self): + node = astroid.extract_node(''' + def my_func(self): + """This is a sphinx docstring. + + :raises re.anothererror: Sometimes + """ + from re import error + raise error('hi') + ''') + raise_node = node.body[1] + with self.assertAddsMessages( + Message( + msg_id='missing-raises-doc', + node=node, + args=('error', ))): + self.checker.visit_raise(raise_node) + + def test_find_invalid_missing_sphinx_attr_raises(self): + raise_node = astroid.extract_node(''' + def my_func(self): + """This is a sphinx docstring. + + :raises bogusmodule.error: Sometimes + """ + from re import error + raise error('hi') #@ + ''') + # pylint allows this to pass since the comparison between Raises and + # raise are based on the class name, not the qualified name. + with self.assertNoMessages(): + self.checker.visit_raise(raise_node) + + def test_ignores_raise_uninferable(self): raise_node = astroid.extract_node(''' from unknown import Unknown |