forked from oppia/oppia
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdocstrings_checker.py
320 lines (259 loc) · 10.7 KB
/
docstrings_checker.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
# coding: utf-8
#
# Copyright 2018 The Oppia Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS-IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utility methods for docstring checking."""
from __future__ import annotations
import ast
import os
import re
import sys
from core import python_utils
from scripts import common
_PARENT_DIR = os.path.abspath(os.path.join(os.getcwd(), os.pardir))
_PYLINT_PATH = os.path.join(
_PARENT_DIR, 'oppia_tools', 'pylint-%s' % common.PYLINT_VERSION)
sys.path.insert(0, _PYLINT_PATH)
import astroid # isort:skip pylint: disable=wrong-import-order, wrong-import-position
from pylint.checkers import utils # isort:skip pylint: disable=wrong-import-order, wrong-import-position
from pylint.extensions import _check_docs_utils # isort:skip pylint: disable=wrong-import-order, wrong-import-position
def space_indentation(s):
"""The number of leading spaces in a string
Args:
s: str. The input string.
Returns:
int. The number of leading spaces.
"""
return len(s) - len(s.lstrip(' '))
def get_setters_property_name(node):
"""Get the name of the property that the given node is a setter for.
Args:
node: str. The node to get the property name for.
Returns:
str|None. The name of the property that the node is a setter for,
or None if one could not be found.
"""
decorator_nodes = node.decorators.nodes if node.decorators else []
for decorator_node in decorator_nodes:
if (isinstance(decorator_node, astroid.Attribute) and
decorator_node.attrname == 'setter' and
isinstance(decorator_node.expr, astroid.Name)):
return decorator_node.expr.name
return None
def get_setters_property(node):
"""Get the property node for the given setter node.
Args:
node: astroid.FunctionDef. The node to get the property for.
Returns:
astroid.FunctionDef|None. The node relating to the property of
the given setter node, or None if one could not be found.
"""
setters_property = None
property_name = get_setters_property_name(node)
class_node = utils.node_frame_class(node)
if property_name and class_node:
class_attrs = class_node.getattr(node.name)
for attr in class_attrs:
if utils.decorated_with_property(attr):
setters_property = attr
break
return setters_property
def returns_something(return_node):
"""Check if a return node returns a value other than None.
Args:
return_node: astroid.Return. The return node to check.
Returns:
bool. True if the return node returns a value other than None, False
otherwise.
"""
returns = return_node.value
if returns is None:
return False
return not (isinstance(returns, astroid.Const) and returns.value is None)
def possible_exc_types(node):
"""Gets all of the possible raised exception types for the given raise node.
Caught exception types are ignored.
Args:
node: astroid.node_classes.NodeNG. The raise
to find exception types for.
Returns:
set(str). A list of exception types.
"""
excs = []
if isinstance(node.exc, astroid.Name):
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)
if isinstance(target, astroid.ClassDef):
excs = [target.name]
elif isinstance(target, astroid.FunctionDef):
for ret in target.nodes_of_class(astroid.Return):
if ret.frame() != target:
continue
val = utils.safe_infer(ret.value)
if (val and isinstance(val, (
astroid.Instance, astroid.ClassDef)) 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 set(
exc for exc in excs if not utils.node_ignores_exception(
node, exc))
except astroid.InferenceError:
return set()
def docstringify(docstring):
"""Converts a docstring in its str form to its Docstring object
as defined in the pylint library.
Args:
docstring: str. Docstring for a particular class or function.
Returns:
Docstring. Pylint Docstring class instance representing
a node's docstring.
"""
for docstring_type in [GoogleDocstring]:
instance = docstring_type(docstring)
if instance.is_valid():
return instance
return _check_docs_utils.Docstring(docstring)
class GoogleDocstring(_check_docs_utils.GoogleDocstring):
"""Class for checking whether docstrings follow the Google Python Style
Guide.
"""
re_multiple_type = _check_docs_utils.GoogleDocstring.re_multiple_type
re_param_line = re.compile(
r"""
\s* \*{{0,2}}(\w+) # identifier potentially with asterisks
\s* ( [:]
\s*
({type}|\S*)
(?:,\s+optional)?
[.] )? \s* # optional type declaration
\s* (.*) # beginning of optional description
""".format(
type=re_multiple_type,
), flags=re.X | re.S | re.M)
re_returns_line = re.compile(
r"""
\s* (({type}|\S*).)? # identifier
\s* (.*) # beginning of description
""".format(
type=re_multiple_type,
), flags=re.X | re.S | re.M)
re_yields_line = re_returns_line
re_raise_line = re.compile(
r"""
\s* ({type}|\S*)?[.:] # identifier
\s* (.*) # beginning of description
""".format(
type=re_multiple_type,
), flags=re.X | re.S | re.M)
class ASTDocStringChecker:
"""Checks that docstrings meet the code style."""
def __init__(self):
pass
@classmethod
def get_args_list_from_function_definition(cls, function_node):
"""Extracts the arguments from a function definition.
Ignores class specific arguments (self and cls).
Args:
function_node: ast.FunctionDef. Represents a function.
Returns:
list(str). The args for a function as listed in the function
definition.
"""
# Ignore self and cls args.
args_to_ignore = ['self', 'cls']
return python_utils.get_args_of_function_node(
function_node, args_to_ignore)
@classmethod
def build_regex_from_args(cls, function_args):
"""Builds a regex string from a function's arguments to match against
the docstring. Ensures the docstring contains an 'Args' header, and
each of the arguments are listed, followed by a colon, separated by new
lines, and are listed in the correct order.
Args:
function_args: list(str). The arguments for a function.
Returns:
str. A regex that checks for an "Arg" header and then each arg term
with a colon in order with any characters in between.
The resulting regex looks like this (the backslashes are escaped):
(Args:)[\\S\\s]*(arg_name0:)[\\S\\s]*(arg_name1:)
If passed an empty list, returns None.
"""
if len(function_args) > 0:
formatted_args = ['({}:)'.format(arg) for arg in function_args]
return r'(Args:)[\S\s]*' + r'[\S\s]*'.join(formatted_args)
@classmethod
def compare_arg_order(cls, func_def_args, docstring):
"""Compares the arguments listed in the function definition and
docstring, and raises errors if there are missing or mis-ordered
arguments in the docstring.
Args:
func_def_args: list(str). The args as listed in the function
definition.
docstring: str. The contents of the docstring under the Args
header.
Returns:
list(str). Each str contains an error message. If no linting
errors were found, the list will be empty.
"""
results = []
# If there is no docstring or it doesn't have an Args section, exit
# without errors.
if docstring is None or 'Args' not in docstring:
return results
# First check that each arg is in the docstring.
for arg_name in func_def_args:
arg_name_colon = arg_name + ':'
if arg_name_colon not in docstring:
if arg_name not in docstring:
results.append('Arg missing from docstring: {}'.format(
arg_name))
else:
results.append('Arg not followed by colon: {}'.format(
arg_name))
# Only check ordering if there's more than one argument in the
# function definition, and no other errors have been found.
if len(func_def_args) > 0 and len(results) == 0:
regex_pattern = cls.build_regex_from_args(func_def_args)
regex_result = re.search(regex_pattern, docstring)
if regex_result is None:
results.append('Arg ordering error in docstring.')
return results
@classmethod
def check_docstrings_arg_order(cls, function_node):
"""Extracts the arguments from a function definition.
Args:
function_node: ast node object. Represents a function.
Returns:
list(str). List of docstring errors associated with
the function. If the function has no errors, the list is empty.
"""
func_def_args = cls.get_args_list_from_function_definition(
function_node)
docstring = ast.get_docstring(function_node)
func_result = cls.compare_arg_order(func_def_args, docstring)
return func_result