Skip to content

Commit

Permalink
Merge pull request #105 from life4/lint-exc-docstring
Browse files Browse the repository at this point in the history
Linter: extract exceptions from docstrings
  • Loading branch information
orsinium committed Mar 16, 2022
2 parents 5fa4e19 + 2d8ba8c commit b2e460f
Show file tree
Hide file tree
Showing 84 changed files with 4,589 additions and 5 deletions.
81 changes: 78 additions & 3 deletions deal/linter/_extractors/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import ast
import builtins
import re
from inspect import cleandoc
from typing import Iterator, Optional, Union

import astroid
Expand All @@ -9,8 +11,14 @@
from .common import TOKENS, Extractor, Token, get_full_name, get_name, get_stub, infer
from .contracts import get_contracts

try:
import docstring_parser
except ImportError:
docstring_parser = None


get_exceptions = Extractor()
REX_GOOGLE_SECTION = re.compile(r'[A-Z][a-z]+:\s*')


@get_exceptions.register(*TOKENS.ASSERT)
Expand Down Expand Up @@ -90,7 +98,7 @@ def _exceptions_from_stubs(expr: astroid.Call, stubs: StubsManager) -> Iterator[
names = stub.get(func=func_name, contract=Category.RAISES)
for name in names:
name = getattr(builtins, name, name)
yield Token(value=name, line=expr.lineno, col=expr.col_offset)
yield Token(value=name)


def _exceptions_from_func(expr: Union[ast.Call, astroid.Call]) -> Iterator[Token]:
Expand All @@ -100,15 +108,82 @@ def _exceptions_from_func(expr: Union[ast.Call, astroid.Call]) -> Iterator[Token

# recursively infer exceptions from the function body
for error in get_exceptions(body=value.body, dive=False):
yield Token(value=error.value, line=expr.lineno, col=expr.col_offset)
yield Token(value=error.value)

# get explicitly specified exceptions from `@deal.raises`
name: Optional[str]
for contract in get_contracts(value):
if contract.name != 'raises':
continue
for arg in contract.args:
name = get_name(arg)
if name is None:
continue
yield Token(value=name, line=expr.lineno, col=expr.col_offset)
name = getattr(builtins, name, name)
yield Token(value=name)

# get exceptions from the docstring
name: str
for name in _excs_from_doc(value.doc):
name = getattr(builtins, name, name)
yield Token(value=name)
return None


# TODO: use it on the target function docstring too
def _excs_from_doc(doc: Optional[str]) -> Iterator[str]:
if doc is None:
return

if docstring_parser is not None:
parsed = docstring_parser.parse(doc)
for exc_info in parsed.raises:
if exc_info.type_name:
yield exc_info.type_name
return

google_section = ''
numpy_section = ''
google_section_indent = 4
numpy_section_indent = 0
lines = cleandoc(doc).splitlines() + ['']
for line, next_line in zip(lines, lines[1:]):
words = line.split()
if not words:
continue
indent = _get_indent(line)
is_numpy_header = _is_header_highlight(next_line)

# sphinx and epydoc docstring
if len(words) >= 2 and words[0] in (':raises', '@raise'):
yield words[1].rstrip(':')
continue

# google docstring
if REX_GOOGLE_SECTION.fullmatch(line) and not is_numpy_header:
google_section = line.strip().rstrip(':').lower()
google_section_indent = _get_indent(next_line)
continue
if google_section == 'raises' and indent == google_section_indent:
yield words[0].rstrip(':')
continue

# numpy docstring
next_line = next_line.strip()
if is_numpy_header:
numpy_section = line.strip().rstrip(':').lower()
continue
if _is_header_highlight(line):
numpy_section_indent = _get_indent(next_line)
continue
if numpy_section == 'raises' and indent == numpy_section_indent:
yield line.rstrip()


def _get_indent(line: str) -> int:
return len(line) - len(line.lstrip())


def _is_header_highlight(line: str) -> bool:
line = line.strip()
return len(set(line)) == 1 and line[0] in '-+'
7 changes: 7 additions & 0 deletions deal/linter/stubs/numpy/_globals.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"_CopyMode.__bool__": {
"raises": [
"ValueError"
]
}
}
19 changes: 19 additions & 0 deletions deal/linter/stubs/numpy/_pytesttester.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"PytestTester.__call__": {
"has": [
"import",
"stdout"
],
"raises": [
"AssertionError",
"TypeError",
"ValueError"
]
},
"_show_numpy_info": {
"has": [
"import",
"stdout"
]
}
}
41 changes: 41 additions & 0 deletions deal/linter/stubs/numpy/_version.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"git_pieces_from_vcs": {
"has": [
"stdout"
],
"raises": [
"AssertionError",
"NotThisMethod"
]
},
"git_versions_from_keywords": {
"has": [
"stdout"
],
"raises": [
"NotThisMethod"
]
},
"render": {
"raises": [
"ValueError"
]
},
"run_command": {
"has": [
"stdout"
],
"raises": [
"AssertionError"
]
},
"versions_from_parentdir": {
"has": [
"stdout"
],
"raises": [
"NotThisMethod",
"TypeError"
]
}
}
110 changes: 110 additions & 0 deletions deal/linter/stubs/numpy/array_api/_array_object.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
{
"Array.T": {
"raises": [
"ValueError"
]
},
"Array.__abs__": {
"raises": [
"TypeError"
]
},
"Array.__array_namespace__": {
"raises": [
"ValueError"
]
},
"Array.__bool__": {
"raises": [
"TypeError",
"ValueError"
]
},
"Array.__float__": {
"raises": [
"TypeError",
"ValueError"
]
},
"Array.__getitem__": {
"raises": [
"IndexError"
]
},
"Array.__imatmul__": {
"raises": [
"ValueError"
]
},
"Array.__int__": {
"raises": [
"TypeError",
"ValueError"
]
},
"Array.__invert__": {
"raises": [
"TypeError"
]
},
"Array.__neg__": {
"raises": [
"TypeError"
]
},
"Array.__new__": {
"raises": [
"TypeError"
]
},
"Array.__pos__": {
"raises": [
"TypeError"
]
},
"Array.__pow__": {
"has": [
"import"
]
},
"Array.__rpow__": {
"has": [
"import"
]
},
"Array.__setitem__": {
"raises": [
"IndexError"
]
},
"Array._check_allowed_dtypes": {
"raises": [
"TypeError"
]
},
"Array._new": {
"raises": [
"TypeError"
]
},
"Array._promote_scalar": {
"raises": [
"TypeError"
]
},
"Array._validate_index": {
"raises": [
"IndexError"
]
},
"Array.mT": {
"has": [
"import"
]
},
"Array.to_device": {
"raises": [
"ValueError"
]
}
}
Loading

0 comments on commit b2e460f

Please sign in to comment.