Skip to content

Commit 8f5ac85

Browse files
authored
Merge pull request #1001 from dodona-edu/chore/update-pylint
Update pylint to v4
2 parents 004093a + fd76892 commit 8f5ac85

File tree

5 files changed

+91
-122
lines changed

5 files changed

+91
-122
lines changed

src/backend/workers/python/build_package.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,4 @@ def check_tar(tarname, out_dir="."):
4545

4646

4747
if __name__ == "__main__":
48-
create_package("python_package", "python-runner friendly_traceback pylint<3.0.0 tomli typing-extensions json-tracer>=0.7.0", extra_deps="papyros")
48+
create_package("python_package", "python-runner friendly_traceback pylint>=4,<5 tomli typing-extensions json-tracer>=0.7.0", extra_deps="papyros")

src/backend/workers/python/papyros/linting.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,6 @@
77
from pylint.lint import Run
88
from pylint.reporters.text import TextReporter
99

10-
# Workaround for Pyodide + astroid: pylint hangs indefinitely when astroid
11-
# tries to recursively parse imported modules' ASTs (e.g. re, pandas).
12-
# Since we run in a single-threaded WebAssembly environment, this causes an
13-
# infinite hang. We short-circuit ALL module resolution to return synthetic
14-
# empty modules. This preserves core linting (syntax errors, undefined
15-
# variables, unused imports, style checks, custom checkers) while only
16-
# losing type-inference-based checks on imported symbols.
17-
from astroid.manager import AstroidManager as _AstroidManager
18-
from astroid.builder import AstroidBuilder as _AstroidBuilder
19-
20-
def _patched_ast_from_module_name(self, modname, context_file=None, use_cache=True):
21-
return _AstroidBuilder(self).string_build("", modname=modname)
22-
23-
_AstroidManager.ast_from_module_name = _patched_ast_from_module_name
2410

2511
PYLINT_RC_FILE = os.path.abspath("/tmp/papyros/pylint_config.rc")
2612
PYLINT_PLUGINS = "pylint_ast_checker"

src/backend/workers/python/papyros/papyros.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -305,21 +305,8 @@ def serialize_traceback(self, exc):
305305

306306
def lint(self, code):
307307
with self._without_file_tracking():
308-
# PyLint runs into an issue when trying to import its dependencies
309-
# Temporarily overriding os.devnull solves this issue
310-
TEMP_DEV_NULL = "/home/pyodide/__papyros_dev_null"
311-
with open(TEMP_DEV_NULL, "w") as f:
312-
pass
313-
orig_dev_null = os.devnull
314-
os.devnull = TEMP_DEV_NULL
315-
316308
self.set_source_code(code)
317309
from .linting import lint
318-
os.devnull = orig_dev_null
319-
try:
320-
os.remove(TEMP_DEV_NULL)
321-
except OSError:
322-
pass
323310
return lint(code)
324311

325312
def has_doctests(self, code):
Lines changed: 89 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
# source: https://github.com/dodona-edu/judge-pythia/blob/e41f6e943830f5f9b5aebe689140ec7ec53383c9/pylint_ast_checker.py
2-
import astroid
2+
from astroid import nodes as astroid_nodes
33
from pylint.checkers import BaseChecker
4-
from pylint.checkers.utils import check_messages
5-
from pylint.interfaces import IAstroidChecker
4+
from pylint.checkers.utils import only_required_for_messages
65

76
class NoOpChecker(BaseChecker):
87

9-
__implements__ = IAstroidChecker
10-
118
name = 'no_op_checker'
129

1310
msgs = {
@@ -43,59 +40,57 @@ class NoOpChecker(BaseChecker):
4340
)
4441
}
4542

46-
@check_messages('no-op-pass')
43+
@only_required_for_messages('no-op-pass')
4744
def visit_pass(self, node):
48-
49-
if isinstance(node.parent, astroid.If):
45+
46+
if isinstance(node.parent, astroid_nodes.If):
5047
if len(node.parent.orelse) > 0 and node.parent.orelse[0] is node:
5148
self.add_message('no-op-pass', node=node)
5249

