Skip to content

Commit 7fe9db1

Browse files
committed
[FIX] *: support for pylint 4
- astroid 4 deprecates toplevel exports of nodes, thankfully that was never actually necessary so we can just import that unconditionally - remove support for pre-jammy pylint / astroid, specifically `astroid.nodes` was added in astroid 2.7.0 and `astroid.node_classes` deprecated then and removed in 3.0, this can affect Bullseye users as it shipped with astroid 2.5 - Astroid 4 changes `spec.Finder.find_module` in order to cache it (pylint-dev/astroid#2509), we can just make our method static for all versions as we don't need `self` anyway. - The mail test triggers `function-redefined` (E0102), fix it. - Skip the escpos script thing which triggers a bunch of `undefined-variable` (E0602) false positives. closes odoo#236993 X-original-commit: 628b693 Related: odoo/enterprise#100228 Signed-off-by: Xavier Morel (xmo) <[email protected]>
1 parent 5d0ff25 commit 7fe9db1

File tree

5 files changed

+110
-131
lines changed

5 files changed

+110
-131
lines changed

addons/lunch/models/lunch_product.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,13 @@ def _sync_active_from_related(self):
9090
self.filtered(lambda p: not p.active and (p.category_id.active and p.supplier_id.active)).action_unarchive()
9191

9292
@api.constrains('active', 'category_id')
93-
def _check_active(self):
93+
def _check_active_categories(self):
9494
invalid_products = self.filtered(lambda product: product.active and not product.category_id.active)
9595
if invalid_products:
9696
raise UserError(_("The following product categories are archived. You should either unarchive the categories or change the category of the product.\n%s", '\n'.join(invalid_products.category_id.mapped('name'))))
9797

9898
@api.constrains('active', 'supplier_id')
99-
def _check_active(self):
99+
def _check_active_suppliers(self):
100100
invalid_products = self.filtered(lambda product: product.active and not product.supplier_id.active)
101101
if invalid_products:
102102
raise UserError(_("The following suppliers are archived. You should either unarchive the suppliers or change the supplier of the product.\n%s", '\n'.join(invalid_products.supplier_id.mapped('name'))))

addons/test_mail/tests/test_mail_mail.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
# Part of Odoo. See LICENSE file for full copyright and licensing details.
23

34
import pytz
@@ -744,16 +745,16 @@ def _connect(*args, **kwargs):
744745
# SMTP sending issues
745746
with self.mock_mail_gateway():
746747
_send_current = self.send_email_mocked.side_effect
748+
self.addCleanup(setattr, self.send_email_mocked, 'side_effect', _send_current)
749+
747750
self._reset_data()
748751
mail.write({'email_to': '[email protected]'})
749752

750753
# should always raise for those errors, even with raise_exception=False
751754
for error, error_class in [
752755
(smtplib.SMTPServerDisconnected("Some exception"), smtplib.SMTPServerDisconnected),
753756
(MemoryError("Some exception"), MemoryError)]:
754-
def _send_email(*args, **kwargs):
755-
raise error
756-
self.send_email_mocked.side_effect = _send_email
757+
self.send_email_mocked.side_effect = error
757758

758759
with self.assertRaises(error_class):
759760
mail.send(raise_exception=False)
@@ -770,9 +771,7 @@ def _send_email(*args, **kwargs):
770771
(MailDeliveryException("Some exception"), 'Some exception', 'unknown'),
771772
(MailDeliveryException("OutboundSpamException"), 'OutboundSpamException', 'mail_spam'),
772773
(ValueError("Unexpected issue"), 'Unexpected issue', 'unknown')]:
773-
def _send_email(*args, **kwargs):
774-
raise error
775-
self.send_email_mocked.side_effect = _send_email
774+
self.send_email_mocked.side_effect = error
776775

777776
self._reset_data()
778777
mail.send(raise_exception=False)
@@ -783,8 +782,6 @@ def _send_email(*args, **kwargs):
783782
self.assertEqual(notification.failure_type, failure_type)
784783
self.assertEqual(notification.notification_status, 'exception')
785784

786-
self.send_email_mocked.side_effect = _send_current
787-
788785
def test_mail_mail_values_misc(self):
789786
""" Test various values on mail.mail, notably default values """
790787
msg = self.env['mail.mail'].create({})

