1
1
import os
2
2
import sys
3
3
import inspect
4
+ from collections import defaultdict
4
5
5
6
import astroid
6
7
from pylint .checkers .variables import VariablesChecker
8
+ from pylint .checkers .typecheck import TypeChecker
7
9
import pylint
8
10
import pytest
9
11
10
12
11
13
def _is_pytest_mark_usefixtures (decorator ):
12
14
# expecting @pytest.mark.usefixture(...)
13
15
try :
14
- if isinstance (decorator , astroid .Call ):
15
- if decorator .func .attrname == 'usefixtures' and \
16
- decorator .func .expr .attrname == 'mark' and \
17
- decorator .func .expr .expr .name == 'pytest' :
18
- return True
16
+ if isinstance (decorator , astroid .Call ) and \
17
+ decorator .func .attrname == 'usefixtures' and \
18
+ decorator .func .expr .attrname == 'mark' and \
19
+ decorator .func .expr .expr .name == 'pytest' :
20
+ return True
19
21
except AttributeError :
20
22
pass
21
23
return False
@@ -42,6 +44,31 @@ def _is_pytest_fixture(decorator):
42
44
return False
43
45
44
46
47
+ def _is_class_autouse_fixture (function ):
48
+ try :
49
+ for decorator in function .decorators .nodes :
50
+ if isinstance (decorator , astroid .Call ):
51
+ func = decorator .func
52
+
53
+ if func and func .attrname in ('fixture' , 'yield_fixture' ) \
54
+ and func .expr .name == 'pytest' :
55
+
56
+ is_class = is_autouse = False
57
+
58
+ for kwarg in decorator .keywords or []:
59
+ if kwarg .arg == 'scope' and kwarg .value .value == 'class' :
60
+ is_class = True
61
+ if kwarg .arg == 'autouse' and kwarg .value .value is True :
62
+ is_autouse = True
63
+
64
+ if is_class and is_autouse :
65
+ return True
66
+ except AttributeError :
67
+ pass
68
+
69
+ return False
70
+
71
+
45
72
def _can_use_fixture (function ):
46
73
if isinstance (function , astroid .FunctionDef ):
47
74
@@ -85,21 +112,26 @@ def pytest_sessionfinish(self, session):
85
112
self .fixtures = session ._fixturemanager ._arg2fixturedefs
86
113
87
114
88
- ORIGINAL = {}
115
+ ORIGINAL = defaultdict ( dict )
89
116
90
117
91
118
def unregister ():
92
- VariablesChecker .add_message = ORIGINAL ['add_message' ]
93
- del ORIGINAL ['add_message' ]
94
- VariablesChecker .visit_functiondef = ORIGINAL ['visit_functiondef' ]
95
- del ORIGINAL ['visit_functiondef' ]
96
- VariablesChecker .visit_module = ORIGINAL ['visit_module' ]
97
- del ORIGINAL ['visit_module' ]
119
+ VariablesChecker .add_message = ORIGINAL ['VariablesChecker' ]['add_message' ]
120
+ VariablesChecker .visit_functiondef = ORIGINAL ['VariablesChecker' ]['visit_functiondef' ]
121
+ VariablesChecker .visit_module = ORIGINAL ['VariablesChecker' ]['visit_module' ]
122
+ del ORIGINAL ['VariablesChecker' ]
123
+ TypeChecker .in_setup = False
124
+ TypeChecker .request_cls = set ()
125
+ TypeChecker .class_node = None
126
+ TypeChecker .visit_assignattr = ORIGINAL ['TypeChecker' ]['visit_assignattr' ]
127
+ TypeChecker .visit_assign = ORIGINAL ['TypeChecker' ]['visit_assign' ]
128
+ TypeChecker .visit_functiondef = ORIGINAL ['TypeChecker' ]['visit_functiondef' ]
129
+ del ORIGINAL ['TypeChecker' ]
98
130
99
131
100
132
# pylint: disable=protected-access
101
133
def register (_ ):
102
- '''Patch VariablesChecker to add additional checks for pytest fixtures
134
+ '''Patch Pylint Checker classes to add additional checks for pytest
103
135
'''
104
136
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
105
137
def patched_visit_module (self , node ):
@@ -134,8 +166,8 @@ def patched_visit_module(self, node):
134
166
# restore output devices
135
167
sys .stdout , sys .stderr = stdout , stderr
136
168
137
- ORIGINAL ['visit_module' ](self , node )
138
- ORIGINAL ['visit_module' ] = VariablesChecker .visit_module
169
+ ORIGINAL ['VariablesChecker' ][ ' visit_module' ](self , node )
170
+ ORIGINAL ['VariablesChecker' ][ ' visit_module' ] = VariablesChecker .visit_module
139
171
VariablesChecker .visit_module = patched_visit_module
140
172
# <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
141
173
@@ -156,8 +188,8 @@ def patched_visit_functiondef(self, node):
156
188
for arg in node .args .args :
157
189
self ._invoked_with_func_args .add (arg .name )
158
190
159
- ORIGINAL ['visit_functiondef' ](self , node )
160
- ORIGINAL ['visit_functiondef' ] = VariablesChecker .visit_functiondef
191
+ ORIGINAL ['VariablesChecker' ][ ' visit_functiondef' ](self , node )
192
+ ORIGINAL ['VariablesChecker' ][ ' visit_functiondef' ] = VariablesChecker .visit_functiondef
161
193
VariablesChecker .visit_functiondef = patched_visit_functiondef
162
194
# <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
163
195
@@ -209,12 +241,65 @@ def patched_add_message(self, msgid, line=None, node=None, args=None,
209
241
return
210
242
211
243
if int (pylint .__version__ .split ('.' )[0 ]) >= 2 :
212
- ORIGINAL ['add_message' ](
244
+ ORIGINAL ['VariablesChecker' ][ ' add_message' ](
213
245
self , msgid , line , node , args , confidence , col_offset )
214
246
else :
215
247
# python2 + pylint1.9 backward compatibility
216
- ORIGINAL ['add_message' ](
248
+ ORIGINAL ['VariablesChecker' ][ ' add_message' ](
217
249
self , msgid , line , node , args , confidence )
218
- ORIGINAL ['add_message' ] = VariablesChecker .add_message
250
+ ORIGINAL ['VariablesChecker' ][ ' add_message' ] = VariablesChecker .add_message
219
251
VariablesChecker .add_message = patched_add_message
220
252
# <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
253
+
254
+ # remembering current state across visit_* methods
255
+ TypeChecker .in_setup = False
256
+ TypeChecker .request_cls = set ()
257
+ TypeChecker .class_node = None
258
+
259
+ # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
260
+ def visit_assignattr (self , node ):
261
+ if TypeChecker .in_setup and isinstance (node .expr , astroid .Name ) and \
262
+ node .expr .name in TypeChecker .request_cls and \
263
+ node .attrname not in TypeChecker .class_node .locals :
264
+ try :
265
+ # find Assign node which contains the source "value"
266
+ assign_node = node
267
+ while not isinstance (assign_node , astroid .Assign ):
268
+ assign_node = assign_node .parent
269
+
270
+ # hack class locals
271
+ TypeChecker .class_node .locals [node .attrname ] = [assign_node .value ]
272
+ except : # pylint: disable=bare-except
273
+ # cannot find valid assign expr, skipping the entire attribute
274
+ pass
275
+ ORIGINAL ['TypeChecker' ]['visit_assignattr' ](self , node )
276
+ ORIGINAL ['TypeChecker' ]['visit_assignattr' ] = TypeChecker .visit_assignattr
277
+ TypeChecker .visit_assignattr = visit_assignattr
278
+ # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
279
+
280
+ # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
281
+ def visit_assign (self , node ):
282
+ '''hijack visit_assign to store the aliases for `cls`'''
283
+ if TypeChecker .in_setup and isinstance (node .value , astroid .Attribute ) and \
284
+ node .value .attrname == 'cls' and node .value .expr .name == 'request' :
285
+ # storing the aliases for cls from request.cls
286
+ TypeChecker .request_cls = set (map (lambda t : t .name , node .targets ))
287
+ ORIGINAL ['TypeChecker' ]['visit_assign' ](self , node )
288
+ ORIGINAL ['TypeChecker' ]['visit_assign' ] = TypeChecker .visit_assign
289
+ TypeChecker .visit_assign = visit_assign
290
+ # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
291
+
292
+ # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
293
+ def visit_functiondef (self , node ):
294
+ '''determine if a method is class setup method'''
295
+ TypeChecker .in_setup = False
296
+ TypeChecker .request_cls = set ()
297
+ TypeChecker .class_node = None
298
+
299
+ if _can_use_fixture (node ) and _is_class_autouse_fixture (node ):
300
+ TypeChecker .in_setup = True
301
+ TypeChecker .class_node = node .parent
302
+ ORIGINAL ['TypeChecker' ]['visit_functiondef' ](self , node )
303
+ ORIGINAL ['TypeChecker' ]['visit_functiondef' ] = TypeChecker .visit_functiondef
304
+ TypeChecker .visit_functiondef = visit_functiondef
305
+ # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
0 commit comments