53-
@check_messages('no-op-if-true')
54-
def visit_if(self,node):
55-
50+
@only_required_for_messages('no-op-if-true')
51+
def visit_if(self, node):
52+
5653
if (
57-
isinstance(node.test, astroid.Compare) and
58-
any(operation =='==' for operation,_ in node.test.ops) and
59-
any(isinstance(operand, astroid.Const) and
54+
isinstance(node.test, astroid_nodes.Compare) and
55+
any(operation == '==' for operation,_ in node.test.ops) and
56+
any(isinstance(operand, astroid_nodes.Const) and
6057
operand.value is True for operand in node.test.get_children())
6158
):
6259
self.add_message('no-op-if-true', node=node)
6360

64-
@check_messages('no-op-increment-zero', 'no-op-increment-empty', 'no-op-multiply-one')
65-
def visit_augassign(self,node):
66-
67-
if node.op == '+=' and isinstance(node.value, astroid.Const):
61+
@only_required_for_messages('no-op-increment-zero', 'no-op-increment-empty', 'no-op-multiply-one')
62+
def visit_augassign(self, node):
63+
64+
if node.op == '+=' and isinstance(node.value, astroid_nodes.Const):
6865
if node.value.value == 0:
6966
self.add_message('no-op-increment-zero', node=node)
7067
elif node.value.value == '':
71-
self.add_message('no-op-increment-empty',node=node)
68+
self.add_message('no-op-increment-empty', node=node)
7269
elif (
73-
node.op =='*=' and
74-
isinstance(node.value, astroid.Const) and
70+
node.op == '*=' and
71+
isinstance(node.value, astroid_nodes.Const) and
7572
node.value.value == 1
7673
):
7774
self.add_message('no-op-multiply-one', node=node)
7875

79-
@check_messages('no-op-useless-assign')
80-
def visit_assign(self,node):
81-
76+
@only_required_for_messages('no-op-useless-assign')
77+
def visit_assign(self, node):
78+
8279
# only keep variables, e.g. [x, y, 2] -> [x, y]
8380
child_names = [
84-
child.name for child in node.get_children()
81+
child.name for child in node.get_children()
8582
if (
86-
isinstance(child,astroid.Name) or
87-
isinstance(child, astroid.AssignName)
83+
isinstance(child, astroid_nodes.Name) or
84+
isinstance(child, astroid_nodes.AssignName)
8885
)
8986
]
90-
87+
9188
if len(child_names) > len(set(child_names)):
9289
# at least two names were equal (e.g. ['x', 'y', 'x')
9390
self.add_message('no-op-useless-assign', node=node)
9491

9592
class RewriteChecker(BaseChecker):
9693

97-
__implements__ = IAstroidChecker
98-
9994
name = 'rewrite_checker'
10095

10196
msgs = {
@@ -124,98 +119,101 @@ class RewriteChecker(BaseChecker):
124119
'is not': 'is'
125120
}
126121

127-
@check_messages('rewrite-if-pass')
128-
def visit_if(self,node):
129-
122+
@only_required_for_messages('rewrite-if-pass')
123+
def visit_if(self, node):
124+
130125
# check whether the body of the if clause starts with the pass statement
131126
# and there is an else clause
132-
if isinstance(node.body[0], astroid.Pass) and len(node.orelse) > 0:
133-
134-
# swap body of the else clause in the if clause and remove the else
127+
if isinstance(node.body[0], astroid_nodes.Pass) and len(node.orelse) > 0:
128+
129+
# swap body of the else clause in the if clause and remove the else
135130
# clause
136131
node.body = node.orelse
137132
node.orelse = []
138-
133+
139134
# invert the test
140135
# TODO: could be improved for recursive statements, etc.
141136
if (
142-
isinstance(node.test, astroid.Compare) and
137+
isinstance(node.test, astroid_nodes.Compare) and
143138
len(node.test.ops) == 1
144139
):
145140
node.test.ops[0] = (
146141
self.inverse_operators[node.test.ops[0][0]],
147142
node.test.ops[0][1]
148143
)
149144
elif (
150-
isinstance(node.test, astroid.UnaryOp) and
145+
isinstance(node.test, astroid_nodes.UnaryOp) and
151146
node.test.op == 'not'
152-
):
147+
):
153148
# not <x> becomes <x>
154149
node.test = node.test.operand
155150
else:
156-
old_test, node.test = node.test, astroid.UnaryOp()
157-
node.test.op, node.test.operand = 'not', old_test
158-
151+
old_test = node.test
152+
node.test = astroid_nodes.UnaryOp(
153+
op='not', lineno=node.lineno, col_offset=node.col_offset,
154+
parent=node, end_lineno=node.end_lineno, end_col_offset=node.end_col_offset,
155+
)
156+
node.test.operand = old_test
157+
159158
# add message with corrected node as argument
160159
self.add_message(
161-
'rewrite-if-pass',
160+
'rewrite-if-pass',
162161
node=node,
163162
args=node.as_string()
164163
)
165164