odoo/addons/test_lint/tests/_odoo_checker_sql_injection.py

Lines changed: 66 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,7 @@
77
from contextlib import ExitStack
88
from typing import Optional
99

10-
import astroid
11-
12-
try:
13-
from astroid import NodeNG
14-
except ImportError:
15-
from astroid.node_classes import NodeNG
10+
from astroid import nodes
1611

1712
import pylint.interfaces
1813
from pylint.checkers import BaseChecker, utils
@@ -40,10 +35,10 @@
4035

4136
function_definitions = collections.defaultdict(list)
4237
callsites_for_queries = collections.defaultdict(list)
43-
root_call: contextvars.ContextVar[Optional[astroid.Call]] =\
38+
root_call: contextvars.ContextVar[Optional[nodes.Call]] =\
4439
contextvars.ContextVar('root_call', default=None)
4540
@contextlib.contextmanager
46-
def push_call(node: astroid.Call):
41+
def push_call(node: nodes.Call):
4742
with ExitStack() as s:
4843
if root_call.get() is None:
4944
t = root_call.set(node)
@@ -77,39 +72,39 @@ def _infer_filename(self, node):
7772
return node.file
7873
def _get_return_node(self, node):
7974
ret = []
80-
nodes = deque([node])
81-
while nodes:
82-
node = nodes.popleft()
83-
if isinstance(node, astroid.Return):
75+
q = deque([node])
76+
while q:
77+
node = q.popleft()
78+
if isinstance(node, nodes.Return):
8479
ret.append(node)
8580
else:
86-
nodes.extend(node.get_children())
81+
q.extend(node.get_children())
8782
return ret
8883

8984
def _is_asserted(self, node): # If there is an assert on the value of the node, it's very likely to be safe
90-
asserted = deque((assert_.test for assert_ in node.scope().nodes_of_class(astroid.Assert)))
85+
asserted = deque(assert_.test for assert_ in node.scope().nodes_of_class(nodes.Assert))
9186
while asserted:
9287
n = asserted.popleft()
93-
if isinstance(n, astroid.Name) and n.name == node.name:
88+
if isinstance(n, nodes.Name) and n.name == node.name:
9489
return True
9590
else:
9691
asserted.extend(n.get_children())
9792
return False
9893

9994
def _get_attribute_chain(self, node):
100-
if isinstance(node, astroid.Attribute):
95+
if isinstance(node, nodes.Attribute):
10196
return self._get_attribute_chain(node.expr) + '.' + node.attrname
102-
elif isinstance(node, astroid.Name):
97+
elif isinstance(node, nodes.Name):
10398
return node.name
104-
elif isinstance(node, astroid.Call):
99+
elif isinstance(node, nodes.Call):
105100
return self._get_attribute_chain(node.func)
106101
return '' #FIXME
107102

108103
def _evaluate_function_call(self, node, args_allowed, position):
109-
name = node.func.attrname if isinstance(node.func, astroid.Attribute) else node.func.name
104+
name = node.func.attrname if isinstance(node.func, nodes.Attribute) else node.func.name
110105
if name == 'SQL':
111106
return True
112-
if isinstance(node.scope(), astroid.GeneratorExp):
107+
if isinstance(node.scope(), nodes.GeneratorExp):
113108
return True
114109
if name == node.scope().name:
115110
return True
@@ -125,13 +120,13 @@ def _evaluate_function_call(self, node, args_allowed, position):
125120
)
126121
return True
127122

128-
def _is_fstring_cst(self, node: astroid.JoinedStr, args_allowed=False, position=None):
123+
def _is_fstring_cst(self, node: nodes.JoinedStr, args_allowed=False, position=None):
129124
# an fstring is constant if all its FormattedValue are constant, or
130125
# are access to private attributes (nb: whitelist?)
131126
return self.all_const((
132127
node.value for node in node.values
133-
if isinstance(node, astroid.FormattedValue)
134-
if not (isinstance(node.value, astroid.Attribute) and node.value.attrname.startswith('_'))
128+
if isinstance(node, nodes.FormattedValue)
129+
if not (isinstance(node.value, nodes.Attribute) and node.value.attrname.startswith('_'))
135130
),
136131
args_allowed=args_allowed,
137132
position=position
@@ -143,72 +138,70 @@ def all_const(self, nodes, args_allowed=False, position=None):
143138
for node in nodes
144139
)
145140

