forked from TheAlgorithms/algorithms-keeper
-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrequire_doctest.py
295 lines (256 loc) · 7.96 KB
/
require_doctest.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
from typing import Union
import libcst as cst
import libcst.matchers as m
from fixit import CstContext, CstLintRule
from fixit import InvalidTestCase as Invalid
from fixit import ValidTestCase as Valid
MISSING_DOCTEST: str = (
"As there is no test file in this pull request nor any test function or class in "
"the file `{filepath}`, please provide doctest for the function `{nodename}`"
)
INIT: str = "__init__"
class RequireDoctestRule(CstLintRule):
VALID = [
# Module-level docstring contains doctest.
Valid(
"""
'''
Module-level docstring contains doctest
>>> foo()
None
'''
def foo():
pass
class Bar:
def baz(self):
pass
def bar():
pass
"""
),
# Module contains a test function.
Valid(
"""
def foo():
pass
def bar():
pass
# Contains a test function
def test_foo():
pass
class Baz:
def baz(self):
pass
def spam():
pass
"""
),
# Module contains multiple test function.
Valid(
"""
def foo():
pass
def bar():
pass
def test_foo():
pass
def test_bar():
pass
class Baz:
def baz(self):
pass
def spam():
pass
"""
),
# Module contains a test class.
Valid(
"""
def foo():
pass
class Baz:
def baz(self):
pass
def bar():
pass
# Contains a test class
class TestSpam:
def test_spam(self):
pass
def egg():
pass
"""
),
# Class level docstring contains doctest, so skip doctest checking only
# for that class.
Valid(
"""
def foo():
'''
>>> foo()
'''
pass
class Spam:
'''
Class-level docstring contains doctest
>>> Spam()
'''
def foo(self):
pass
def spam(self):
pass
def bar():
'''
>>> bar()
'''
pass
"""
),
# No doctest required for the ``__init__`` function.
Valid(
"""
def spam():
'''
>>> spam()
'''
pass
class Bar:
# No doctest needed for the init function
def __init__(self):
pass
def bar(self):
'''
>>> bar()
'''
pass
"""
),
# No doctest required in the ``web_programming`` directory.
Valid(
"""
def foo():
pass
""",
filename="web_programming/foo.py",
),
]
INVALID = [
Invalid(
"""
def bar():
pass
"""
),
# Only the ``__init__`` function does not require doctest.
Invalid(
"""
def foo():
'''
>>> foo()
'''
pass
class Spam:
def __init__(self):
pass
def spam(self):
pass
"""
),
# Check that `_skip_doctest` attribute is reset after leaving the class.
Invalid(
"""
def bar():
'''
>>> bar()
'''
pass
class Spam:
'''
>>> Spam()
'''
def spam():
pass
def egg():
pass
"""
),
]
def __init__(self, context: CstContext) -> None:
super().__init__(context)
self._skip_doctest: bool = False
self._temporary: bool = False
def should_skip_file(self) -> bool:
return self.context.file_path.match("web_programming/*")
def visit_Module(self, node: cst.Module) -> None:
self._skip_doctest = self._has_testnode(node) or self._has_doctest(node)
def visit_ClassDef(self, node: cst.ClassDef) -> None:
# Temporary storage of the ``skip_doctest`` value only during the class visit.
# If the class-level docstring contains doctest, then the checks should only be
# skipped for all its methods and not for other functions/class in the module.
# After leaving the class, ``skip_doctest`` should be reset to whatever the
# value was before.
self._temporary = self._skip_doctest
self._skip_doctest = self._has_doctest(node)
def leave_ClassDef(self, original_node: cst.ClassDef) -> None:
self._skip_doctest = self._temporary
def visit_FunctionDef(self, node: cst.FunctionDef) -> None:
nodename = node.name.value
if nodename != INIT and not self._has_doctest(node):
self.report(
node,
MISSING_DOCTEST.format(
filepath=self.context.file_path, nodename=nodename
),
)
def _has_doctest(
self, node: Union[cst.Module, cst.ClassDef, cst.FunctionDef]
) -> bool:
"""Check whether the given node contains doctests.
If the ``_skip_doctest`` attribute is ``True``, the function will by default
return ``True``, otherwise it will extract the docstring and look for doctest
patterns (>>> ) in it. If there is no docstring for the node, this will mean
the absence of doctest.
"""
if not self._skip_doctest:
docstring = node.get_docstring()
if docstring is not None:
for line in docstring.splitlines():
if line.strip().startswith(">>> "):
return True
return False
return True
@staticmethod
def _has_testnode(node: cst.Module) -> bool:
return m.matches(
node,
m.Module(
body=[
# Sequence wildcard matchers matches LibCAST nodes in a row in a
# sequence. It does not implicitly match on partial sequences. So,
# when matching against a sequence we will need to provide a
# complete pattern. This often means using helpers such as
# ``ZeroOrMore()`` as the first and last element of the sequence.
m.ZeroOrMore(),
m.AtLeastN(
n=1,
matcher=m.OneOf(
m.FunctionDef(
name=m.Name(
value=m.MatchIfTrue(
lambda value: value.startswith("test_")
)
)
),
m.ClassDef(
name=m.Name(
value=m.MatchIfTrue(
lambda value: value.startswith("Test")
)
)
),
),
),
m.ZeroOrMore(),
]
),
)