166-
@check_messages('rewrite-assign')
167-
def visit_assign(self,node):
168-
165+
@only_required_for_messages('rewrite-assign')
166+
def visit_assign(self, node):
167+
169168
# TODO: current implementation does not rewrite assignment statements
170169
# having multiple targets or recursive binary operations
171170
# (eg. x = x + 5 + 6)
172171
if len(node.targets) == 1:
173-
172+
174173
target = node.targets[0]
175-
176-
if isinstance(node.value, astroid.BinOp):
177-
174+
175+
if isinstance(node.value, astroid_nodes.BinOp):
176+
178177
rewrite = False
179-
178+
180179
# e.g. x = x + 4
181180
if (
182-
isinstance(node.value.left, astroid.Name) and
183-
isinstance(target, astroid.AssignName) and
181+
isinstance(node.value.left, astroid_nodes.Name) and
182+
isinstance(target, astroid_nodes.AssignName) and
184183
node.value.left.name == target.name
185184
):
186185
rewrite = True
187186

188187
# check for binary operations of the form x["y"] = x["y"] + 5
189188
elif (
190-
isinstance(node.value.left, astroid.Subscript) and
191-
isinstance(target, astroid.Subscript) and
192-
isinstance(node.value.left.value, astroid.Name) and
193-
isinstance(target.value, astroid.Name) and
194-
node.value.left.value.name == target.value.name and
195-
isinstance(node.value.left.slice, astroid.Index) and
196-
isinstance(target.slice, astroid.Index)
189+
isinstance(node.value.left, astroid_nodes.Subscript) and
190+
isinstance(target, astroid_nodes.Subscript) and
191+
isinstance(node.value.left.value, astroid_nodes.Name) and
192+
isinstance(target.value, astroid_nodes.Name) and
193+
node.value.left.value.name == target.value.name
197194
):
198-
# e.g. x[y] = x[y] + 1 (where y is a variable)
199-
if (
200-
isinstance(node.value.left.slice.value, astroid.Name) and
201-
isinstance(target.slice.value, astroid.Name) and
202-
node.value.left.slice.value.name == target.slice.value.name
203-
):
204-
rewrite = True
205-
# e.g. x[0] = x[0] + 1 or x["y"] = x["y"] + 1
206-
elif (
207-
isinstance(node.value.left.slice.value, astroid.Const) and
208-
isinstance(target.slice.value, astroid.Const) and
209-
node.value.left.slice.value.value == target.slice.value.value
210-
):
211-
rewrite = True
195+
# e.g. x[y] = x[y] + 1 (where y is a variable)
196+
if (
197+
isinstance(node.value.left.slice, astroid_nodes.Name) and
198+
isinstance(target.slice, astroid_nodes.Name) and
199+
node.value.left.slice.name == target.slice.name
200+
):
201+
rewrite = True
202+
# e.g. x[0] = x[0] + 1 or x["y"] = x["y"] + 1
203+
elif (
204+
isinstance(node.value.left.slice, astroid_nodes.Const) and
205+
isinstance(target.slice, astroid_nodes.Const) and
206+
node.value.left.slice.value == target.slice.value
207+
):
208+
rewrite = True
212209