146-
def _is_constexpr(self, node: NodeNG, *, args_allowed=False, position=None):
147-
if isinstance(node, astroid.Const): # astroid.const is always safe
141+
def _is_constexpr(self, node: nodes.NodeNG, *, args_allowed=False, position=None):
142+
if isinstance(node, nodes.Const): # astroid.const is always safe
148143
return True
149-
elif isinstance(node, (astroid.List, astroid.Set)):
144+
elif isinstance(node, (nodes.List, nodes.Set)):
150145
return self.all_const(node.elts, args_allowed=args_allowed)
151-
elif isinstance(node, astroid.Tuple):
146+
elif isinstance(node, nodes.Tuple):
152147
if position is None:
153148
return self.all_const(node.elts, args_allowed=args_allowed)
154149
else:
155150
return self._is_constexpr(node.elts[position], args_allowed=args_allowed)
156-
elif isinstance(node, astroid.Dict):
151+
elif isinstance(node, nodes.Dict):
157152
return all(
158153
self._is_constexpr(k, args_allowed=args_allowed) and self._is_constexpr(v, args_allowed=args_allowed)
159154
for k, v in node.items
160155
)
161-
elif isinstance(node, astroid.Starred):
156+
elif isinstance(node, nodes.Starred):
162157
return self._is_constexpr(node.value, args_allowed=args_allowed, position=position)
163-
elif isinstance(node, astroid.BinOp): # recusively infer both side of the operation. Failing if either side is not inferable
158+
elif isinstance(node, nodes.BinOp): # recusively infer both side of the operation. Failing if either side is not inferable
164159
left_operand = self._is_constexpr(node.left, args_allowed=args_allowed)
165160
# This case allows to always consider a string formatted with %d to be safe
166161
if node.op == '%' and \
167-
isinstance(node.left, astroid.Const) and \
162+
isinstance(node.left, nodes.Const) and \
168163
node.left.pytype() == 'builtins.str' and \
169164
'%d' in node.left.value and \
170165
not '%s' in node.left.value:
171166
return True
172167
right_operand = self._is_constexpr(node.right, args_allowed=args_allowed)
173168
return left_operand and right_operand
174-
elif isinstance(node, astroid.Name) or isinstance(node, astroid.AssignName): # Variable: find the assignement instruction in the AST and infer its value.
175-
assignements = node.lookup(node.name)
169+
elif isinstance(node, (nodes.Name, nodes.AssignName)): # Variable: find the assignement instruction in the AST and infer its value.
170+
assignment = node.lookup(node.name)
176171
assigned_node = []
177-
for n in assignements[1]: #assignement[0] contains the scope, so assignment[1] contains the assignement nodes
172+
for n in assignment[1]: # assignment[0] contains the scope, so assignment[1] contains the assignement nodes
178173
# FIXME: makes no sense, assuming this gets
179174
# `visit_functiondef`'d we should just ignore it
180-
if isinstance(n.parent, astroid.FunctionDef):
175+
if isinstance(n.parent, (nodes.FunctionDef, nodes.Arguments)):
181176
assigned_node += [args_allowed]
182-
elif isinstance(n.parent, astroid.Arguments):
183-
assigned_node += [args_allowed]
184-
elif isinstance(n.parent, astroid.Tuple): # multi assign a,b = (a,b)
177+
elif isinstance(n.parent, nodes.Tuple): # multi assign a,b = (a,b)
185178
statement = n.statement()
186-
if isinstance(statement, astroid.For):
179+
if isinstance(statement, nodes.For):
187180
assigned_node += [self._is_constexpr(statement.iter, args_allowed=args_allowed)]
188-
elif isinstance(statement, astroid.Assign):
181+
elif isinstance(statement, nodes.Assign):
189182
assigned_node += [self._is_constexpr(statement.value, args_allowed=args_allowed, position=n.parent.elts.index(n))]
190183
else:
191184
raise TypeError(f"Expected statement Assign or For, got {statement}")
192-
elif isinstance(n.parent, astroid.For):
185+
elif isinstance(n.parent, nodes.For):
193186
assigned_node.append(self._is_constexpr(n.parent.iter, args_allowed=args_allowed))
194-
elif isinstance(n.parent, astroid.AugAssign):
187+
elif isinstance(n.parent, nodes.AugAssign):
195188
left = self._is_constexpr(n.parent.target, args_allowed=args_allowed)
196189
right = self._is_constexpr(n.parent.value, args_allowed=args_allowed)
197190
assigned_node.append(left and right)
198-
elif isinstance(n.parent, astroid.Module):
191+
elif isinstance(n.parent, nodes.Module):
199192
return True
200193
else:
201-
if isinstance(n.parent, astroid.Comprehension):
194+
if isinstance(n.parent, nodes.Comprehension):
202195
assigned_node += [self._is_constexpr(n.parent.iter, args_allowed=args_allowed)]
203196
else:
204197
assigned_node += [self._is_constexpr(n.parent.value, args_allowed=args_allowed)]
205198
if assigned_node and all(assigned_node):
206199
return True
207200
return self._is_asserted(node)
208-
elif isinstance(node, astroid.JoinedStr):
201+
elif isinstance(node, nodes.JoinedStr):
209202
return self._is_fstring_cst(node, args_allowed)
210-
elif isinstance(node, astroid.Call):
211-
if isinstance(node.func, astroid.Attribute):
203+
elif isinstance(node, nodes.Call):
204+
if isinstance(node.func, nodes.Attribute):
212205
if node.func.attrname == 'append':
213206
return self._is_constexpr(node.args[0])
214207
elif node.func.attrname == 'format':
@@ -219,16 +212,16 @@ def _is_constexpr(self, node: NodeNG, *, args_allowed=False, position=None):
219212
)
220213
with push_call(node):
221214
return self._evaluate_function_call(node, args_allowed=args_allowed, position=position)
222-
elif isinstance(node, astroid.IfExp):
215+
elif isinstance(node, nodes.IfExp):
223216
body = self._is_constexpr(node.body, args_allowed=args_allowed)
224217
orelse = self._is_constexpr(node.orelse, args_allowed=args_allowed)
225218
return body and orelse
226-
elif isinstance(node, astroid.Subscript):
219+
elif isinstance(node, nodes.Subscript):
227220
return self._is_constexpr(node.value, args_allowed=args_allowed)
228-
elif isinstance(node, astroid.BoolOp):
221+
elif isinstance(node, nodes.BoolOp):
229222
return self.all_const(node.values, args_allowed=args_allowed)
230223

