summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog4
-rw-r--r--pylint/extensions/_check_docs_utils.py30
-rw-r--r--pylint/extensions/docparams.py6
-rw-r--r--pylint/test/extensions/test_check_raise_docs.py200
4 files changed, 226 insertions, 14 deletions
diff --git a/ChangeLog b/ChangeLog
index d4ed3616c..09d82e474 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -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