213210
if rewrite:
214-
215-
newnode = astroid.AugAssign()
211+
212+
newnode = astroid_nodes.AugAssign(
213+
op=node.value.op + '=', lineno=node.lineno, col_offset=node.col_offset,
214+
parent=node.parent, end_lineno=node.end_lineno, end_col_offset=node.end_col_offset,
215+
)
216216
newnode.target = target
217-
# node.value.op is of the form '+', '-' , '/' , '*'
218-
newnode.op = node.value.op + '='
219217
newnode.value = node.value.right
220218
self.add_message(
221219
'rewrite-assign',
@@ -224,11 +222,9 @@ def visit_assign(self,node):
224222
)
225223

226224
class UnnecessaryConversionChecker(BaseChecker):
227-
228-
__implements__ = IAstroidChecker
229-
225+
230226
name = 'un_conv_checker'
231-
227+
232228
msgs = {
233229
'C0009': (
234230
'Useless call to int(): no type conversion required.',
@@ -247,28 +243,28 @@ class UnnecessaryConversionChecker(BaseChecker):
247243
)
248244
}
249245

250-
@check_messages('un-conv-int', 'un-conv-float', 'un-conv-str')
251-
def visit_callfunc(self, node):
252-
253-
#check if 'regular' function:
254-
if hasattr(node.func, 'name'):
246+
@only_required_for_messages('un-conv-int', 'un-conv-float', 'un-conv-str')
247+
def visit_call(self, node):
248+
249+
# check if 'regular' function:
250+
if hasattr(node.func, 'name'):
255251

256252
# case: str(input(...))
257253
if (
258254
# check if parent is str()
259255
node.func.name == 'input' and
260-
isinstance(node.parent, astroid.Call) and
256+
isinstance(node.parent, astroid_nodes.Call) and
261257
hasattr(node.parent.func, 'name') and
262258
node.parent.func.name == 'str' and
263259
# limited to a single argument
264260
len(node.parent.args) == 1
265261
):
266262
self.add_message('un-conv-str', node=node.parent)
267-
263+
268264
# check if a single constant argument is passed
269265
elif (
270266
len(node.args) == 1 and
271-
isinstance(node.args[0], astroid.Const)
267+
isinstance(node.args[0], astroid_nodes.Const)
272268
):
273269
# case: int(<builtin.int>)
274270
if (
@@ -290,11 +286,11 @@ def visit_callfunc(self, node):
290286
self.add_message('un-conv-str', node=node)
291287

292288
def register(linter):
293-
289+
294290
"""
295291
Required method to auto register custom checkers to pylint.
296292
"""
297-
293+
298294
linter.register_checker(NoOpChecker(linter))
299295
linter.register_checker(RewriteChecker(linter))
300296
linter.register_checker(UnnecessaryConversionChecker(linter))

src/backend/workers/python/papyros/pylint_config.rc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,5 @@ const-rgx=[_A-Za-z0-9]{1,30}$
4343
# I0011 Warning locally suppressed using disable-msg
4444
# I0012 Warning locally suppressed using disable-msg
4545
# old version: disable=I0011,I0012,W0704,W0142,W0212,W0232,W0702,R0201,W0614,R0914,R0912,R0915,R0913,R0904,R0801,C0303,C0111,C0304,R0903,W0141,W0621,C0301,W0631,R0911,C1001
46-
disable=W0311,W0621,W0622,R0902,R0903,C0111,C0301,C0303,C0304,C0413,I0011,E0401,E0611,E1101
46+
disable=W0311,W0621,W0622,R0902,R0903,C0114,C0115,C0116,C0301,C0303,C0304,C0413,I0011
4747
evaluation=max(10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10), 0)

0 commit comments

Comments
 (0)