231-
elif isinstance(node, astroid.Attribute):
224+
elif isinstance(node, nodes.Attribute):
232225
attr_chain = self._get_attribute_chain(node)
233226
while attr_chain:
234227
if attr_chain in ATTRIBUTE_WHITELIST or attr_chain.startswith('_'):
@@ -243,20 +236,17 @@ def _is_constexpr(self, node: NodeNG, *, args_allowed=False, position=None):
243236
def _get_cursor_name(self, node):
244237
expr_list = []
245238
node_expr = node.expr
246-
while isinstance(node_expr, astroid.Attribute):
239+
while isinstance(node_expr, nodes.Attribute):
247240
expr_list.insert(0, node_expr.attrname)
248241
node_expr = node_expr.expr
249-
if isinstance(node_expr, astroid.Name):
242+
if isinstance(node_expr, nodes.Name):
250243
expr_list.insert(0, node_expr.name)
251244
cursor_name = '.'.join(expr_list)
252245
return cursor_name
253246

254-
def _allowable(self, node):
255-
"""
256-
:type node: NodeNG
257-
"""
247+
def _allowable(self, node: nodes.NodeNG) -> bool:
258248
scope = node.scope()
259-
if isinstance(scope, astroid.FunctionDef) and (scope.name.startswith("_") or scope.name == 'init'):
249+
if isinstance(scope, nodes.FunctionDef) and (scope.name.startswith("_") or scope.name == 'init'):
260250
return True
261251

262252
infered = utils.safe_infer(node)
@@ -269,23 +259,23 @@ def _allowable(self, node):
269259

270260
# self._thing is OK (mostly self._table), self._thing() also because
271261
# it's a common pattern of reports (self._select, self._group_by, ...)
272-
return (isinstance(node, astroid.Attribute)
273-
and isinstance(node.expr, astroid.Name)
262+
return (isinstance(node, nodes.Attribute)
263+
and isinstance(node.expr, nodes.Name)
274264
and node.attrname.startswith('_')
275265
)
276266

277-
def _check_concatenation(self, node):
267+
def _check_concatenation(self, node: nodes.NodeNG) -> bool | None:
278268
node = self.resolve(node)
279269

280270
if self._allowable(node):
281271
return False
282272

283-
if isinstance(node, astroid.BinOp) and node.op in ('%', '+'):
284-
if isinstance(node.right, astroid.Tuple):
273+
if isinstance(node, nodes.BinOp) and node.op in ('%', '+'):
274+
if isinstance(node.right, nodes.Tuple):
285275
# execute("..." % (self._table, thing))
286276
if not all(map(self._allowable, node.right.elts)):
287277
return True
288-
elif isinstance(node.right, astroid.Dict):
278+
elif isinstance(node.right, nodes.Dict):
289279
# execute("..." % {'table': self._table}
290280
if not all(self._allowable(v) for _, v in node.right.items):
291281
return True
@@ -308,8 +298,8 @@ def _check_concatenation(self, node):
308298
return self._check_concatenation(node.left)
309299

310300
# check execute("...".format(self._table, table=self._table))
311-
if isinstance(node, astroid.Call) \
312-
and isinstance(node.func, astroid.Attribute) \
301+
if isinstance(node, nodes.Call) \
302+
and isinstance(node.func, nodes.Attribute) \
313303
and node.func.attrname == 'format':
314304

315305
return not (
@@ -318,18 +308,20 @@ def _check_concatenation(self, node):
318308
)
319309

320310
# check execute(f'foo {...}')
321-
if isinstance(node, astroid.JoinedStr):
311+
if isinstance(node, nodes.JoinedStr):
322312
return not all(
323313
self._allowable(formatted.value)
324-
for formatted in node.nodes_of_class(astroid.FormattedValue)
314+
for formatted in node.nodes_of_class(nodes.FormattedValue)
325315
)
326316

317+
return None
318+
327319
def resolve(self, node):
328320
# if node is a variable, find how it was built
329-
if isinstance(node, astroid.Name):
321+
if isinstance(node, nodes.Name):
330322
for target in node.lookup(node.name)[1]:
331323
# could also be e.g. arguments (if the source is a function parameter)
332-
if isinstance(target.parent, astroid.Assign):
324+
if isinstance(target.parent, nodes.Assign):
333325
# FIXME: handle multiple results (e.g. conditional assignment)
334326
return target.parent.value
335327
# otherwise just return the original node for checking
@@ -341,9 +333,9 @@ def _check_sql_injection_risky(self, node):
341333
current_file_bname = os.path.basename(self.linter.current_file)
342334
if not (
343335
# .execute() or .executemany()
344-
isinstance(node, astroid.Call) and node.args and
345-
((isinstance(node.func, astroid.Attribute) and node.func.attrname in ('execute', 'executemany', 'SQL') and self._get_cursor_name(node.func) in DFTL_CURSOR_EXPR) or
346-
(isinstance(node.func, astroid.Name) and node.func.name == 'SQL')) and
336+
isinstance(node, nodes.Call) and node.args and
337+
((isinstance(node.func, nodes.Attribute) and node.func.attrname in ('execute', 'executemany', 'SQL') and self._get_cursor_name(node.func) in DFTL_CURSOR_EXPR) or
338+
(isinstance(node.func, nodes.Name) and node.func.name == 'SQL')) and
347339
# ignore in test files, probably not accessible
348340
not current_file_bname.startswith('test_')
349341
):
@@ -387,7 +379,7 @@ def visit_functiondef(self, node):
387379
)
388380

389381
@functools.lru_cache(None)
390-
def _is_const_def(self, node: astroid.FunctionDef, /, *, position: Optional[int], const_args: bool = False) -> bool:
382+
def _is_const_def(self, node: nodes.FunctionDef, /, *, position: Optional[int], const_args: bool = False) -> bool:
391383
if node.name.startswith('__') or node.name in FUNCTION_WHITELIST:
392384
return True
393385

odoo/addons/test_lint/tests/_pylint_path_setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ class AddonsPackageFinder(spec.Finder):
4343
forcefully discovered as namespace packages since otherwise they get
4444
discovered as directory packages.
4545
"""
46+
@staticmethod
4647
def find_module(
47-
self,
4848
modname: str,
4949
module_parts: Sequence[str],
5050
processed: list[str],

0 commit comments

Comments
 (0)